f0533dc073
The Gitea-OAuth callback's `auth.provision_user` matched an existing `users` row only by `gitea_id`. A human who signed in first via OTC owns an email-only row (`gitea_id` NULL); a later Gitea-OAuth sign-in with the same email missed that row and INSERTed a new one, colliding on the `idx_users_email` case-insensitive unique index → IntegrityError → 500 callback. Fix (mirror of `otc.provision_or_link_user`): when no `gitea_id` row exists, reconcile by email and link the OAuth identity (gitea_id/gitea_login/profile) onto the existing email row. Owner-zero (§6.1) bootstrap applied on link (matching the fresh-insert path); `permission_state` preserved so linking never changes admission status as a side effect. One §9-surfaced framework fragility of two. backend 680 green (+3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
"""§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
|