Files
siftlode/backend/tests/test_welcome_mail.py
T
peter ca9ca85b18 test: no test throws a response away; the stream session is narrowed, not baselined
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).
2026-10-03 19:06:47 +02:00

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 == []