o2o-site-AEO/solution/backend/tests/test_site_slug.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

129 lines
5.5 KiB
Python

"""사이트 주소(네임스페이스) 확인·예약."""
import uuid
import pytest
from sqlalchemy import text
from common.enums import ErrorType, SiteStatus
from services import site_slug
async def _place(client, headers, name="주소펜션"):
r = await client.post("/v1/place", headers=headers, json={"name": name, "category": 1})
return r.json()["place"]["place_id"]
async def _check(client, headers, pid, slug):
return (await client.get(f"/v1/place/{pid}/site/slug/check", headers=headers, params={"slug": slug})).json()
async def _reserve(client, headers, pid, slug):
return (await client.post(f"/v1/place/{pid}/site/slug", headers=headers, json={"slug": slug})).json()
@pytest.mark.parametrize(
"slug, reason",
[
("doflo", None),
("stay-mumum-2", None),
("도플로", site_slug.REASON_FORMAT), # 한글
("Doflo", site_slug.REASON_FORMAT), # 대문자
("my_site", site_slug.REASON_FORMAT), # 언더스코어
("a--b", site_slug.REASON_FORMAT), # 연속 하이픈(퓨니코드 접두 xn-- 와 헷갈린다)
("-doflo", site_slug.REASON_FORMAT),
("doflo-", site_slug.REASON_FORMAT),
("ab", site_slug.REASON_LENGTH), # 3자 미만
("a" * 51, site_slug.REASON_LENGTH), # 50자 초과
("admin", site_slug.REASON_RESERVED),
("api", site_slug.REASON_RESERVED),
("robots", site_slug.REASON_RESERVED),
("undefined", site_slug.REASON_RESERVED), # 프론트 버그가 그대로 주소가 되는 걸 막는다
],
)
def test_slug_rules(slug, reason):
"""검증: 형식·예약어 규칙(확인과 저장이 같이 쓰는 단 하나의 정의)."""
assert site_slug.validate_slug(slug) == reason
async def test_check_then_reserve(auth_headers, client):
"""검증: 주소를 확인하고 예약한다."""
h = await auth_headers("slug1")
pid = await _place(client, h)
assert (await _check(client, h, pid, "doflo"))["available"] is True
saved = await _reserve(client, h, pid, "doflo")
assert saved["result"]["code"] == ErrorType.SUCCESS.value
assert saved["site"]["domain"] == "doflo"
# 자기 자신은 중복이 아니다 — 저장해 둔 화면을 다시 열었을 때 '중복'이라고 하면 안 된다.
again = await _check(client, h, pid, "doflo")
assert again["available"] is True and "reason" not in again
async def test_invalid_slug_is_refused_on_save_too(auth_headers, client):
"""검증: 확인에서 막힌 값은 저장에서도 막힌다(클라이언트 검증을 믿지 않는다)."""
h = await auth_headers("slug2")
pid = await _place(client, h)
for slug, reason in (("도플로", site_slug.REASON_FORMAT), ("ab", site_slug.REASON_LENGTH),
("admin", site_slug.REASON_RESERVED)):
checked = await _check(client, h, pid, slug)
assert checked["available"] is False and checked["reason"] == reason
saved = await _reserve(client, h, pid, slug)
assert saved["result"]["success"] is False and saved["reason"] == reason
assert "site" not in saved
async def test_taken_by_other_place_offers_suggestion(auth_headers, client):
"""검증: 남이 쓰는 주소는 못 쓴다."""
h = await auth_headers("slug3")
mine = await _place(client, h, "도플로")
other = await _place(client, h, "도플로2호점")
await _reserve(client, h, mine, "doflo")
checked = await _check(client, h, other, "doflo")
assert checked["available"] is False
assert checked["reason"] == site_slug.REASON_TAKEN
assert checked["suggestion"] == "doflo-2"
saved = await _reserve(client, h, other, "doflo")
assert saved["result"]["code"] == ErrorType.DB_ALREADY_SAME_KEY.value
assert saved["suggestion"] == "doflo-2"
# 제안대로면 통과한다 — 제안이 실제로 쓸 수 있는 값이어야 의미가 있다.
assert (await _reserve(client, h, other, "doflo-2"))["site"]["domain"] == "doflo-2"
async def test_published_site_slug_is_locked(auth_headers, client, db_engine):
"""검증: ★ 이미 발행된 사이트의 주소는 바꿀 수 없다."""
h = await auth_headers("slug4")
pid = await _place(client, h)
await _reserve(client, h, pid, "published-stay")
# 발행 상태를 만든다(빌드 잡을 돌리는 대신 상태만) — 잠금은 published_at·status 가 근거다.
async with db_engine.begin() as conn:
await conn.execute(
text("UPDATE sites SET status = :st, published_at = now() WHERE place_id = :p"),
{"st": SiteStatus.PUBLISHED.value, "p": uuid.UUID(pid)},
)
locked = await _reserve(client, h, pid, "published-stay-new")
assert locked["result"]["code"] == ErrorType.SITE_SLUG_LOCKED.value
assert locked["reason"] == site_slug.REASON_LOCKED
# 같은 값 재전송은 변경이 아니다.
same = await _reserve(client, h, pid, "published-stay")
assert same["result"]["code"] == ErrorType.SUCCESS.value
assert same["site"]["domain"] == "published-stay"
async def test_other_owners_place_is_blocked(auth_headers, client):
"""검증: 남의 사업장 주소는 확인도 예약도 못 한다."""
h = await auth_headers("slug5")
intruder = await auth_headers("slug6")
pid = await _place(client, h)
assert (await _check(client, intruder, pid, "doflo"))["result"]["code"] == ErrorType.PLACE_NOT_FOUND.value
assert (await _reserve(client, intruder, pid, "doflo"))["result"]["code"] == ErrorType.PLACE_NOT_FOUND.value