feat(slice-2): accounts domain — request_code/verify/get_account (INV-2/INV-3/INV-6)
Email-canonical get-or-create, peppered-hash one-time codes with 10-min TTL, single-use, 5-attempt cap, and 60s resend cooldown; uniform new-or-known (no enumeration, §6.6). All identity rules in the domain layer once (INV-6); scenario + invariant tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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 == []
|
||||
Reference in New Issue
Block a user