"""The admin lifeline for a sign-up that is stuck outside (E9.5 S5, roadmap UX-25), DB-lane. Approve an address BEFORE the person registers and their sign-up produces no queue row at all — `_decide` only takes a still-pending invite — so the Access-requests screen stayed empty while they sat locked out, and the badge read zero. The only working move was re-typing the address into the whitelist box, which nobody would guess. Two earlier attempts at this were reverted after review, and both moved AUTHORITY rather than visibility: auto-activating on registration, and reopening the approved invite to pending. So the tests here pin the boundary as much as the feature — an admin click, gated on an address that is already allowed, and nothing an unauthenticated caller can reach. """ from sqlalchemy import select from app.models import AuditLog, AuthToken, Invite, User _PW = "correct horse battery" def _pending_access(client): r = client.get("/api/admin/pending-access") assert r.status_code == 200 return {u["email"]: u for u in r.json()} def _stuck_registrant(db, make_user, admin): """The UX-25 shape: the admin approved the address first, the account arrived afterwards.""" db.add(Invite(email="later@example.com", status="approved", decided_by=admin.email)) db.commit() return make_user(email="later@example.com", password=_PW, is_active=False) def test_a_pre_approved_registrant_is_listed_and_offers_activation(db, make_user, as_user): admin = make_user(email="admin@example.com", role="admin") _stuck_registrant(db, make_user, admin) row = _pending_access(as_user(admin))["later@example.com"] assert row["is_active"] is False assert ( row["can_activate"] is True ) # the address is already allowed — the click just finishes it def test_an_ordinary_registrant_awaiting_a_decision_is_not_listed_twice(db, make_user, as_user): # They already have a pending invite with Approve on it in the queue above, and their address is # not allowed YET — the decision is the queue's to own. Caught in UAT on 2026-09-05: an earlier # filter keyed on "unconfirmed" put every fresh registration in both lists at once. admin = make_user(email="admin@example.com", role="admin") db.add(Invite(email="normal@example.com", status="pending")) db.commit() make_user(email="normal@example.com", password=_PW, is_active=False) assert "normal@example.com" not in _pending_access(as_user(admin)) def test_a_fresh_registration_stays_in_the_queue_and_out_of_this_list(db, make_user, as_user): # The realistic version of the case above: a brand-new sign-up is inactive AND unconfirmed. It # still belongs only to the queue — the admin's next act is the decision, not a re-send. admin = make_user(email="admin@example.com", role="admin") db.add(Invite(email="fresh@example.com", status="pending")) db.commit() make_user(email="fresh@example.com", password=_PW, is_active=False, email_verified=False) assert "fresh@example.com" not in _pending_access(as_user(admin)) def test_activating_lets_them_in_and_is_recorded(db, make_user, as_user): admin = make_user(email="admin@example.com", role="admin") stuck = _stuck_registrant(db, make_user, admin) r = as_user(admin).post(f"/api/admin/users/{stuck.id}/activate") assert r.status_code == 200 and r.json()["is_active"] is True db.expire_all() assert db.get(User, stuck.id).is_active is True actions = db.execute(select(AuditLog.action)).scalars().all() assert ( "user_activate" in actions ) # the admin acted on someone's access; it is not a silent read def test_activating_an_address_that_is_not_allowed_is_refused(db, make_user, as_user): # The guard that keeps this button from manufacturing an inconsistent account: password sign-in # only checks is_active and would let them in, while their Google sign-in would be turned away # and silently reopened as a fresh access request. Whitelisting is the move that says "let them # in"; this one only finishes a decision already made. admin = make_user(email="admin@example.com", role="admin") nobody = make_user(email="nobody@example.com", password=_PW, is_active=False) r = as_user(admin).post(f"/api/admin/users/{nobody.id}/activate") assert r.status_code == 409 assert r.json()["detail"]["reason"] == "not_allowed" db.expire_all() assert db.get(User, nobody.id).is_active is False def test_activating_twice_is_not_an_error(db, make_user, as_user): admin = make_user(email="admin@example.com", role="admin") stuck = _stuck_registrant(db, make_user, admin) client = as_user(admin) assert client.post(f"/api/admin/users/{stuck.id}/activate").status_code == 200 assert client.post(f"/api/admin/users/{stuck.id}/activate").status_code == 200 def test_a_suspended_account_is_not_offered_as_stuck(db, make_user, as_user): # It cannot sign in either, but that is a decision already made — not a person waiting on one. admin = make_user(email="admin@example.com", role="admin") db.add(Invite(email="banned@example.com", status="approved", decided_by=admin.email)) db.commit() make_user(email="banned@example.com", password=_PW, is_active=False, is_suspended=True) assert "banned@example.com" not in _pending_access(as_user(admin)) # --- the confirmation resend --------------------------------------------------------------------- def test_an_unconfirmed_account_is_listed_and_can_be_sent_a_fresh_link( db, make_user, as_user, monkeypatch ): from app.routes import admin as admin_routes from app.routes import auth as auth_routes monkeypatch.setattr(admin_routes.email_mod, "email_enabled", lambda db=None: True) sent: list[tuple[str, str]] = [] monkeypatch.setattr( auth_routes.email_mod, "send_verify_email", lambda to, lang, link, **kw: sent.append((to, link)) is None, ) admin = make_user(email="admin@example.com", role="admin") # Active but unconfirmed — a real state since E9.5 S1 made the two gates order-independent. It is # not the admin's to decide, but the mail may simply never have arrived, so it is theirs to help # with. The approved invite is what puts them on this screen at all: the list is "the address may # sign in, this account still can't", so an undecided request stays with the queue that owns it. db.add(Invite(email="unconfirmed@example.com", status="approved", decided_by=admin.email)) db.commit() u = make_user(email="unconfirmed@example.com", password=_PW, email_verified=False) assert "unconfirmed@example.com" in _pending_access(as_user(admin)) r = as_user(admin).post(f"/api/admin/users/{u.id}/resend-verification") assert r.status_code == 200 and r.json()["email"] == "unconfirmed@example.com" assert [to for to, _ in sent] == ["unconfirmed@example.com"] tokens = db.execute(select(AuthToken).where(AuthToken.user_id == u.id)).scalars().all() assert [t.kind for t in tokens] == ["verify"] def test_the_resend_says_so_when_the_instance_cannot_send_mail(db, make_user, as_user, monkeypatch): from app.routes import admin as admin_routes monkeypatch.setattr(admin_routes.email_mod, "email_enabled", lambda db=None: False) admin = make_user(email="admin@example.com", role="admin") u = make_user(email="unconfirmed@example.com", password=_PW, email_verified=False) r = as_user(admin).post(f"/api/admin/users/{u.id}/resend-verification") # Mail is fail-soft everywhere else, which is exactly what left the 0.78.5 sign-ups with nothing # to fall back on. An admin pressing a button gets the failure, not a success toast. assert r.status_code == 409 and r.json()["detail"]["reason"] == "email_off" def test_resending_to_an_already_confirmed_account_is_refused(db, make_user, as_user): admin = make_user(email="admin@example.com", role="admin") u = make_user(email="fine@example.com", password=_PW) r = as_user(admin).post(f"/api/admin/users/{u.id}/resend-verification") assert r.status_code == 409 and r.json()["detail"]["reason"] == "not_applicable" # --- the badge ----------------------------------------------------------------------------------- def test_the_badge_counts_stuck_sign_ups_but_not_ones_the_queue_already_shows( db, make_user, client ): # Signed in for real, not via the as_user override: /api/me resolves its user through # optional_current_user, which that override does not cover (see test_me_email_flag). admin = make_user(email="admin@example.com", password=_PW, role="admin") _stuck_registrant(db, make_user, admin) # approved first → counts db.add(Invite(email="normal@example.com", status="pending")) # already in the queue → doesn't db.commit() make_user(email="normal@example.com", password=_PW, is_active=False) make_user(email="unconfirmed@example.com", password=_PW, email_verified=False) # not ours assert ( client.post( "/auth/password-login", json={"email": admin.email, "password": _PW} ).status_code == 200 ) me = client.get("/api/me").json() assert me["authenticated"] is True # An unconfirmed account waits on its OWNER, not on the admin — listing it is help, badging it # would be nagging about something the admin did not cause. assert me["pending_activations"] == 1 assert me["pending_invites"] == 1 # unchanged meaning: still just the undecided requests