"""End-to-end integration tests for the v0.23.0 sign-in state-resume vertical (§6.2, roadmap item #29). New behavior: each authenticated user's last-viewed route + a small bag of light view state is tracked server-side, so the next sign-in can land them back where they left off rather than on the empty-state home view. The tests below prove: * `PUT /api/me/last-state` requires auth — an anonymous client gets 401, and nothing is stored. * An authenticated PUT upserts the route, and the stored route is read back for that user off `GET /api/auth/me` (`last_route`). * A second PUT overwrites (upsert, one row per user) — the latest route wins. * `last_route_state` round-trips as decoded JSON on `/api/auth/me`. * Per-user isolation: user A's stored route is not visible to user B. * `resume_enabled = 0` disables resume: the PUT no-ops (does not rewrite the stored route) and `/api/auth/me` hands back a null `last_route` even though a stored row exists. The fakes from `test_propose_vertical` give us a working app harness + the `sign_in_as` / `provision_user_row` seams. """ from __future__ import annotations from test_propose_vertical import ( # noqa: F401 FakeGitea, app_with_fake_gitea, provision_user_row, sign_in_as, tmp_env, ) def test_put_last_state_requires_auth(app_with_fake_gitea): from fastapi.testclient import TestClient from app import db app, _fake = app_with_fake_gitea with TestClient(app) as client: # Anonymous — no session cookie set. r = client.put("/api/me/last-state", json={"route": "/rfc/open-human-model"}) assert r.status_code == 401 # Nothing landed in the table. row = db.conn().execute("SELECT COUNT(*) AS n FROM user_session_state").fetchone() assert row["n"] == 0 def test_put_last_state_upserts_and_reads_back(app_with_fake_gitea): from fastapi.testclient import TestClient app, _fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor", email="alice@test") # First POST stores a route + light state. r = client.put( "/api/me/last-state", json={"route": "/rfc/open-human-model", "state": {"tab": "discussion", "scroll": 420}}, ) assert r.status_code == 200 assert r.json()["stored"] is True # /api/auth/me hands the route + decoded state back. me = client.get("/api/auth/me").json() assert me["authenticated"] is True assert me["user"]["resume_enabled"] is True assert me["user"]["last_route"] == "/rfc/open-human-model" assert me["user"]["last_route_state"] == {"tab": "discussion", "scroll": 420} # A later POST overwrites — one row per user, latest wins. r = client.put("/api/me/last-state", json={"route": "/proposals/7"}) assert r.status_code == 200 me = client.get("/api/auth/me").json() assert me["user"]["last_route"] == "/proposals/7" # state was omitted on the second POST → cleared to null. assert me["user"]["last_route_state"] is None def test_last_state_is_per_user(app_with_fake_gitea): from fastapi.testclient import TestClient app, _fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") provision_user_row(user_id=3, login="bob", role="contributor") sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor", email="alice@test") client.put("/api/me/last-state", json={"route": "/rfc/alice-route"}) # Switch to bob — he has no stored route yet. sign_in_as(client, user_id=3, gitea_login="bob", display_name="Bob", role="contributor", email="bob@test") me = client.get("/api/auth/me").json() assert me["user"]["last_route"] is None client.put("/api/me/last-state", json={"route": "/rfc/bob-route"}) me = client.get("/api/auth/me").json() assert me["user"]["last_route"] == "/rfc/bob-route" # Back to alice — her route is untouched by bob's write. sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor", email="alice@test") me = client.get("/api/auth/me").json() assert me["user"]["last_route"] == "/rfc/alice-route" def test_resume_disabled_no_ops_put_and_hides_route(app_with_fake_gitea): from fastapi.testclient import TestClient from app import db app, _fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor", email="alice@test") # Seed a stored row, then flip resume_enabled off directly (the # profile-settings toggle UI to do this from the client is a # follow-up; the column + behavior ship now). client.put("/api/me/last-state", json={"route": "/rfc/before-disable"}) db.conn().execute( "UPDATE user_session_state SET resume_enabled = 0 WHERE user_id = ?", (2,), ) # /api/auth/me reports resume off and hands back a null route # even though a stored row exists. me = client.get("/api/auth/me").json() assert me["user"]["resume_enabled"] is False assert me["user"]["last_route"] is None # A PUT while disabled no-ops: stored=False and the stored route # is NOT rewritten. r = client.put("/api/me/last-state", json={"route": "/rfc/after-disable"}) assert r.status_code == 200 assert r.json()["stored"] is False row = db.conn().execute( "SELECT last_route FROM user_session_state WHERE user_id = ?", (2,) ).fetchone() assert row["last_route"] == "/rfc/before-disable"