from types import SimpleNamespace import pytest from bson import ObjectId from app.services import inbound_smtp from app.services.raw_mime_storage import StoredMime RAW = ( b"From: Sender \r\n" b"To: user@gentic.email\r\n" b"Subject: Hello\r\n\r\n" b"Retry safe body\r\n" ) class FakeMessages: def __init__(self): self.id = ObjectId() self.updates = 0 async def update_one(self, *args, **kwargs): self.updates += 1 async def find_one(self, *args, **kwargs): return {"_id": self.id} class FakeEntries: def __init__(self): self.updates = 0 async def update_one(self, *args, **kwargs): self.updates += 1 class FakeStorage: async def put(self, raw, delivery_key): return StoredMime("mail", f"raw/{delivery_key}.eml", 7, len(raw), "crc") class FakeSession: def __init__(self, account): self.account = account async def __aenter__(self): return self async def __aexit__(self, *args): pass async def scalar(self, query): return self.account def envelope(raw=RAW): return SimpleNamespace( original_content=raw, rcpt_tos=["user@gentic.email"], mail_from="sender@example.com", account_ids={"user@gentic.email": "account-1"}, ) @pytest.mark.asyncio async def test_rcpt_rejects_unknown_account(monkeypatch): monkeypatch.setattr(inbound_smtp, "SessionFactory", lambda: FakeSession(None)) target = SimpleNamespace(rcpt_tos=[], rcpt_options=[]) response = await inbound_smtp.InboundHandler().handle_RCPT( None, None, target, "missing@gentic.email", [], ) assert response.startswith("550 5.1.1") assert target.rcpt_tos == [] @pytest.mark.asyncio async def test_rcpt_accepts_account_and_tracks_owner(monkeypatch): account = SimpleNamespace(id="account-1") monkeypatch.setattr(inbound_smtp, "SessionFactory", lambda: FakeSession(account)) target = SimpleNamespace(rcpt_tos=[], rcpt_options=[]) response = await inbound_smtp.InboundHandler().handle_RCPT( None, None, target, "User@GetAll.Help", [], ) assert response.startswith("250 2.1.5") assert target.rcpt_tos == ["user@gentic.email"] assert target.account_ids == {"user@gentic.email": "account-1"} @pytest.mark.asyncio async def test_data_returns_success_only_after_idempotent_records(monkeypatch): messages = FakeMessages() entries = FakeEntries() monkeypatch.setattr( inbound_smtp, "mongo_db", SimpleNamespace(messages=messages, mailbox_entries=entries), ) monkeypatch.setattr(inbound_smtp, "get_raw_mime_storage", lambda: FakeStorage()) response = await inbound_smtp.InboundHandler().handle_DATA(None, None, envelope()) assert response.startswith("250 2.0.0") assert messages.updates == 1 assert entries.updates == 1 @pytest.mark.asyncio async def test_data_returns_retryable_status_on_storage_failure(monkeypatch): class BrokenStorage: async def put(self, raw, delivery_key): raise TimeoutError("GCS unavailable") monkeypatch.setattr(inbound_smtp, "get_raw_mime_storage", lambda: BrokenStorage()) response = await inbound_smtp.InboundHandler().handle_DATA(None, None, envelope()) assert response.startswith("451 4.3.0") @pytest.mark.asyncio async def test_data_rejects_oversized_message(monkeypatch): monkeypatch.setattr( inbound_smtp, "get_settings", lambda: SimpleNamespace(inbound_max_bytes=10), ) response = await inbound_smtp.InboundHandler().handle_DATA(None, None, envelope()) assert response.startswith("552 5.3.4")