import re import psycopg import pytest from app.domains import accounts from app.platform import db, mailer @pytest.fixture() def conn(fresh_db_url): with psycopg.connect(fresh_db_url) as c: db.migrate(c) yield c def _code_in(outbox) -> str: # The 6-digit code appears in the latest message body (LogMailer dev channel). return re.search(r"\b(\d{6})\b", outbox[-1].body).group(1) def test_14_01_0002_request_code_sends_a_six_digit_code(conn): m = mailer.LogMailer() accounts.request_code(conn, m, "Merchant@Example.com") assert len(m.outbox) == 1 assert m.outbox[-1].to == "merchant@example.com" # normalized recipient (INV-2) assert re.search(r"\b\d{6}\b", m.outbox[-1].body) def test_14_01_0003_request_code_stores_only_a_hash(conn): m = mailer.LogMailer() accounts.request_code(conn, m, "merchant@example.com") code = _code_in(m.outbox) rows = conn.execute("SELECT code_hash FROM auth_code WHERE email = %s", ("merchant@example.com",)).fetchall() assert len(rows) == 1 # The plaintext code is never stored (INV-3). assert code not in rows[0][0] assert len(rows[0][0]) >= 16 # a hash, not the 6-digit code def test_request_code_uniform_for_new_and_known(conn): # No account enumeration (§6.6): the call behaves identically whether or not the email # already has an account — it never signals existence and never raises. m = mailer.LogMailer() accounts.request_code(conn, m, "newcomer@example.com") # no account exists # create an account, then request again for a known email conn.execute("INSERT INTO account (email) VALUES (%s)", ("known@example.com",)) conn.commit() accounts.request_code(conn, m, "known@example.com") assert len(m.outbox) == 2 # both sent a code; caller cannot tell new from known def test_puc_02c_resend_is_rate_limited(conn): m = mailer.LogMailer() accounts.request_code(conn, m, "merchant@example.com") with pytest.raises(accounts.ResendCooldown) as exc: accounts.request_code(conn, m, "merchant@example.com") assert 0 < exc.value.retry_after_s <= 60 assert len(m.outbox) == 1 # the second code was NOT sent def test_request_code_rejects_invalid_email(conn): m = mailer.LogMailer() with pytest.raises(accounts.InvalidEmail): accounts.request_code(conn, m, "not-an-email") assert m.outbox == [] class _FailingMailer: """A mailer whose relay always refuses — the INV-9 honest-failure path.""" def send(self, to: str, subject: str, body: str) -> None: raise mailer.MailerError("relay refused") def test_delivery_failure_leaves_no_orphan_code(conn): # ecomm#7: send happens BEFORE commit — a failed delivery must not strand an # auth_code row (which would also trip the resend cooldown for 60s). with pytest.raises(accounts.DeliveryFailed): accounts.request_code(conn, _FailingMailer(), "merchant@example.com") rows = conn.execute("SELECT count(*) FROM auth_code").fetchone()[0] assert rows == 0 def test_delivery_failure_does_not_trip_cooldown(conn): with pytest.raises(accounts.DeliveryFailed): accounts.request_code(conn, _FailingMailer(), "merchant@example.com") # the immediate retry (relay back up) succeeds — no ResendCooldown m = mailer.LogMailer() accounts.request_code(conn, m, "merchant@example.com") assert len(m.outbox) == 1