The 7 baselined Optional accesses in plex/stream.py were one fact the type did not carry: the initial spawn (replace_only=None) always supersedes, so only the fallback respawn can get None. An assert at the call site says so; the pyright baseline drops 195 -> 188. test_abuse_limits: the public-URL and YouTube enqueue tests stopped at the profile lookup (400 no_profile) and passed on that unrelated refusal. They now send an inline spec and assert the job is queued. The same shape — a request whose response is discarded — sat at 53 more places across the suite (all returned what they should, measured with a TestClient probe); each now asserts its status, and test_response_checked keeps it that way (seen red).
255 lines
9.6 KiB
Python
255 lines
9.6 KiB
Python
"""One mail, one click — the branching welcome/invitation mail (E9.5 S6), DB-lane.
|
|
|
|
The funnel's common ordering makes a person open TWO messages: one saying their access is approved,
|
|
another asking them to confirm their address. The C option carries the confirmation link inside the
|
|
mail that is already going out, so the usual case is one mail and one click.
|
|
|
|
Also here: the silent no-op that is the most likely explanation for the 2026-09-03 report that a
|
|
confirmation mail "never arrived". Re-registering an address that already had an unconfirmed account
|
|
sent nothing at all, while answering with the same cheerful "check your inbox".
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.models import AuditLog, AuthToken, Invite, User
|
|
|
|
_PW = "correct horse battery"
|
|
|
|
|
|
def _capture(monkeypatch, name):
|
|
"""Record the arguments a given email_mod.send_* was called with."""
|
|
from app import email as email_mod
|
|
|
|
calls: list[tuple] = []
|
|
monkeypatch.setattr(email_mod, name, lambda *a, **kw: calls.append((a, kw)) or True)
|
|
return calls
|
|
|
|
|
|
def _smtp_on(monkeypatch):
|
|
from app import email as email_mod
|
|
|
|
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: True)
|
|
|
|
|
|
def _approve(client, invite_id):
|
|
r = client.post(f"/api/admin/invites/{invite_id}/approve")
|
|
assert r.status_code == 200
|
|
return r
|
|
|
|
|
|
# --- the approval mail branches ------------------------------------------------------------------
|
|
|
|
|
|
def test_an_unconfirmed_account_gets_the_confirmation_link_in_the_approval_mail(
|
|
db, make_user, as_user, monkeypatch
|
|
):
|
|
_smtp_on(monkeypatch)
|
|
sent = _capture(monkeypatch, "send_access_approved")
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="waiting@example.com", status="pending")
|
|
db.add(inv)
|
|
db.commit()
|
|
make_user(email="waiting@example.com", password=_PW, email_verified=False, is_active=False)
|
|
|
|
_approve(as_user(admin), inv.id)
|
|
|
|
(email, _lang, link, has_account, hours), _ = sent[0]
|
|
assert email == "waiting@example.com" and has_account is True
|
|
assert link and "#verify=" in link
|
|
# The whole point: this is the ONLY message they now have to open.
|
|
assert hours == 24 * 7
|
|
|
|
|
|
def test_the_link_in_that_mail_actually_confirms_the_address(db, make_user, as_user, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
sent = _capture(monkeypatch, "send_access_approved")
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="waiting@example.com", status="pending")
|
|
db.add(inv)
|
|
db.commit()
|
|
user = make_user(
|
|
email="waiting@example.com", password=_PW, email_verified=False, is_active=False
|
|
)
|
|
_approve(as_user(admin), inv.id)
|
|
token = sent[0][0][2].split("#verify=")[1]
|
|
|
|
r = as_user(admin).post("/auth/verify", json={"token": token})
|
|
|
|
assert r.status_code == 200
|
|
db.expire_all()
|
|
assert db.get(User, user.id).email_verified is True
|
|
|
|
|
|
def test_that_link_outlives_a_weekend(db, make_user, as_user, monkeypatch):
|
|
# An approval is not a mail anyone sits waiting for; the standard 24h would routinely expire
|
|
# before it is read, and the one-mail promise with it.
|
|
_smtp_on(monkeypatch)
|
|
_capture(monkeypatch, "send_access_approved")
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="waiting@example.com", status="pending")
|
|
db.add(inv)
|
|
db.commit()
|
|
user = make_user(
|
|
email="waiting@example.com", password=_PW, email_verified=False, is_active=False
|
|
)
|
|
|
|
_approve(as_user(admin), inv.id)
|
|
|
|
tok = db.execute(select(AuthToken).where(AuthToken.user_id == user.id)).scalars().all()[-1]
|
|
assert tok.expires_at > datetime.now(timezone.utc) + timedelta(days=6)
|
|
|
|
|
|
def test_an_already_confirmed_account_is_just_told_to_sign_in(db, make_user, as_user, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
sent = _capture(monkeypatch, "send_access_approved")
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="ready@example.com", status="pending")
|
|
db.add(inv)
|
|
db.commit()
|
|
make_user(email="ready@example.com", password=_PW, is_active=False)
|
|
|
|
_approve(as_user(admin), inv.id)
|
|
|
|
(_, _lang, link, has_account, _), _ = sent[0]
|
|
assert link is None and has_account is True # no link to offer, and none needed
|
|
|
|
|
|
def test_someone_with_no_account_yet_is_told_a_second_mail_is_coming(
|
|
db, make_user, as_user, monkeypatch
|
|
):
|
|
_smtp_on(monkeypatch)
|
|
sent = _capture(monkeypatch, "send_access_approved")
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="stranger@example.com", status="pending")
|
|
db.add(inv)
|
|
db.commit()
|
|
|
|
_approve(as_user(admin), inv.id)
|
|
|
|
(_, _lang, link, has_account, _), _ = sent[0]
|
|
# Nothing to link to yet. Saying a confirmation mail follows is what makes its ABSENCE
|
|
# noticeable — the failure that went unnoticed on 2026-09-03.
|
|
assert link is None and has_account is False
|
|
|
|
|
|
# --- the deliberate invitation --------------------------------------------------------------------
|
|
|
|
|
|
def test_adding_an_address_to_the_whitelist_mails_nobody(db, make_user, as_user, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
approved = _capture(monkeypatch, "send_access_approved")
|
|
invited = _capture(monkeypatch, "send_access_invitation")
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
|
|
r = as_user(admin).post("/api/admin/invites", json={"email": "maybe@example.com"})
|
|
|
|
# Whitelisting opens a door as a possibility — often already agreed in person — and an address
|
|
# can sit approved for weeks. A mail fired automatically here would be nudging someone who never
|
|
# asked to be nudged.
|
|
assert r.status_code == 200 and r.json()["invited_at"] is None
|
|
assert approved == [] and invited == []
|
|
|
|
|
|
def test_the_admin_can_then_invite_them_on_purpose(db, make_user, as_user, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
invited = _capture(monkeypatch, "send_access_invitation")
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="maybe@example.com", status="approved", decided_by=admin.email)
|
|
db.add(inv)
|
|
db.commit()
|
|
|
|
r = as_user(admin).post(f"/api/admin/invites/{inv.id}/invite")
|
|
|
|
assert r.status_code == 200 and r.json()["invited_at"] is not None
|
|
assert invited[0][0][0] == "maybe@example.com"
|
|
# Recorded on the row, so "did I already write to them?" has an answer where the decision is
|
|
# made — the wrong answer means mailing a real person twice.
|
|
db.expire_all()
|
|
assert db.get(Invite, inv.id).invited_at is not None
|
|
assert "invite_send" in db.execute(select(AuditLog.action)).scalars().all()
|
|
|
|
|
|
def test_an_undecided_request_cannot_be_invited(db, make_user, as_user, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="asking@example.com", status="pending")
|
|
db.add(inv)
|
|
db.commit()
|
|
|
|
r = as_user(admin).post(f"/api/admin/invites/{inv.id}/invite")
|
|
|
|
assert r.status_code == 409 and r.json()["detail"]["reason"] == "not_approved"
|
|
|
|
|
|
def test_the_invitation_says_so_when_the_instance_cannot_send_mail(
|
|
db, make_user, as_user, monkeypatch
|
|
):
|
|
from app import email as email_mod
|
|
|
|
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: False)
|
|
admin = make_user(email="admin@example.com", role="admin")
|
|
inv = Invite(email="maybe@example.com", status="approved", decided_by=admin.email)
|
|
db.add(inv)
|
|
db.commit()
|
|
|
|
r = as_user(admin).post(f"/api/admin/invites/{inv.id}/invite")
|
|
|
|
assert r.status_code == 409 and r.json()["detail"]["reason"] == "email_off"
|
|
db.expire_all()
|
|
assert db.get(Invite, inv.id).invited_at is None # nothing sent, nothing recorded
|
|
|
|
|
|
# --- the silent register no-op --------------------------------------------------------------------
|
|
|
|
|
|
def test_registering_again_re_sends_the_link_instead_of_sending_nothing(db, client, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
sent = _capture(monkeypatch, "send_verify_email")
|
|
assert (
|
|
client.post(
|
|
"/auth/register", json={"email": "twice@example.com", "password": _PW}
|
|
).status_code
|
|
== 200
|
|
)
|
|
assert len(sent) == 1
|
|
|
|
r = client.post("/auth/register", json={"email": "twice@example.com", "password": _PW})
|
|
|
|
# The old behaviour was to return in silence: same cheerful "check your inbox", no mail. That is
|
|
# the most likely explanation for the 2026-09-03 confirmation mail that "never arrived" — none
|
|
# was ever sent. The response stays uniform; only the dead end is gone.
|
|
assert r.status_code == 200 and r.json() == {"ok": True, "status": "ok"}
|
|
assert len(sent) == 2
|
|
assert sent[1][0][0] == "twice@example.com"
|
|
|
|
|
|
def test_registering_again_does_not_create_a_second_account(db, client, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
_capture(monkeypatch, "send_verify_email")
|
|
for _ in range(2):
|
|
assert (
|
|
client.post(
|
|
"/auth/register", json={"email": "twice@example.com", "password": _PW}
|
|
).status_code
|
|
== 200
|
|
)
|
|
|
|
db.expire_all()
|
|
rows = db.execute(select(User).where(User.email == "twice@example.com")).scalars().all()
|
|
assert len(rows) == 1
|
|
|
|
|
|
def test_a_confirmed_account_still_gets_nothing(db, client, make_user, monkeypatch):
|
|
_smtp_on(monkeypatch)
|
|
sent = _capture(monkeypatch, "send_verify_email")
|
|
make_user(email="done@example.com", password=_PW) # already verified
|
|
|
|
r = client.post("/auth/register", json={"email": "done@example.com", "password": _PW})
|
|
|
|
# There is nothing to confirm, so there is nothing to send — and mailing "confirm your address"
|
|
# to someone who already did would be a lie.
|
|
assert r.status_code == 200
|
|
assert sent == []
|