o2o-site-AEO/solution/backend/tests/test_booking_request.py
Mina Choi 872d00f3c4 [feat] solution: 미니 블로그·이용후기·예약요청 추가, /s/stay 목업·발행 사이트 UI 다수 수정
인앱 미니 블로그(AI 자동 포스트, 이메일 승인)·이용후기(즉시 게시)·예약 요청(메일 발송)을
새로 붙였고, 병행해서 /s/stay 목업과 발행 사이트 공통 렌더러(UnitsSection·FestivalSection·
LocalGuideSection·WeatherSection 등)의 UI 버그를 다수 고쳤다. 범위가 넓지만 한 주 분량
작업을 한 커밋으로 묶어 달라는 요청에 따라 하나로 묶는다.

- solution/backend: post/review/booking_request 라우터·서비스·CRUD 추가, 스케줄러에
  블로그 초안 생성(새벽 4:10)·발송(아침 9:00) cron 등록, 마이그레이션 4건 추가
- solution/frontend, admin/frontend: 생성된 API 클라이언트 갱신, 리뷰 모더레이션·
  블로그 글 관리 페이지 추가
- solution/site/src: 객실 상세+실시간예약(날짜선택·연락처 폼)을 모달로 통합, 축제·
  주변안내 카드 클릭 시 모달 전환, 후기 목록 카드 UI, 공용 Modal 컴포넌트 신설,
  날씨 문구 동기화 버그 수정(하늘줄·기온줄 한 타이머로), 시설·편의 가능/불가 아이콘
  색상 하이라이트, 헤더 메뉴 순서를 실제 섹션 순서에 맞춤, 하단 탭바 아이콘 정렬 버그
  (line-height) 수정, 추천일정 점선 연결+데스크톱 자동펼침/모바일 축소, 채널 라벨에
  크롤링 원문("NOL")이 새던 것을 bookingLabel() 로 교체
- solution/site/scripts/mockup: /s/stay 패치 스크립트·주입 CSS·JS 다수 수정, stay4~6
  빌드 스크립트 추가(다른 세션 작업)

테스트: solution/site `npx tsc --noEmit` 통과, `npx vitest run` 93 passed,
solution/backend `pytest tests/test_booking_request.py` 6 passed(로컬 DB 대상).
예약 요청 메일은 실제 발송까지 확인(place 66894a1b 소유자 이메일 누락을 DB에서 보정).
2026-09-18 09:03:18 +09:00

110 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""예약 요청 폼 → 사장님 메일.
★ 이 파일이 지키는 것:
- 발행된 사이트의 업장만 받는다 (place_id 를 바꿔 아무 업장에나 메일을 쏘지 못한다)
- DB 에 아무것도 남기지 않는다
- 봇(허니팟·즉시 제출)은 실패로 알리지 않고 조용히 버린다
- SMTP 미설정이면 500 이 아니라 "전화로 문의" 로 답한다
"""
import uuid
import pytest
from sqlalchemy import text
from common.enums import PlaceCategory, SiteStatus
from services import mail_service
@pytest.fixture
def sent(monkeypatch):
box = []
monkeypatch.setattr(mail_service, "is_configured", lambda: True)
monkeypatch.setattr(mail_service, "send", lambda **kw: box.append(kw) or True)
return box
async def _seed(db_engine, owner_id, *, status=SiteStatus.PUBLISHED, email="owner@example.com") -> str:
place_id = uuid.uuid4()
async with db_engine.begin() as conn:
await conn.execute(text("UPDATE users SET email = :email WHERE user_id = :uid"),
{"email": email, "uid": owner_id})
await conn.execute(
text("INSERT INTO places (place_id, owner_user_id, name, category, status) "
"VALUES (:pid, :uid, :name, :cat, 1)"),
{"pid": place_id, "uid": owner_id, "name": "스테이,머뭄", "cat": PlaceCategory.LODGING.value},
)
await conn.execute(
text("INSERT INTO sites (site_id, place_id, status) VALUES (:sid, :pid, :st)"),
{"sid": uuid.uuid4(), "pid": place_id, "st": status.value},
)
return str(place_id)
def _form(place_id, **over):
body = {
"place_id": place_id, "name": "김손님", "phone": "010-1234-5678",
"email": "guest@example.com", "stay": "9월 20일 21일", "guests": "2명",
"message": "늦게 도착합니다", "consent": True, "elapsed_ms": 9000,
}
body.update(over)
return body
async def test_sends_mail_to_owner(client, db_engine, owner_id, sent):
place_id = await _seed(db_engine, owner_id)
res = await client.post("/v1/site/booking-request", json=_form(place_id))
assert res.status_code == 200
assert res.json()["success"] is True
assert len(sent) == 1
assert sent[0]["to"] == "owner@example.com"
assert sent[0]["reply_to"] == "guest@example.com"
for value in ("김손님", "010-1234-5678", "늦게 도착합니다"):
assert value in sent[0]["text"]
async def test_rejects_unpublished_place(client, db_engine, owner_id, sent):
place_id = await _seed(db_engine, owner_id, status=SiteStatus.DRAFT)
res = await client.post("/v1/site/booking-request", json=_form(place_id))
assert res.json()["success"] is False
assert sent == []
async def test_unknown_place_does_not_send(client, db_engine, owner_id, sent):
res = await client.post("/v1/site/booking-request", json=_form(str(uuid.uuid4())))
assert res.json()["success"] is False
assert sent == []
async def test_honeypot_and_instant_submit_are_dropped(client, db_engine, owner_id, sent):
place_id = await _seed(db_engine, owner_id)
bot = await client.post("/v1/site/booking-request", json=_form(place_id, company="광고회사"))
fast = await client.post("/v1/site/booking-request", json=_form(place_id, elapsed_ms=10))
# 봇에게는 걸렸다고 알리지 않는다 — 알리면 다음 시도가 그 조건을 피한다.
assert bot.json()["success"] is True
assert fast.json()["success"] is True
assert sent == []
async def test_requires_consent(client, db_engine, owner_id, sent):
place_id = await _seed(db_engine, owner_id)
res = await client.post("/v1/site/booking-request", json=_form(place_id, consent=False))
assert res.json()["success"] is False
assert sent == []
async def test_without_smtp_answers_instead_of_failing(client, db_engine, owner_id, monkeypatch):
monkeypatch.setattr(mail_service, "is_configured", lambda: False)
place_id = await _seed(db_engine, owner_id)
res = await client.post("/v1/site/booking-request", json=_form(place_id))
assert res.status_code == 200
assert res.json()["success"] is False
assert "전화" in res.json()["message"]