랜딩의 "이렇게 나옵니다" 섹션이 걸 그림이 없었다. 발행은 되는데 그 사이트가 어떻게 생겼는지 밖에서 알 방법이 payload 안에만 있었다. ★ 스크린샷이 아니라 그 사이트의 대표 사진(og:image)이다. 헤드리스 브라우저는 봇 탐지 우회 우려로 영구 금지돼 있고(DECISIONS 1-1), 워커(python:slim)· 프리렌더(node:alpine) 어디에도 Chromium 이 없다. - site_thumbnail: 대표 사진을 받아 <prefix>/thumbs/<slug>.<ext> 로 올린다. s/<slug>/ 안에 두지 않는 이유 — _remove_stale_site_files 가 매 발행마다 그 경로를 프리렌더 산출물로 통째로 교체해 조용히 지운다 - site_payload: primary_media()·publish_origin()·region_label() 공개. isPrimary 계산을 한 곳으로 모아 og:image 와 썸네일이 갈릴 수 없게 했다 - build_service: azure publish 직후·IndexNow 전에 저장. 실패해도 발행은 그대로 (payload 와 같은 원칙). thumbnail_url 은 발행 상태 전이 UPDATE 에 합쳐 1회 - GET /v1/showcase: 인증 없음. 발행된 사이트만, place_id·전화·상세주소는 안 나간다 - conftest: fake_renderer 가 늘 ok=True 라 NO_UNIQUE_CONTENT 되짚기 경로가 통째로 안 돌고 있었다(기존에 깨져 있던 테스트 4건 포함 수정) 전체 562 passed
92 lines
4.7 KiB
Python
92 lines
4.7 KiB
Python
"""공개 쇼케이스 목록 — 랜딩이 로그인 없이 부르는 유일한 사이트 조회.
|
|
|
|
이 경로가 절대 하면 안 되는 것:
|
|
- 발행되지 않은 사이트를 보여주는 것 — 열면 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, company_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, company_id, name, category, status, road_address, address, phone) "
|
|
"VALUES (:p,:c,:n,:cat,:st,:road,:addr,:phone)"),
|
|
{"p": pid, "c": uuid.UUID(company_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, company_id):
|
|
"""검증: 발행본 1개 + 미발행(DRAFT) 1개를 두고 인증 헤더 없이 부른다.
|
|
기대결과: 발행본만 나온다."""
|
|
await _publish(db_engine, company_id, "하조대펜션", status=SiteStatus.PUBLISHED.value,
|
|
domain="hajodae", thumb="https://w4ai.o2o.kr/thumbs/hajodae.jpg")
|
|
await _publish(db_engine, company_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, company_id):
|
|
"""검증: 응답 항목의 키를 그대로 본다.
|
|
기대결과: 상호명·업종·지역·주소·썸네일뿐. 지역은 시·군까지고 상세 주소는 없다."""
|
|
await _publish(db_engine, company_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, company_id):
|
|
"""★ 썸네일은 발행의 부수 효과라 실패할 수 있다(대표 사진이 없거나 CDN 이 죽었거나).
|
|
그때 카드는 글자로 떨어져야지 목록에서 사라지면 안 된다."""
|
|
await _publish(db_engine, company_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, company_id):
|
|
await _publish(db_engine, company_id, "먼저", status=SiteStatus.PUBLISHED.value, domain="first", minutes_ago=60)
|
|
await _publish(db_engine, company_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
|