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>
259 lines
10 KiB
Python
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}
|