"""§6 hardening — the Gitea-OAuth `auth.provision_user` reconciles by email. Regression cover for a §9-surfaced fragility: a human who signed in first via OTC owns an email-only `users` row (`gitea_id` NULL). When they later sign in via Gitea OAuth with the SAME email, `provision_user` used to match only by `gitea_id`, miss the OTC row, and INSERT a new row — colliding on the `idx_users_email` unique index and 500-ing the callback. The fix links the OAuth identity onto the existing email row (the mirror of the OTC linker). """ from __future__ import annotations from test_propose_vertical import ( # noqa: F401 FakeGitea, app_with_fake_gitea, provision_user_row, tmp_env, ) def _cfg(): from app.config import load_config return load_config() def test_oauth_links_onto_existing_otc_email_row(app_with_fake_gitea): from fastapi.testclient import TestClient from app import auth, db app, _fake = app_with_fake_gitea with TestClient(app): # An OTC-first user: email-only row, gitea_id NULL, still 'pending'. db.conn().execute( """ INSERT INTO users (gitea_id, gitea_login, email, display_name, avatar_url, role, permission_state) VALUES (NULL, NULL, ?, ?, '', 'contributor', 'pending') """, ("dual@example.com", "dual"), ) otc_id = db.conn().execute( "SELECT id FROM users WHERE email = ? COLLATE NOCASE", ("dual@example.com",) ).fetchone()["id"] # Now the same human signs in via Gitea OAuth (new gitea_id, same email). # Case-different email proves the NOCASE match. user = auth.provision_user( _cfg(), {"id": 9001, "login": "dualgitea", "email": "Dual@Example.com", "full_name": "Dual User", "avatar_url": "http://x/a.png"}, ) # Linked onto the SAME row — no second row, no 500. assert user.user_id == otc_id rows = db.conn().execute( "SELECT id, gitea_id, gitea_login, permission_state, role FROM users WHERE email = ? COLLATE NOCASE", ("dual@example.com",), ).fetchall() assert len(rows) == 1 assert rows[0]["id"] == otc_id assert rows[0]["gitea_id"] == 9001 # OAuth identity attached assert rows[0]["gitea_login"] == "dualgitea" assert rows[0]["permission_state"] == "pending" # admission state preserved assert rows[0]["role"] == "contributor" def test_oauth_provisions_fresh_user_when_email_matches_no_one(app_with_fake_gitea): from fastapi.testclient import TestClient from app import auth, db app, _fake = app_with_fake_gitea with TestClient(app): user = auth.provision_user( _cfg(), {"id": 9100, "login": "freshoauth", "email": "fresh@example.com", "full_name": "Fresh", "avatar_url": ""}, ) row = db.conn().execute( "SELECT gitea_id, gitea_login, role, permission_state FROM users WHERE id = ?", (user.user_id,), ).fetchone() assert row["gitea_id"] == 9100 assert row["gitea_login"] == "freshoauth" # Reaching provision_user means admission passed → granted (unchanged). assert row["permission_state"] == "granted" def test_returning_oauth_user_matched_by_gitea_id_not_duplicated(app_with_fake_gitea): from fastapi.testclient import TestClient from app import auth, db app, _fake = app_with_fake_gitea with TestClient(app): provision_user_row(user_id=55, login="returning", role="contributor") before = db.conn().execute("SELECT COUNT(*) AS n FROM users").fetchone()["n"] user = auth.provision_user( _cfg(), {"id": 55, "login": "returning-renamed", "email": "returning@test", "full_name": "Returning", "avatar_url": ""}, ) after = db.conn().execute("SELECT COUNT(*) AS n FROM users").fetchone()["n"] assert user.user_id == 55 assert after == before # matched by gitea_id; no new row row = db.conn().execute( "SELECT gitea_login FROM users WHERE id = ?", (55,) ).fetchone() assert row["gitea_login"] == "returning-renamed" # profile refreshed