# backend/tests/test_federation_real.py — C7 real(가짜) sync → event_bus 발행 # 같은 코드 경로(GmailConnector.normalize/write/sync)로 mail.received 가 실제 발행됨을 증명. from app import seed as seedmod from app.automation.event_bus import bus from app.connectors.base import RawRecord from app.connectors.mail.real_gmail import GmailConnector from app.models import ConnectorAccount, ConnectorDomain, ConnectorMode, ConnState def _mail_account(s): # phase-16+: 메일 mock 계정 시드 제거 → real 연결 계정을 직접 생성. a = ConnectorAccount( id="ca-mail-personal", domain=ConnectorDomain.mail, provider="gmail", mode=ConnectorMode.real, state=ConnState.connected, external_account_id="me@gmail.com", name="me@gmail.com", ) s.add(a) s.commit() return a class FakeGmailConnector(GmailConnector): """network 없이 fetch 만 가짜로 — normalize/write/sync 는 real 코드 그대로.""" payloads: list = [] def fetch(self, session, *, full=False): for p in self.payloads: yield RawRecord( external_id=p["id"], payload=p, etag=str(p.get("historyId", "")), external_updated_at=None, ) _MSG = { "id": "gmail-real-1", "historyId": "9001", "labelIds": ["UNREAD", "IMPORTANT"], "snippet": "현우님 온보딩 시안 v3 도착", "internalDate": "1718000000000", "payload": { "headers": [ {"name": "From", "value": "hyunwoo@lumi.co"}, {"name": "Subject", "value": "온보딩 시안 v3"}, ] }, } def test_real_sync_publishes_mail_received_once(session): s, _ = session seedmod.run_seed(session=s, reset=True) acct = _mail_account(s) before = len([e for e in bus.history if e.type == "mail.received"]) conn = FakeGmailConnector(acct) conn.payloads = [_MSG] r1 = conn.sync(s) assert r1.upserted == 1 and r1.events_published == ["mail.received"] # 같은 historyId(etag) 재공급 → 멱등(이벤트 0) conn2 = FakeGmailConnector(acct) conn2.payloads = [_MSG] r2 = conn2.sync(s) assert r2.upserted == 0 and r2.events_published == [] after = len([e for e in bus.history if e.type == "mail.received"]) assert after - before == 1 # 새 메일당 정확히 1회 def test_real_sync_failure_does_not_crash(session): s, _ = session seedmod.run_seed(session=s, reset=True) acct = _mail_account(s) class BoomConnector(GmailConnector): def fetch(self, session, *, full=False): raise RuntimeError("network down") res = BoomConnector(acct).sync(s) # 예외가 앱을 멈추지 않음 assert res.errors == 1 and "network down" in res.detail s.refresh(acct) assert acct.state == "error"