Files
siftlode/backend/tests/test_last_login.py
T
peter 6d8b7be6d8 feat!: remove the shared demo account
The demo account — one shared user that whitelisted emails entered from
the login page, reset on a schedule — goes end to end (owner decision,
E34): demo.py and the reset job, users.is_demo and demo_whitelist
(migration 0098 deletes the demo user, its audit rows, the reset job's
override and notices, then drops the column and table; the downgrade
restores the empty schema), the /auth/demo entry, the admin whitelist and
reset, the Demo tab, the banner, the login page's "Try the demo", and
every is_demo branch in routes, userscope, scheduler and the UI.

require_human only ever refused the demo account, so its 87 uses become
current_user (owner-approved); require_write_scope keeps its own check.
The login page's "or" divider now shows only beside the Google button:
without the demo entry under it, an instance without Google would have
said "or" to nothing.

A contract test (removedFeatures) fails on the word anywhere in the
backend, frontend, e2e or scripts — seen red on a planted hit. Size
ceilings tightened for the modules that shrank; admin.py is back under
the limit.
2026-10-05 00:19:38 +02:00

100 lines
3.7 KiB
Python

"""`users.last_login_at` (E22 S1), DB-lane.
The roadmap assumed stamping it in `_establish_session` would cover Google and password alike. It
would not have: only password login called it, and the Google callback carried its own copy of the
same four lines. So these pin BOTH doors through the real routes — plus the one that must not stamp
(a refused password) and the admin list that shows it.
"""
from datetime import datetime, timedelta, timezone
from app.config import settings
from app.routes import auth as auth_routes
_PW = "correct horse battery"
def _recent(ts: datetime | None) -> bool:
return ts is not None and datetime.now(timezone.utc) - ts < timedelta(minutes=1)
def test_a_password_sign_in_stamps_last_login(client, db, make_user):
user = make_user(email="pw@example.com", password=_PW)
assert user.last_login_at is None
r = client.post("/auth/password-login", json={"email": "pw@example.com", "password": _PW})
assert r.status_code == 200
db.refresh(user)
assert _recent(user.last_login_at)
def test_a_refused_password_does_not_stamp(client, db, make_user):
user = make_user(email="wrong@example.com", password=_PW)
r = client.post(
"/auth/password-login", json={"email": "wrong@example.com", "password": "not it"}
)
assert r.status_code == 401
db.refresh(user)
assert user.last_login_at is None
class _FakeGoogle:
"""authlib's client, reduced to the one call the callback makes after Google redirects back."""
def __init__(self, userinfo: dict):
self._userinfo = userinfo
async def authorize_access_token(self, _request):
return {"userinfo": self._userinfo, "access_token": "at", "scope": "openid email profile"}
def test_a_google_sign_in_stamps_last_login(client, db, make_user, monkeypatch):
from cryptography.fernet import Fernet
from app import security
# The callback stores the OAuth grant encrypted; a throwaway key stands in for TOKEN_ENCRYPTION_KEY.
monkeypatch.setattr(security, "_fernet", Fernet(Fernet.generate_key()))
user = make_user(email="g@example.com", password=None, google_sub="sub-g")
monkeypatch.setattr(settings, "admin_emails", "g@example.com") # the bootstrap allow-list
fake = _FakeGoogle({"sub": "sub-g", "email": "g@example.com", "email_verified": True})
monkeypatch.setattr(auth_routes, "google_oauth", lambda _db: fake)
r = client.get("/auth/callback", follow_redirects=False)
assert r.status_code in (302, 307) and r.headers["location"] == "/"
db.refresh(user)
assert _recent(user.last_login_at)
# …and it is a real sign-in, not only a stamp: the session now answers as this account.
assert client.get("/api/me").json()["id"] == user.id
def test_the_admin_user_list_carries_last_login(as_user, db, make_user):
admin = make_user(email="boss@example.com", role="admin")
never = make_user(email="never@example.com")
admin.last_login_at = datetime(2026, 9, 16, 8, 30, tzinfo=timezone.utc)
db.commit()
rows = {u["email"]: u for u in as_user(admin).get("/api/admin/users").json()}
assert rows["boss@example.com"]["last_login_at"].startswith("2026-09-16T08:30")
assert rows[never.email]["last_login_at"] is None
def test_the_account_sees_its_own_last_sign_in(client, db, make_user):
# Settings → Account shows it (prod UAT 2026-09-19: only the admin's user list did).
make_user(email="me@example.com", password=_PW)
assert (
client.post(
"/auth/password-login", json={"email": "me@example.com", "password": _PW}
).status_code
== 200
)
stamp = client.get("/api/me").json()["last_login_at"]
assert _recent(datetime.fromisoformat(stamp))