"""One outbound path and one send cap (MAS-WP-0012).""" import sqlite3 import pytest from mas_modes.harness import ROOT, Harness, reference_ticket_service from mas_modes.mailer import Mailer, parse_duration, parse_size STATUTE = ROOT / "mas-modes-v0.1-spec/conformance/statute.json" ENTRYPOINT = ROOT / "deploy/railiance/entrypoint.sh" OK = {"from": "c@example.net", "authentication": {"dmarc": "pass"}, "subject": "s", "message_id": ""} def mailer(**kw): m = Mailer(smtp=("sink", 0), **kw) m.sent = [] m._send = lambda *mail: m.sent.append(mail) return m def test_both_roles_share_one_cap(): h = Harness(STATUTE, {"ticket": {"role": "master"}, "case": {"role": "slave"}}, max_sent=3) try: for n in range(2): assert h.mail("ticket", f"ticket {n}").status == 201 # 2 creation replies h.mail("case", "case intake") [(_, body)] = h.master.pump() code, resp = h.master.send(body["case"], body["message_ref"], "on it") assert (code, resp["sent"]) == (200, True) # third send: the cap code, resp = h.master.send(body["case"], body["message_ref"], "again") assert code == 409 and resp["reason"] == "send-cap-reached" and resp["held"] is True assert h.mail("ticket", "ticket 3").status == 201 # ticket created, reply held assert len(h.sent) == 3 # exactly N, across both roles held = [m for m in h.mailer.outbox() if m["held"]] assert [m["body"] for m in held][0] == "again" and len(held) == 2 finally: h.close() def test_held_mail_is_purged_after_max_age(): clock = [0.0] m = mailer(max_sent=0, held_max_age=parse_duration("7d"), clock=lambda: clock[0]) assert m.send("t1", "a@d", OK, "s", "held") == ("send-cap-reached", True) clock[0] += 7 * 86400 - 1 assert m.purge_held() == 0 clock[0] += 2 assert m.purge_held() == 1 and m.outbox() == [] m.send("t1", "a@d", OK, "s", "new") # purging also runs on every send clock[0] += 8 * 86400 m.send("t1", "a@d", OK, "s", "newer") assert [r["body"] for r in m.outbox()] == ["newer"] def test_held_storage_limit(): m = mailer(max_sent=0, held_max_bytes=parse_size("1KB")) assert m.send("t", "a@d", OK, "s", "x" * 600)[1] is True assert m.send("t", "a@d", OK, "s", "y" * 600) == ("send-cap-reached", False) # would exceed 1 KB assert len(m.outbox()) == 1 and m.held_bytes() < 1024 def test_sent_mail_is_never_purged(): clock = [0.0] m = mailer(max_sent=1, clock=lambda: clock[0]) assert m.reply("t", "a@d", OK, "s", "sent") is None clock[0] += 365 * 86400 m.purge_held() assert [(r["body"], r["sent"]) for r in m.outbox()] == [("sent", 1)] assert m.cap_reached() def test_legacy_outbox_counts_once(tmp_path): db = tmp_path / "tickets.sqlite3" con = sqlite3.connect(db) con.execute("CREATE TABLE outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, ticket_id TEXT," " mail_from TEXT, mail_to TEXT, subject TEXT, body TEXT, in_reply_to TEXT," " sent INTEGER DEFAULT 0)") con.executemany("INSERT INTO outbox (ticket_id, mail_from, mail_to, subject, body, in_reply_to, sent)" " VALUES (?,?,?,?,?,?,?)", [("gs-1", "t@d", "c@x", "s", "b", None, 1)] * 2 + [("gs-2", "t@d", "c@x", "s", "b", None, 0)]) con.commit() con.close() out = tmp_path / "tickets-outbox.sqlite3" TicketService = reference_ticket_service() svc = TicketService(str(db), mailer=mailer(path=str(out), max_sent=3)) assert svc.mailer.sent_count() == 2 and len(svc.outbox()) == 3 svc = TicketService(str(db), mailer=mailer(path=str(out), max_sent=3)) # restart: no re-import assert len(svc.outbox()) == 3 assert svc.mailer.reply("gs-1", "t@d", OK, "s", "third") is None assert svc.send_cap_reached() # 2 legacy + 1 new = the cap of 3 @pytest.mark.parametrize("text, seconds", [("7d", 604800), ("12h", 43200), ("30m", 1800), ("45", 45)]) def test_parse_duration(text, seconds): assert parse_duration(text) == seconds @pytest.mark.parametrize("text, size", [("100MB", 100 * 1024 ** 2), ("512KB", 512 * 1024), ("1gb", 1024 ** 3), ("2048", 2048)]) def test_parse_size(text, size): assert parse_size(text) == size def test_bad_config_values(): with pytest.raises(ValueError): parse_duration("seven days") with pytest.raises(ValueError): parse_size("100 MiB") def test_entrypoint_passes_one_cap_and_hold_limits(): text = ENTRYPOINT.read_text() assert "--max-sent ${MAS_MAX_SENT:-300}" in text assert "--email-held-duration-max ${MAS_EMAIL_HELD_DURATION_MAX:-7d}" in text assert "--emails-held-storage-max ${MAS_EMAILS_HELD_STORAGE_MAX:-100MB}" in text