143 lines
3.7 KiB
Python
143 lines
3.7 KiB
Python
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 <sender@example.com>\r\n"
|
|
b"To: user@getall.help\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@getall.help"],
|
|
mail_from="sender@example.com",
|
|
account_ids={"user@getall.help": "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@getall.help",
|
|
[],
|
|
)
|
|
|
|
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@getall.help"]
|
|
assert target.account_ids == {"user@getall.help": "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")
|