"""Unit tests for `app.email_envelope.build_envelope` (v0.18.0 Slice 1). These tests don't spin up the FastAPI app or touch the DB — they exercise the helper directly. The integration tests in test_otc_vertical / test_admin_create_user_invite_vertical / test_notifications_vertical exercise the helper's *use* via the shared `_SENT` buffer (the send path appends the envelope dict before invoking the helper). """ from __future__ import annotations from email.utils import parsedate_to_datetime from app.email_envelope import build_envelope def _base_kwargs(**overrides): base = dict( to_address="recipient@example.com", from_address="notifications@ohm.wiggleverse.org", from_name="OHM", subject="A test subject", body_plain="Hello, world.\n", ) base.update(overrides) return base # --------------------------------------------------------------------------- # Always-present headers # --------------------------------------------------------------------------- def test_envelope_sets_from_to_subject(): msg = build_envelope(**_base_kwargs()) assert msg["To"] == "recipient@example.com" assert msg["Subject"] == "A test subject" # `From` is the display-form: "OHM ". assert "OHM" in msg["From"] assert "" in msg["From"] def test_envelope_sets_date_header_parseable(): msg = build_envelope(**_base_kwargs()) raw = msg["Date"] assert raw, "Date header must be set" # parsedate_to_datetime raises ValueError on malformed input. dt = parsedate_to_datetime(raw) assert dt is not None def test_envelope_sets_message_id_with_from_domain_by_default(): msg = build_envelope(**_base_kwargs()) mid = msg["Message-ID"] assert mid, "Message-ID must be set" # Shape per RFC 5322 / make_msgid: assert mid.startswith("<") and mid.endswith(">") assert "@ohm.wiggleverse.org>" in mid def test_envelope_message_id_domain_override(): msg = build_envelope(**_base_kwargs(msgid_domain="example.test")) assert "@example.test>" in msg["Message-ID"] def test_envelope_message_id_falls_back_to_localhost_if_from_has_no_at(): # Defensive: a malformed from_address shouldn't crash the helper. msg = build_envelope(**_base_kwargs(from_address="bare-no-at-sign")) assert "@localhost>" in msg["Message-ID"] # --------------------------------------------------------------------------- # Auto-Submitted (RFC 3834) # --------------------------------------------------------------------------- def test_envelope_sets_auto_submitted_for_transactional_default(): msg = build_envelope(**_base_kwargs()) assert msg["Auto-Submitted"] == "auto-generated" def test_envelope_omits_auto_submitted_when_transactional_is_false(): msg = build_envelope(**_base_kwargs(is_transactional=False)) assert msg["Auto-Submitted"] is None # --------------------------------------------------------------------------- # Reply-To # --------------------------------------------------------------------------- def test_envelope_sets_reply_to_when_provided(): msg = build_envelope(**_base_kwargs(reply_to="ohm@wiggleverse.org")) assert msg["Reply-To"] == "ohm@wiggleverse.org" def test_envelope_omits_reply_to_when_absent(): msg = build_envelope(**_base_kwargs()) assert msg["Reply-To"] is None # --------------------------------------------------------------------------- # List-Unsubscribe (the headers RFC 8058 / Gmail-Yahoo care about) # --------------------------------------------------------------------------- def test_envelope_no_list_unsubscribe_when_neither_given(): """OTC mail: the recipient explicitly requested the code; no unsubscribe semantics. The header MUST be absent (presence would imply OHM has the recipient on a list, which it doesn't).""" msg = build_envelope(**_base_kwargs()) assert msg["List-Unsubscribe"] is None assert msg["List-Unsubscribe-Post"] is None def test_envelope_mailto_only_list_unsubscribe(): """Admin invite / per-RFC invite: `mailto:` form only, no URL. The recipient isn't a user yet, so there's no per-user opt-out URL to flip; the operator handles ad-hoc opt-outs manually.""" msg = build_envelope(**_base_kwargs( unsubscribe_mailto="ohm@wiggleverse.org?subject=remove", )) assert msg["List-Unsubscribe"] == "" # NO List-Unsubscribe-Post when only a mailto is present — the # one-click semantic requires a URL the MUA can POST to. assert msg["List-Unsubscribe-Post"] is None def test_envelope_full_one_click_list_unsubscribe(): """Watcher notification / bundle: `mailto:` + signed-URL + `List-Unsubscribe-Post: List-Unsubscribe=One-Click`. Gmail and Yahoo enforce this for bulk-adjacent mail per RFC 8058.""" msg = build_envelope(**_base_kwargs( unsubscribe_mailto="ohm@wiggleverse.org?subject=remove", unsubscribe_url="https://ohm.wiggleverse.org/api/email/unsubscribe?t=abc", )) lu = msg["List-Unsubscribe"] assert "" in lu assert "" in lu assert msg["List-Unsubscribe-Post"] == "List-Unsubscribe=One-Click" def test_envelope_url_only_list_unsubscribe_still_sets_post(): msg = build_envelope(**_base_kwargs( unsubscribe_url="https://ohm.wiggleverse.org/api/email/unsubscribe?t=abc", )) assert msg["List-Unsubscribe"] == "" assert msg["List-Unsubscribe-Post"] == "List-Unsubscribe=One-Click" # --------------------------------------------------------------------------- # Body shape — plain-only vs multipart/alternative # --------------------------------------------------------------------------- def test_envelope_plain_only_body_is_text_plain(): msg = build_envelope(**_base_kwargs()) # No HTML alternative -> single-part text/plain. assert msg.get_content_type() == "text/plain" assert msg.get_content().strip() == "Hello, world." def test_envelope_with_html_is_multipart_alternative(): msg = build_envelope(**_base_kwargs(body_html="

Hello, world.

")) assert msg.get_content_type() == "multipart/alternative" # Two parts: text/plain first (so plain-text clients picking the # first part get the readable text), text/html second. parts = list(msg.iter_parts()) assert len(parts) == 2 assert parts[0].get_content_type() == "text/plain" assert parts[1].get_content_type() == "text/html" assert "Hello, world." in parts[0].get_content() assert "world" in parts[1].get_content()