teecup/tests/test_flag_plant.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

259 lines
10 KiB
Python

"""
Flaggturnering: GPS-flaggplanting + runde 2+ (migrasjon 069, 2026-08-14).
Kaller de faktiske router-funksjonene i rounds.py direkte, samme mønster
som test_concurrency_version_check.py -- ikke en gjenimplementering.
Kun frittstående runder (rounds.py) dekkes her -- individual_tournaments.py
sin org-speilende variant mangler conftest-hjelpere for tournament_round-
familien og er i stedet manuelt browserverifisert (se CHANGELOG.md).
"""
import pytest
from fastapi import HTTPException
from app import db as app_db
from app.auth import CurrentUser
from app.routers.rounds import (
HoleUpdate,
FlagPlantIn,
FlagOverflowHoleIn,
get_flag_map,
get_flag_plant,
list_flag_overflow_holes,
plant_flag,
unplant_flag,
update_flag_overflow_hole,
update_hole,
)
from tests.conftest import create_hole, create_participant, create_round, create_user
async def _setup_flag_round(holes_planned: int = 18, start_hole: int = 1):
async with app_db.plain_connection() as conn:
owner_id = await create_user()
round_id = await create_round(conn, owner_id)
await conn.execute(
"UPDATE round SET play_format = 'flag', holes_planned = $2, start_hole = $3 WHERE id = $1",
round_id, holes_planned, start_hole,
)
participant_id = await create_participant(
conn, round_id, user_id=owner_id, is_owner=True, course_handicap_snapshot=10,
)
for n in range(1, 19):
await create_hole(conn, participant_id, hole_number=n, par=4, stroke_index=n)
return owner_id, round_id, participant_id
async def _play_hole(round_id: str, participant_id: str, hole_number: int, score: int, user: CurrentUser):
await update_hole(round_id, participant_id, hole_number, HoleUpdate(score=score, played=True), user=user)
async def test_plant_flag_succeeds_on_first_unplayed_hole(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
await _play_hole(round_id, participant_id, 1, 5, user)
await _play_hole(round_id, participant_id, 2, 4, user)
result = await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=3, lap=1, on_green=False),
user=user,
)
assert result.lap == 1
assert result.hole_number == 3
assert result.on_green is False
assert result.distance_to_pin_cm is None
async def test_plant_flag_rejects_mismatched_hole(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
await _play_hole(round_id, participant_id, 1, 5, user)
# Faktisk posisjon er hull 2 (kun hull 1 spilt) -- forsøker å plante på hull 5.
with pytest.raises(HTTPException) as exc_info:
await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=5, lap=1, on_green=False),
user=user,
)
assert exc_info.value.status_code == 400
assert exc_info.value.detail["code"] == "VALIDATION_FAILED"
async def test_plant_flag_on_green_requires_distance(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
with pytest.raises(HTTPException) as exc_info:
await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=True),
user=user,
)
assert exc_info.value.status_code == 400
assert exc_info.value.detail["code"] == "VALIDATION_FAILED"
async def test_plant_flag_on_green_with_distance_succeeds(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
result = await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=True, distance_to_pin_cm=340),
user=user,
)
assert result.on_green is True
assert result.distance_to_pin_cm == 340
async def test_replanting_overwrites_not_duplicates(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
first = await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
user=user,
)
await _play_hole(round_id, participant_id, 1, 5, user)
second = await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=60.0, lng=10.8, hole_number=2, lap=1, on_green=False),
user=user,
)
assert second.id != first.id
current = await get_flag_plant(round_id, participant_id, user=user)
assert current.hole_number == 2
async def test_unplant_removes_flag(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
user=user,
)
await unplant_flag(round_id, participant_id, user=user)
assert await get_flag_plant(round_id, participant_id, user=user) is None
async def test_get_flag_plant_none_when_never_planted(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
assert await get_flag_plant(round_id, participant_id, user=user) is None
async def test_overflow_hole_rejected_before_lap_one_complete(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
# Kun 5 av 18 hull spilt i lap 1 -- lap 2 skal avvises.
for n in range(1, 6):
await _play_hole(round_id, participant_id, n, 4, user)
with pytest.raises(HTTPException) as exc_info:
await update_flag_overflow_hole(
round_id, participant_id, 2, 1, FlagOverflowHoleIn(score=4), user=user,
)
assert exc_info.value.status_code == 400
assert exc_info.value.detail["code"] == "VALIDATION_FAILED"
async def test_full_lap_two_flow(pool):
owner_id, round_id, participant_id = await _setup_flag_round()
user = CurrentUser(user_id=owner_id)
for n in range(1, 19):
await _play_hole(round_id, participant_id, n, 4, user)
# Forsøk på å plante i lap 1 avvises nå -- spilleren har allerede
# fullført alle 18 hull i lap 1, faktisk posisjon er lap 2, hull 1.
with pytest.raises(HTTPException) as exc_info:
await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
user=user,
)
assert exc_info.value.status_code == 400
overflow_hole = await update_flag_overflow_hole(
round_id, participant_id, 2, 1, FlagOverflowHoleIn(score=5), user=user,
)
assert overflow_hole.lap == 2
assert overflow_hole.hole_number == 1
assert overflow_hole.score == 5
holes = await list_flag_overflow_holes(round_id, participant_id, user=user)
assert len(holes) == 1
assert holes[0].hole_number == 1
# Nå kan flagget faktisk plantes riktig i lap 2, hull 2.
lap2_plant = await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=2, lap=2, on_green=False),
user=user,
)
assert lap2_plant.lap == 2
assert lap2_plant.hole_number == 2
async def test_flag_plant_respects_non_default_start_hole(pool):
owner_id, round_id, participant_id = await _setup_flag_round(start_hole=10)
user = CurrentUser(user_id=owner_id)
# Spillerekkefølgen er 10,11,...,18,1,...,9 -- ingen hull spilt ennå,
# så flagget skal kun kunne plantes på hull 10 (ikke fysisk hull 1).
with pytest.raises(HTTPException) as exc_info:
await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False),
user=user,
)
assert exc_info.value.status_code == 400
result = await plant_flag(
round_id, participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=10, lap=1, on_green=False),
user=user,
)
assert result.hole_number == 10
async def test_flag_map_shows_only_own_flag_when_switch_off(pool):
owner_id, round_id, owner_participant_id = await _setup_flag_round()
other_user_id = await create_user()
async with app_db.plain_connection() as conn:
other_participant_id = await create_participant(conn, round_id, user_id=other_user_id, is_owner=False)
owner = CurrentUser(user_id=owner_id)
other = CurrentUser(user_id=other_user_id)
await plant_flag(
round_id, owner_participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False), user=owner,
)
await plant_flag(
round_id, other_participant_id,
FlagPlantIn(lat=60.0, lng=10.8, hole_number=1, lap=1, on_green=False), user=other,
)
result = await get_flag_map(round_id, user=owner)
assert result.visible_to_all is False
assert [f.participant_id for f in result.flags] == [owner_participant_id]
async def test_flag_map_shows_all_flags_when_switch_on(pool):
owner_id, round_id, owner_participant_id = await _setup_flag_round()
other_user_id = await create_user()
async with app_db.plain_connection() as conn:
other_participant_id = await create_participant(conn, round_id, user_id=other_user_id, is_owner=False)
await conn.execute("UPDATE round SET flag_map_visible = true WHERE id = $1", round_id)
owner = CurrentUser(user_id=owner_id)
other = CurrentUser(user_id=other_user_id)
await plant_flag(
round_id, owner_participant_id,
FlagPlantIn(lat=59.9, lng=10.7, hole_number=1, lap=1, on_green=False), user=owner,
)
await plant_flag(
round_id, other_participant_id,
FlagPlantIn(lat=60.0, lng=10.8, hole_number=1, lap=1, on_green=False), user=other,
)
result = await get_flag_map(round_id, user=other)
assert result.visible_to_all is True
assert {f.participant_id for f in result.flags} == {owner_participant_id, other_participant_id}