teecup/tests/test_scoring_concurrency.py
Erol Haagenrud 5c2455c1fe
All checks were successful
Backend-tester / test (push) Successful in 21s
Optimistisk versjonssjekk for org-turneringers match-scoring
Fortsetter robusthetslinjen fra ADR-057: hole_score/match_hole_result
(scoring.py) hadde ingen samtidighetsvern, ren siste-skriving-vinner --
den siste dokumenterte, kjente svakheten fra den opprinnelige
"harde kjerner"-vurderingen. Samme version-kolonne-mønster, tilpasset
UPSERT (INSERT ... ON CONFLICT DO UPDATE ... WHERE) i stedet for ren
UPDATE. Offline-kø-kjedingen generalisert til å nøkle på ressurs
(resourceKey), ikke bare URL -- nødvendig siden disse to endepunktene
deler samme URL for alle hull i en match. Se ADR-060/CHANGELOG punkt 69.
23/23 backend-tester grønne, tsc rent. Migrasjon 063 kjørt mot ekte
teecup_db, teecup_api/teecup_frontend rebygget, ingen konsollfeil.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 23:19:29 +02:00

203 lines
7.2 KiB
Python

"""
ADR-060: samme optimistiske versjonssjekk som round_hole (ADR-057) fikk,
nå for org-turneringenes match-scoring (hole_score/match_hole_result,
app/routers/scoring.py). Kaller de faktiske router-funksjonene direkte --
samme kodesti som produksjon, ikke en SQL-gjenimplementering.
Autorisasjon: testbrukeren er satt som org-EIER (organization_membership
role='owner'), som tilfredsstiller user_is_match_participant sin
is_org_admin-fallback (team_authz.py) uten å måtte bygge ut hele
player/team_roster/match_participant-kjeden for HVER test -- kun
test_hole_score_individual_ball_participant_path bygger den fulle kjeden,
siden DEN grenen i submit_hole_score krever en ekte match_participant-rad
å slå opp uavhengig av org-admin-status.
"""
import pytest
from fastapi import HTTPException
from app import db as app_db
from app.auth import CurrentUser
from app.routers.scoring import (
HoleResultCreate,
HoleScoreCreate,
submit_hole_result,
submit_hole_score,
)
from tests.conftest import (
add_membership,
create_course,
create_match,
create_match_participant,
create_org,
create_player,
create_session,
create_team,
create_team_roster,
create_tee,
create_tournament,
create_user,
)
async def _setup_match(scoring_mode: str = "stroke", format_: str = "foursome"):
org_id = await create_org()
user_id = await create_user()
await add_membership(org_id, user_id, role="owner")
tournament_id = await create_tournament(org_id)
team_a = await create_team(org_id, tournament_id)
team_b = await create_team(org_id, tournament_id)
course_id = await create_course(org_id)
session_id = await create_session(
org_id, tournament_id, course_id, format=format_, scoring_mode=scoring_mode
)
match_id = await create_match(org_id, session_id, team_a, team_b)
return org_id, user_id, match_id, team_a, team_b
async def test_hole_score_shared_ball_succeeds_and_increments_version(pool):
org_id, user_id, match_id, _, _ = await _setup_match()
user = CurrentUser(user_id=user_id)
first = await submit_hole_score(
match_id,
HoleScoreCreate(team_side="a", hole_number=1, gross_strokes=5, expected_version=None),
org_id,
user=user,
)
assert first.version == 1
assert first.gross_strokes == 5
second = await submit_hole_score(
match_id,
HoleScoreCreate(team_side="a", hole_number=1, gross_strokes=4, expected_version=1),
org_id,
user=user,
)
assert second.version == 2
assert second.gross_strokes == 4
async def test_hole_score_stale_version_is_409(pool):
org_id, user_id, match_id, _, _ = await _setup_match()
user = CurrentUser(user_id=user_id)
await submit_hole_score(
match_id,
HoleScoreCreate(team_side="a", hole_number=1, gross_strokes=5, expected_version=None),
org_id,
user=user,
)
# "Enhet 2" hadde OGSÅ lest versjon 1 (før dette skjedde), og prøver nå
# å skrive med en versjon som allerede er forbigått.
with pytest.raises(HTTPException) as exc_info:
await submit_hole_score(
match_id,
HoleScoreCreate(team_side="a", hole_number=1, gross_strokes=6, expected_version=99),
org_id,
user=user,
)
assert exc_info.value.status_code == 409
assert exc_info.value.detail["code"] == "STALE_VERSION"
# Og bekreft at den avviste skrivingen faktisk IKKE ble lagret.
async with app_db.org_connection(org_id) as conn:
row = await conn.fetchrow(
"SELECT gross_strokes, version FROM hole_score WHERE match_id = $1 AND hole_number = 1", match_id
)
assert row["gross_strokes"] == 5
assert row["version"] == 1
async def test_hole_score_expected_version_none_bypasses_check(pool):
org_id, user_id, match_id, _, _ = await _setup_match()
user = CurrentUser(user_id=user_id)
await submit_hole_score(
match_id,
HoleScoreCreate(team_side="a", hole_number=1, gross_strokes=5, expected_version=None),
org_id,
user=user,
)
out = await submit_hole_score(
match_id,
HoleScoreCreate(team_side="a", hole_number=1, gross_strokes=7, expected_version=None),
org_id,
user=user,
)
assert out.gross_strokes == 7
assert out.version == 2
async def test_hole_score_individual_ball_participant_path(pool):
"""Den ANDRE ON CONFLICT-grenen i submit_hole_score (match_participant_id
satt) -- egen kode, egen WHERE-versjonssjekk, verdt å dekke separat fra
delt-ball-testene over. Krever en ekte match_participant-rad (kylling-
og-egg: user_is_match_participant sin participant-sjekk er uavhengig av
org-admin-fallbacken for AKKURAT denne oppslags-grenen i
submit_hole_score, se linje ~534 i scoring.py)."""
org_id, user_id, match_id, team_a, _ = await _setup_match(format_="singles")
user = CurrentUser(user_id=user_id)
course_id = await create_course(org_id)
tee_id = await create_tee(org_id, course_id)
player_id = await create_player(org_id, user_id=user_id)
roster_id = await create_team_roster(org_id, team_a, player_id)
participant_id = await create_match_participant(org_id, match_id, "a", roster_id, tee_id)
first = await submit_hole_score(
match_id,
HoleScoreCreate(
team_side="a", match_participant_id=participant_id, hole_number=1,
gross_strokes=4, expected_version=None,
),
org_id,
user=user,
)
assert first.version == 1
with pytest.raises(HTTPException) as exc_info:
await submit_hole_score(
match_id,
HoleScoreCreate(
team_side="a", match_participant_id=participant_id, hole_number=1,
gross_strokes=9, expected_version=99,
),
org_id,
user=user,
)
assert exc_info.value.status_code == 409
assert exc_info.value.detail["code"] == "STALE_VERSION"
async def test_hole_result_succeeds_and_increments_version(pool):
org_id, user_id, match_id, _, _ = await _setup_match(scoring_mode="hole_result")
user = CurrentUser(user_id=user_id)
first = await submit_hole_result(
match_id, HoleResultCreate(hole_number=1, winning_side="a", expected_version=None), org_id, user=user
)
assert first.version == 1
assert first.winning_side == "a"
second = await submit_hole_result(
match_id, HoleResultCreate(hole_number=1, winning_side="b", expected_version=1), org_id, user=user
)
assert second.version == 2
assert second.winning_side == "b"
async def test_hole_result_stale_version_is_409(pool):
org_id, user_id, match_id, _, _ = await _setup_match(scoring_mode="hole_result")
user = CurrentUser(user_id=user_id)
await submit_hole_result(
match_id, HoleResultCreate(hole_number=1, winning_side="a", expected_version=None), org_id, user=user
)
with pytest.raises(HTTPException) as exc_info:
await submit_hole_result(
match_id, HoleResultCreate(hole_number=1, winning_side="b", expected_version=99), org_id, user=user
)
assert exc_info.value.status_code == 409
assert exc_info.value.detail["code"] == "STALE_VERSION"