여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - 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>
129 lines
5.5 KiB
Python
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
|