teecup/tests/test_flag_plant_individual_tournament.py
Erol Haagenrud 25bdabe3ea Flaggturnering: GPS-flaggplanting (Del A) + kartoversikt (Del B)
Del A (ADR-066, migrasjon 069/070, allerede rullet ut mot teecup_db):
"Plant flagget" via GPS, med en interaktiv "Hullet du ut?"-bekreftelse
før posisjonen registreres, pluss full runde 2+-scoreføring for
spillere som fullfører 18 hull med slag igjen. Ny, isolert overflow-
tabell (rører ikke delt round_hole/tournament_round_hole). Bygget for
både frittstående runder og org-individuelle turneringer. UI-sheeten
er en Claude-skrevet V0-eksport (zip 11).

Del B (ADR-067, migrasjon 071, klar for utrulling): av/på-bryter
(standard AV) for en satellittkartoversikt over ALLE deltakeres
plantede flagg, lap 1 skilt fra lap 2+ med farge+tekstbadge. Frittstående
runder får full tilskuerstøtte (/watch/[id]); org-individuelle
turneringer er bevisst avgrenset til medlemmer/deltakere, siden ingen
offentlig spectator-side finnes for det turneringsformatet ennå.

Se ARCHITECTURE_DECISIONS.md (ADR-066/067) og CHANGELOG.md (punkt 82/83)
for full begrunnelse og verifiseringslogg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 10:38:54 +02:00

226 lines
8.9 KiB
Python

"""
Flaggturnering: GPS-flaggplanting + runde 2+, org-individuelle turneringer
(migrasjon 070, 2026-08-14) -- speiler test_flag_plant.py (frittstående
runder) presist, men gjennom individual_tournaments.py sin self-only
autorisasjon (user_is_own_tournament_participant), ikke rounds.py sin
flight-styrte modell.
Kaller de faktiske router-funksjonene direkte (samme mønster som
test_scoring_concurrency.py) -- organization_id sendes som vanlig
funksjonsargument her, ikke via FastAPIs Depends (som kun løses når
appen faktisk kjører via HTTP).
"""
import pytest
from fastapi import HTTPException
from app.auth import CurrentUser
from app.routers.individual_tournaments import (
FlagOverflowHoleIn,
FlagPlantIn,
HoleUpdate,
get_flag_map,
get_flag_plant,
list_flag_overflow_holes,
plant_flag,
unplant_flag,
update_flag_overflow_hole,
update_hole,
)
from tests.conftest import (
add_membership,
create_org,
create_org_hole,
create_course,
create_player,
create_tee,
create_tournament,
create_tournament_participant,
create_tournament_round,
create_tournament_round_participant,
create_user,
)
async def _setup_flag_tournament():
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)
course_id = await create_course(org_id)
for n in range(1, 19):
await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n)
tee_id = await create_tee(org_id, course_id)
import app.db as app_db
async with app_db.org_connection(org_id) as conn:
await conn.execute(
"UPDATE tournament SET format_type = 'individual', scoring_method = 'flag' WHERE id = $1",
tournament_id,
)
tournament_round_id = await create_tournament_round(org_id, tournament_id, course_id)
player_id = await create_player(org_id, user_id=user_id)
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
rp_id = await create_tournament_round_participant(org_id, tournament_round_id, tp_id, tee_id)
return org_id, user_id, tournament_id, tournament_round_id, rp_id
async def _play_hole(tournament_id, round_id, rp_id, hole_number, score, org_id, user):
await update_hole(
tournament_id, round_id, rp_id, hole_number,
HoleUpdate(gross_strokes=score),
organization_id=org_id, user=user,
)
async def test_plant_flag_succeeds_on_first_unplayed_hole(pool):
org_id, user_id, tournament_id, round_id, rp_id = await _setup_flag_tournament()
user = CurrentUser(user_id=user_id)
await _play_hole(tournament_id, round_id, rp_id, 1, 5, org_id, user)
result = await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=2, lap=1, on_green=False),
organization_id=org_id, user=user,
)
assert result.lap == 1
assert result.hole_number == 2
async def test_plant_flag_rejects_non_owning_player(pool):
org_id, user_id, tournament_id, round_id, rp_id = await _setup_flag_tournament()
stranger_id = await create_user()
await add_membership(org_id, stranger_id, role="member")
stranger = CurrentUser(user_id=stranger_id)
with pytest.raises(HTTPException) as exc_info:
await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
organization_id=org_id, user=stranger,
)
assert exc_info.value.status_code == 403
assert exc_info.value.detail["code"] == "NOT_TOURNAMENT_PARTICIPANT"
async def test_plant_flag_rejects_mismatched_hole(pool):
org_id, user_id, tournament_id, round_id, rp_id = await _setup_flag_tournament()
user = CurrentUser(user_id=user_id)
with pytest.raises(HTTPException) as exc_info:
await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=5, lap=1, on_green=False),
organization_id=org_id, user=user,
)
assert exc_info.value.status_code == 400
assert exc_info.value.detail["code"] == "VALIDATION_FAILED"
async def test_unplant_and_replant(pool):
org_id, user_id, tournament_id, round_id, rp_id = await _setup_flag_tournament()
user = CurrentUser(user_id=user_id)
await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=True, distance_to_pin_cm=120),
organization_id=org_id, user=user,
)
current = await get_flag_plant(tournament_id, round_id, rp_id, organization_id=org_id)
assert current is not None and current.distance_to_pin_cm == 120
await unplant_flag(tournament_id, round_id, rp_id, organization_id=org_id, user=user)
assert await get_flag_plant(tournament_id, round_id, rp_id, organization_id=org_id) is None
async def test_full_lap_two_flow(pool):
org_id, user_id, tournament_id, round_id, rp_id = await _setup_flag_tournament()
user = CurrentUser(user_id=user_id)
for n in range(1, 19):
await _play_hole(tournament_id, round_id, rp_id, n, 4, org_id, user)
with pytest.raises(HTTPException):
await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
organization_id=org_id, user=user,
)
overflow_hole = await update_flag_overflow_hole(
tournament_id, round_id, rp_id, 2, 1,
FlagOverflowHoleIn(gross_strokes=5), organization_id=org_id, user=user,
)
assert overflow_hole.lap == 2
assert overflow_hole.gross_strokes == 5
holes = await list_flag_overflow_holes(tournament_id, round_id, rp_id, organization_id=org_id)
assert len(holes) == 1
lap2_plant = await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=2, lap=2, on_green=False),
organization_id=org_id, user=user,
)
assert lap2_plant.lap == 2
assert lap2_plant.hole_number == 2
async def test_flag_map_shows_only_own_flag_when_switch_off(pool):
org_id, user_id, tournament_id, round_id, rp_id = await _setup_flag_tournament()
other_user_id = await create_user()
await add_membership(org_id, other_user_id, role="member")
other_player_id = await create_player(org_id, user_id=other_user_id)
other_tp_id = await create_tournament_participant(org_id, tournament_id, other_player_id)
import app.db as app_db
async with app_db.org_connection(org_id) as conn:
tee_id = await conn.fetchval("SELECT tee_id FROM tournament_round_participant WHERE id = $1", rp_id)
other_rp_id = await create_tournament_round_participant(org_id, round_id, other_tp_id, tee_id)
user = CurrentUser(user_id=user_id)
other = CurrentUser(user_id=other_user_id)
await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
organization_id=org_id, user=user,
)
await plant_flag(
tournament_id, round_id, other_rp_id,
FlagPlantIn(lat=60.0, lng=10.8, hole_number=1, lap=1, on_green=False),
organization_id=org_id, user=other,
)
result = await get_flag_map(tournament_id, round_id, organization_id=org_id, user=user)
assert result.visible_to_all is False
assert [f.participant_id for f in result.flags] == [rp_id]
async def test_flag_map_shows_all_flags_when_switch_on(pool):
org_id, user_id, tournament_id, round_id, rp_id = await _setup_flag_tournament()
other_user_id = await create_user()
await add_membership(org_id, other_user_id, role="member")
other_player_id = await create_player(org_id, user_id=other_user_id)
other_tp_id = await create_tournament_participant(org_id, tournament_id, other_player_id)
import app.db as app_db
async with app_db.org_connection(org_id) as conn:
tee_id = await conn.fetchval("SELECT tee_id FROM tournament_round_participant WHERE id = $1", rp_id)
await conn.execute("UPDATE tournament SET flag_map_visible = true WHERE id = $1", tournament_id)
other_rp_id = await create_tournament_round_participant(org_id, round_id, other_tp_id, tee_id)
user = CurrentUser(user_id=user_id)
other = CurrentUser(user_id=other_user_id)
await plant_flag(
tournament_id, round_id, rp_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
organization_id=org_id, user=user,
)
await plant_flag(
tournament_id, round_id, other_rp_id,
FlagPlantIn(lat=60.0, lng=10.8, hole_number=1, lap=1, on_green=False),
organization_id=org_id, user=other,
)
result = await get_flag_map(tournament_id, round_id, organization_id=org_id, user=other)
assert result.visible_to_all is True
assert {f.participant_id for f in result.flags} == {rp_id, other_rp_id}