o2o-site-AEO/solution/backend/tests/test_booking_request.py
Mina Choi 412dc3397c [feat] solution/backend: 미니블로그 승인 알림·예약 요청 메일 발송 실패를 Teams 로 — 정상화되면 recovery
카톡·메일 발송이 실패하면 로그 한 줄만 남고 글은 REVIEWED 로 계속 머물렀다. 손님 예약 요청은
사장님께 못 가도 아무도 몰랐다.

- blog_jobs: 카톡 실패(메일 대체) · 메일 실패 · 보낼 곳 없음 → blog_send_failed, 사업장별 dedupe, 성공 시 resolve
- booking_request_service: 전달 실패 → booking_request_failed, 손님 이름·연락처는 싣지 않는다
- docs/ALERTS.md: 두 kind 추가

테스트 2건 추가. 블로그·예약·알림 93 passed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:33:39 +09:00

117 lines
4.4 KiB
Python
Raw 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.

"""예약 요청 폼 → 사장님 메일."""
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"]
async def test_mail_failure_alerts_without_guest_details(client, db_engine, owner_id, monkeypatch):
monkeypatch.setattr(mail_service, "is_configured", lambda: True)
monkeypatch.setattr(mail_service, "send", lambda **kw: False)
place_id = await _seed(db_engine, owner_id)
res = await client.post("/v1/site/booking-request", json=_form(place_id))
assert res.json()["success"] is False
async with db_engine.begin() as conn:
rows = (await conn.execute(text("SELECT kind, title, detail FROM alert_outbox"))).all()
assert [r[0] for r in rows] == ["booking_request_failed"]
for value in ("김손님", "010-1234-5678", "guest@example.com"):
assert value not in rows[0][1] + rows[0][2]