users.last_login_at (migration 0085, no backfill) is stamped by _establish_session, which the Google callback now shares with password login instead of repeating its session setup inline. The demo entry does not stamp its shared account. The admin user list carries the field.
98 lines
3.6 KiB
Python
98 lines
3.6 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 two that must not stamp
|
|
(a refused password, the shared demo account) and the admin list that shows it.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from app.config import settings
|
|
from app.models import DemoWhitelist
|
|
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_demo_entry_does_not_stamp_the_shared_account(client, db):
|
|
db.add(DemoWhitelist(email="visitor@example.com"))
|
|
db.commit()
|
|
|
|
r = client.post("/auth/demo", json={"email": "visitor@example.com"})
|
|
|
|
assert r.json() == {"authenticated": True}
|
|
demo = auth_routes.get_or_create_demo_user(db)
|
|
db.refresh(demo)
|
|
assert demo.last_login_at is None
|
|
|
|
|
|
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
|