The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
108 lines
4.7 KiB
Python
108 lines
4.7 KiB
Python
"""What a refused sign-in SAYS, DB-lane.
|
|
|
|
The states behind a 403 were true and useless: "Your account is awaiting admin approval." never
|
|
revealed WHICH mailbox the account was on. On 2026-09-03 a registration as `npeter83.temu@gmail.com`
|
|
met a Google claim of `npeter83temu@gmail.com`, and that missing address hid the mismatch for a day.
|
|
So these pin the DETAIL, not just the status code — the address, and both gates, since the two are
|
|
order-independent and the screen must report whichever is actually outstanding.
|
|
|
|
The two refusals that answer BEFORE the password is proven carry nothing, and that is the security
|
|
half of the same contract: `invalid` must stay byte-identical for a real account and an unknown one.
|
|
"""
|
|
|
|
from app.config import settings
|
|
from app.models import Invite
|
|
from app.routes.auth import _access_notice_url, upsert_pending_invite
|
|
|
|
_PW = "correct horse battery"
|
|
|
|
|
|
def _login(client, email, password=_PW):
|
|
return client.post("/auth/password-login", json={"email": email, "password": password})
|
|
|
|
|
|
def test_pending_approval_names_the_address_the_account_is_on(client, make_user):
|
|
# The typed spelling and the stored one are the same mailbox; the screen must show the STORED
|
|
# one, because that is the mismatch a user cannot otherwise see.
|
|
make_user(email="np.temu@gmail.com", password=_PW, is_active=False)
|
|
|
|
detail = _login(client, "nptemu@gmail.com").json()["detail"]
|
|
|
|
assert detail["reason"] == "pending_approval"
|
|
assert detail["email"] == "np.temu@gmail.com"
|
|
assert detail["email_verified"] is True and detail["approved"] is False
|
|
|
|
|
|
def test_an_already_approved_account_still_reports_its_missing_verification(client, make_user):
|
|
# The gates are order-independent, so this really happens: approved, not yet verified. A
|
|
# screen that assumed a sequence would tell this user to wait for an admin who already decided.
|
|
make_user(
|
|
email="approved.first@example.com", password=_PW, email_verified=False, is_active=True
|
|
)
|
|
|
|
detail = _login(client, "approved.first@example.com").json()["detail"]
|
|
|
|
assert detail["reason"] == "email_unverified"
|
|
assert detail["email_verified"] is False and detail["approved"] is True
|
|
|
|
|
|
def test_a_suspended_account_is_told_who_to_contact(client, make_user, monkeypatch):
|
|
monkeypatch.setattr(settings, "admin_emails", "ops@example.com")
|
|
make_user(email="banned@example.com", password=_PW, is_suspended=True)
|
|
|
|
r = _login(client, "banned@example.com")
|
|
|
|
assert r.status_code == 403
|
|
assert r.json()["detail"]["reason"] == "suspended"
|
|
assert r.json()["detail"]["operator"] == "ops@example.com"
|
|
|
|
|
|
def test_a_refusal_before_the_password_is_proven_carries_no_account_detail(client, make_user):
|
|
make_user(email="real@example.com", password=_PW, is_active=False)
|
|
|
|
wrong_pw = _login(client, "real@example.com", "not the password").json()["detail"]
|
|
unknown = _login(client, "nobody@example.com").json()["detail"]
|
|
|
|
# No address, no flags — the pending state above must not leak through the wrong-password door.
|
|
assert wrong_pw == unknown == {"code": "login", "reason": "invalid"}
|
|
|
|
|
|
# --- the access-request notice -------------------------------------------------------------------
|
|
|
|
|
|
def test_the_access_notice_names_the_address_and_rides_the_fragment():
|
|
url = _access_notice_url("requested", "someone@example.com")
|
|
|
|
# A fragment never reaches a proxy access log or a Referer header — the same reasoning that put
|
|
# the verify/reset tokens there (SB3), applied to an address.
|
|
assert url == "/#access=requested&email=someone%40example.com"
|
|
|
|
|
|
def test_a_plus_tagged_address_survives_the_round_trip():
|
|
# URLSearchParams reads a bare `+` in a fragment as a SPACE, so the encoder must emit %2B — or
|
|
# the very address family this epic exists to protect arrives mangled.
|
|
url = _access_notice_url("requested", "me+siftlode@gmail.com")
|
|
|
|
assert "me%2Bsiftlode%40gmail.com" in url and "+siftlode" not in url
|
|
|
|
|
|
def test_a_request_already_on_file_under_another_spelling_says_so(db):
|
|
db.add(Invite(email="hartmann.peter@gmail.com", status="pending"))
|
|
db.commit()
|
|
|
|
req = upsert_pending_invite(db, "hartmannpeter@gmail.com")
|
|
url = _access_notice_url("requested", "hartmannpeter@gmail.com", req.invite)
|
|
|
|
assert req.is_new is False # one mailbox, one request — no second row for the admin queue
|
|
assert "linked=hartmann.peter%40gmail.com" in url
|
|
|
|
|
|
def test_the_same_spelling_twice_is_just_a_repeat_and_says_nothing_extra(db):
|
|
db.add(Invite(email="same@example.com", status="pending"))
|
|
db.commit()
|
|
|
|
req = upsert_pending_invite(db, "same@example.com")
|
|
|
|
# Nothing to explain: telling this user we "linked" their request to itself would be noise.
|
|
assert "linked" not in _access_notice_url("requested", "same@example.com", req.invite)
|