"""공개 쇼케이스 목록 — 랜딩이 로그인 없이 부르는 유일한 사이트 조회. 이 경로가 절대 하면 안 되는 것: - 발행되지 않은 사이트를 보여주는 것 — 열면 404 다. - 사업장 식별자·전화번호·상세 주소를 흘리는 것. 사이트 한 곳을 여는 것과 발행 업소 명단을 통째로 긁는 것은 다른 일이다. """ import uuid from datetime import datetime, timedelta, timezone from sqlalchemy import text from common.enums import PlaceCategory, PlaceStatus, SiteStatus async def _publish(db_engine, owner_id, name, *, status, domain, thumb=None, minutes_ago=0): """places + sites 를 직접 넣는다 — 여기서 보는 건 목록 조회지 빌드 파이프라인이 아니다.""" pid, sid = uuid.uuid4(), uuid.uuid4() async with db_engine.begin() as c: await c.execute( text("INSERT INTO places (place_id, owner_user_id, name, category, status, road_address, address, phone) " "VALUES (:p,:c,:n,:cat,:st,:road,:addr,:phone)"), {"p": pid, "c": uuid.UUID(owner_id), "n": name, "cat": PlaceCategory.LODGING.value, "st": PlaceStatus.PUBLISHED.value, "road": "강원특별자치도 양양군 현북면 하조대3길 12-3", "addr": "강원특별자치도 양양군 현북면 하광정리 1-2", "phone": "033-672-0000"}, ) await c.execute( text("INSERT INTO sites (site_id, place_id, domain, status, thumbnail_url, published_at) " "VALUES (:s,:p,:d,:st,:t,:at)"), {"s": sid, "p": pid, "d": domain, "st": status, "t": thumb, "at": datetime.now(timezone.utc) - timedelta(minutes=minutes_ago)}, ) return str(pid) async def test_발행된_사이트만_로그인_없이_보인다(client, db_engine, owner_id): """검증: 발행본 1개 + 미발행(DRAFT) 1개를 두고 인증 헤더 없이 부른다. 기대결과: 발행본만 나온다.""" await _publish(db_engine, owner_id, "하조대펜션", status=SiteStatus.PUBLISHED.value, domain="hajodae", thumb="https://w4ai.o2o.kr/thumbs/hajodae.jpg") await _publish(db_engine, owner_id, "아직펜션", status=SiteStatus.DRAFT.value, domain="notyet") res = await client.get("/v1/showcase") assert res.status_code == 200 items = res.json()["items"] assert [i["name"] for i in items] == ["하조대펜션"] assert items[0]["url"] == "/s/hajodae" assert items[0]["thumbnail_url"] == "https://w4ai.o2o.kr/thumbs/hajodae.jpg" assert items[0]["category"] == PlaceCategory.LODGING.value async def test_개인정보와_내부값은_나가지_않는다(client, db_engine, owner_id): """검증: 응답 항목의 키를 그대로 본다. 기대결과: 상호명·업종·지역·주소·썸네일뿐. 지역은 시·군까지고 상세 주소는 없다.""" await _publish(db_engine, owner_id, "하조대펜션", status=SiteStatus.PUBLISHED.value, domain="hajodae") item = (await client.get("/v1/showcase")).json()["items"][0] assert set(item) <= {"name", "category", "region", "url", "thumbnail_url"} assert item["region"] == "강원특별자치도 양양군" # 번지·전화번호가 어디로도 새지 않았는지 통째로 확인한다. body = (await client.get("/v1/showcase")).text assert "033-672-0000" not in body assert "하조대3길" not in body async def test_썸네일이_없으면_키가_없다(client, db_engine, owner_id): """★ 썸네일은 발행의 부수 효과라 실패할 수 있다(대표 사진이 없거나 CDN 이 죽었거나). 그때 카드는 글자로 떨어져야지 목록에서 사라지면 안 된다.""" await _publish(db_engine, owner_id, "그림없는집", status=SiteStatus.PUBLISHED.value, domain="nopic") item = (await client.get("/v1/showcase")).json()["items"][0] assert item["name"] == "그림없는집" assert "thumbnail_url" not in item async def test_최신_발행순이고_limit_로_자른다(client, db_engine, owner_id): await _publish(db_engine, owner_id, "먼저", status=SiteStatus.PUBLISHED.value, domain="first", minutes_ago=60) await _publish(db_engine, owner_id, "나중", status=SiteStatus.PUBLISHED.value, domain="second", minutes_ago=1) items = (await client.get("/v1/showcase")).json()["items"] assert [i["name"] for i in items] == ["나중", "먼저"] items = (await client.get("/v1/showcase?limit=1")).json()["items"] assert [i["name"] for i in items] == ["나중"] # 상한 밖은 거부한다 — 전체 명단을 한 번에 긁어가는 요청을 열어 두지 않는다. assert (await client.get("/v1/showcase?limit=999")).status_code == 422