Files
webmail-client/backend/tests/plugins/test_antispam_hook_gating.py
T

139 lines
4.5 KiB
Python
Raw Normal View History

2026-09-17 07:59:15 +02:00
"""What the antispam hook refuses to touch (SPEC §C.4).
The gating matters more than the scoring: `message.new` fires for imported
history and for every folder, so a hook that acts on all of it would file
years-old mail into Junk and stall backfill with inline HTTP.
"""
from __future__ import annotations
import uuid
from typing import Any
import pytest
from webmail_plugin_antispam import hook, policy
LIVE_INBOX = {
"message_id": "m",
"account_id": "acc-1",
"mailbox_special_use": "inbox",
"account_state": "live",
"from_email": "someone@example.test",
"subject": "hello",
"to": [],
"flags": [],
}
class _FakeMessages:
def __init__(self, env: dict[str, Any] | None) -> None:
self._env = env
self.raw_calls = 0
async def envelope(self, *_a: object, **_k: object) -> dict[str, Any] | None:
return self._env
async def raw(self, *_a: object, **_k: object) -> bytes | None:
self.raw_calls += 1
return b"From: someone@example.test\r\n\r\nbody\r\n"
class _FakeActions:
def __init__(self) -> None:
self.moves: list[str] = []
async def move_to_special(self, _u: object, _m: object, special_use: str) -> bool:
self.moves.append(special_use)
return True
class _FakeSdk:
def __init__(self, env: dict[str, Any] | None) -> None:
self.messages = _FakeMessages(env)
self.actions = _FakeActions()
self.published: list[tuple[str, dict[str, Any]]] = []
async def publish(self, event: str, payload: dict[str, Any]) -> None:
self.published.append((event, payload))
@pytest.fixture
def wired(monkeypatch: pytest.MonkeyPatch): # type: ignore[no-untyped-def]
"""Swap the hook's SDK and its database/engine calls for fakes."""
recorded: list[dict[str, Any]] = []
async def _record(_u: object, _m: object, **kw: Any) -> None:
recorded.append(kw)
async def _settings(_u: object) -> policy.Settings:
return policy.Settings(
enabled_accounts=[],
move_threshold=12.0,
banner_threshold=6.0,
network_checks=False,
learn_from_provider_junk=True,
)
async def _rules(_u: object) -> dict[str, set[str]]:
return {"allow": set(), "deny": set()}
monkeypatch.setattr(hook, "_record", _record)
monkeypatch.setattr(policy, "load_settings", _settings)
monkeypatch.setattr(policy, "load_rules", _rules)
def _install(env: dict[str, Any] | None, score: float | None = None) -> _FakeSdk:
fake = _FakeSdk(env)
monkeypatch.setattr(hook, "_SDK", fake)
async def _check(*_a: object, **_k: object) -> object:
if score is None:
return None
return type("V", (), {"score": score, "symbols": ["X"]})()
monkeypatch.setattr(hook.rspamd, "check", _check)
return fake
return _install, recorded
async def _fire() -> None:
await hook.on_message_new({"user_id": str(uuid.uuid4()), "message_id": str(uuid.uuid4())})
async def test_skips_while_the_account_is_backfilling(wired) -> None: # type: ignore[no-untyped-def]
"""Imported history must not be scored, moved, or slowed down."""
install, recorded = wired
fake = install({**LIVE_INBOX, "account_state": "backfilling"}, score=99.0)
await _fire()
assert fake.messages.raw_calls == 0 # never even fetched the body
assert fake.actions.moves == []
assert recorded == []
@pytest.mark.parametrize("special_use", ["sent", "junk", "trash", "drafts", None])
async def test_skips_everything_outside_the_inbox(wired, special_use) -> None: # type: ignore[no-untyped-def]
install, recorded = wired
fake = install({**LIVE_INBOX, "mailbox_special_use": special_use}, score=99.0)
await _fire()
assert fake.actions.moves == []
assert recorded == []
async def test_classifies_a_live_inbox_arrival(wired) -> None: # type: ignore[no-untyped-def]
install, recorded = wired
fake = install(LIVE_INBOX, score=99.0)
await _fire()
assert fake.actions.moves == ["junk"]
assert recorded and recorded[0]["action"] == "moved"
assert fake.published and fake.published[0][0] == "plugin.antispam.classified"
async def test_unreachable_engine_delivers_normally(wired) -> None: # type: ignore[no-untyped-def]
"""Acceptance 2: no engine, no interference."""
install, recorded = wired
fake = install(LIVE_INBOX, score=None)
await _fire()
assert fake.actions.moves == []
assert recorded and recorded[0]["action"] == "none"