Portal members: a request that loses the adoption race reads back the winner's user instead of being refused
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m16s
Playwright Tests / test-playwright (2, 2) (push) Failing after 19s
pre-commit / pre-commit (push) Failing after 2m17s
Test Backend / test-backend (3.12) (push) Successful in 4m51s
Test Backend / test-backend (3.14) (push) Successful in 4m32s
Compose Smoke Test / test-compose (push) Failing after 26s
Playwright Tests / merge-reports (push) Failing after 2m17s

This commit is contained in:
2026-11-01 11:21:42 +01:00
parent df1071ce73
commit 64ef7c84d6
2 changed files with 38 additions and 4 deletions
+5 -4
View File
@@ -216,11 +216,12 @@ def adopt_member(session: Session, portal_sub: str) -> User | None:
return None
try:
return create_remote_user(session, portal_sub, str(member["email"]))
except EnrollError:
return None
except IntegrityError:
except (EnrollError, IntegrityError):
# A first page load fires several requests and the websocket at once,
# all of them missing the row and all of them creating it. Whoever
# loses reads back what the winner wrote.
# loses, at the unique index or at the checks before it (the winner
# may commit while this one asks the portal), reads back what the
# winner wrote. An email another local account holds finds nothing to
# read back and stays refused.
session.rollback()
return session.exec(select(User).where(User.portal_sub == portal_sub)).first()
+33
View File
@@ -644,6 +644,39 @@ def test_a_member_the_portal_vouches_for_is_adopted(
assert user_from_token(db, stranger) is None
def test_a_request_that_loses_the_adoption_race_gets_the_winners_user(
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
portal_key: rsa.RSAPrivateKey,
db: Session,
) -> None:
"""A first page load adopts from several requests; none of them is refused."""
from fluksio.cloud.enroll import create_remote_user
theirs = portal_token(portal_key, subject="portal-user-race")
def vouch_after_the_winner_commits(*_: object, **__: object) -> Mock:
# Another request finishes adopting while this one asks the portal.
with Session(db.get_bind()) as other:
create_remote_user(other, "portal-user-race", "race@example.com")
return Mock(
status_code=200,
json=Mock(
return_value={
"user_id": "portal-user-race",
"email": "race@example.com",
}
),
)
with patch(
"fluksio.cloud.enroll.httpx.get", side_effect=vouch_after_the_winner_commits
):
adopted = user_from_token(db, theirs)
assert adopted is not None
assert adopted.portal_sub == "portal-user-race"
def test_the_tunnel_opens_the_live_socket_with_a_ticket(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None: