teecup/tests/test_target_points.py
Erol Haagenrud 93af7c5bef Rangefinder-koordinater for offisielle (TeeOff-koblede) baner
Rangefinder-funksjonalitet fantes til nå kun for GolfAPI-importerte
personlige baner. Offisielle teeoff-koblede baner (org-turneringer OG
frittstående runder spilt på en ekte teeoff-bane) hadde aldri noen
koordinatkilde. Ny migrasjon 079: delte domener (course_poi_type/
location/side) for å unngå duplisert CHECK-vedlikehold på tvers av
tabeller, ny global tabell teeoff_course_coordinate.

Ny delt modul app/target_points.py + resolve_match_course_key() i
hole_history.py (tredje søster til de to eksisterende bane-bro-
funksjonene) -- alle tre kallesteder (frittstående runder, individuell
org-turnering, lag-matchplay) bruker nå samme CourseKey-abstraksjon.
rounds.py sitt eksisterende endepunkt refaktorert til samme mønster --
sideeffekt: frittstående runder på en ekte teeoff-bane får nå også
rangefinder (var alltid tom liste før).

Ny skrive-vei i courses.py (PUT/GET .../coordinates) -- generell,
gjenbrukbar mulighet for å legge inn koordinater på enhver offisiell
bane, ikke en engangsfiks. HoleTargetDistance generalisert (roundId ->
baseUrl) og wiret inn i begge org-turnering-scoringsflytene.

11 nye tester (106/106 backend totalt), full scratch-verifisert
ende-til-ende i nettleser i alle tre flyter inkl. lys/mørk og
selvskjuling, regresjonssjekk av eksisterende GolfAPI-rangefinder
bekreftet uendret oppførsel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 12:06:47 +02:00

299 lines
12 KiB
Python

"""
Rangefinder-koordinater for offisielle (TeeOff-koblede) baner (migrasjon
079, ADR-081). To lag med tester:
1. `get_target_points()`/`resolve_match_course_key()` -- de delte
byggeklossene direkte, uavhengig av hvilket av de tre kallestedene
som bruker dem.
2. Integrasjon mot alle tre endepunktene (frittstående runde, individuell
org-turnering, lag-matchplay) + den nye skrive-veien for offisielle
baners koordinater i courses.py.
"""
from fastapi import HTTPException
import pytest
from app.auth import CurrentUser
from app.hole_history import resolve_match_course_key
from app.target_points import get_target_points
from app.routers.courses import CoursePointIn, list_course_coordinates, replace_course_coordinates
from app.routers.individual_tournaments import get_tournament_round_hole_target_points
from app.routers.rounds import get_round_hole_target_points
from app.routers.scoring import get_match_hole_target_points
from tests.conftest import (
add_membership,
create_course,
create_match,
create_org,
create_org_hole,
create_participant,
create_player,
create_round,
create_session,
create_team,
create_team_roster,
create_tee,
create_tournament,
create_tournament_participant,
create_tournament_round,
create_tournament_round_participant,
create_user,
)
import app.db as app_db
# ---------------------------------------------------------------------------
# 1. get_target_points() / resolve_match_course_key()
# ---------------------------------------------------------------------------
async def test_get_target_points_returns_empty_for_none_key(pool):
async with app_db.plain_connection() as conn:
points = await get_target_points(conn, None, 1)
assert points == []
async def test_get_target_points_teeoff_branch(pool):
async with app_db.plain_connection() as conn:
await conn.execute(
"""
INSERT INTO teeoff_course_coordinate
(external_course_ref, hole_number, poi_type, location, side_fairway, latitude, longitude)
VALUES ('slug:99', 5, 'green', 'middle', 'center', 59.1, 10.4)
"""
)
points = await get_target_points(conn, ("teeoff", "slug", "99"), 5)
other_hole = await get_target_points(conn, ("teeoff", "slug", "99"), 6)
assert len(points) == 1
assert points[0].poi_type == "green"
assert points[0].latitude == 59.1
assert other_hole == []
async def test_get_target_points_golfapi_branch(pool):
async with app_db.plain_connection() as conn:
await conn.execute(
"""
INSERT INTO golfapi_course (course_id, club_id, club_name, course_name, num_holes, num_coordinates)
VALUES ('gapi-1', 'club-1', 'Testklubb', 'Bane', 18, 1)
"""
)
await conn.execute(
"""
INSERT INTO golfapi_course_coordinate
(golfapi_course_id, hole_number, poi_type, location, side_fairway, latitude, longitude)
VALUES ('gapi-1', 3, 'water', 'front', 'left', 59.2, 10.5)
"""
)
points = await get_target_points(conn, ("golfapi", "gapi-1"), 3)
assert len(points) == 1
assert points[0].poi_type == "water"
async def test_resolve_match_course_key_custom_course_returns_none(pool):
user_id = await create_user()
org_id = await create_org()
await add_membership(org_id, user_id, role="member")
tournament_id = await create_tournament(org_id)
course_id = await create_course(org_id) # source defaults to 'custom'
team_a = await create_team(org_id, tournament_id)
team_b = await create_team(org_id, tournament_id)
session_id = await create_session(org_id, tournament_id, course_id)
match_id = await create_match(org_id, session_id, team_a, team_b)
async with app_db.org_connection(org_id) as conn:
key = await resolve_match_course_key(conn, match_id)
assert key is None
async def test_resolve_match_course_key_official_course(pool):
org_id = await create_org()
tournament_id = await create_tournament(org_id)
course_id = await create_course(org_id)
async with app_db.org_connection(org_id) as conn:
await conn.execute(
"UPDATE course SET source = 'official', external_course_ref = 'slug-x:42' WHERE id = $1",
course_id,
)
team_a = await create_team(org_id, tournament_id)
team_b = await create_team(org_id, tournament_id)
session_id = await create_session(org_id, tournament_id, course_id)
match_id = await create_match(org_id, session_id, team_a, team_b)
async with app_db.org_connection(org_id) as conn:
key = await resolve_match_course_key(conn, match_id)
assert key == ("teeoff", "slug-x", "42")
async def test_resolve_match_course_key_international_course(pool):
org_id = await create_org()
tournament_id = await create_tournament(org_id)
course_id = await create_course(org_id)
async with app_db.org_connection(org_id) as conn:
await conn.execute(
"UPDATE course SET source = 'international', external_course_ref = 'gapi-77' WHERE id = $1",
course_id,
)
team_a = await create_team(org_id, tournament_id)
team_b = await create_team(org_id, tournament_id)
session_id = await create_session(org_id, tournament_id, course_id)
match_id = await create_match(org_id, session_id, team_a, team_b)
async with app_db.org_connection(org_id) as conn:
key = await resolve_match_course_key(conn, match_id)
assert key == ("golfapi", "gapi-77")
async def test_resolve_match_course_key_nonexistent_match_returns_none(pool):
org_id = await create_org()
async with app_db.org_connection(org_id) as conn:
key = await resolve_match_course_key(conn, "00000000-0000-0000-0000-000000000000")
assert key is None
# ---------------------------------------------------------------------------
# 2. Integrasjon: de tre GET-endepunktene + skrive-veien i courses.py.
# ---------------------------------------------------------------------------
async def test_round_target_points_teeoff_round_now_returns_points(pool):
"""Regresjonsbevis for refaktoreringen: FØR migrasjon 079 returnerte
dette endepunktet [] for EN HVER teeoff-sourcet frittstående runde,
uansett koordinatdata. Nå slår den opp via samme bane-bro som
hole_history.py."""
user_id = await create_user()
async with app_db.plain_connection() as conn:
round_id = await create_round(conn, user_id) # teeoff/test-facility/test-course
await create_participant(conn, round_id, user_id=user_id, is_owner=True)
await conn.execute(
"""
INSERT INTO teeoff_course_coordinate
(external_course_ref, hole_number, poi_type, location, side_fairway, latitude, longitude)
VALUES ('test-facility:test-course', 4, 'tee_front', 'middle', 'center', 59.0, 10.0)
"""
)
points = await get_round_hole_target_points(round_id, 4, user=CurrentUser(user_id=user_id))
assert len(points) == 1
assert points[0].poi_type == "tee_front"
empty = await get_round_hole_target_points(round_id, 5, user=CurrentUser(user_id=user_id))
assert empty == []
async def test_tournament_round_target_points_official_course(pool):
user_id = await create_user()
org_id = await create_org()
await add_membership(org_id, user_id, role="member")
tournament_id = await create_tournament(org_id)
course_id = await create_course(org_id)
async with app_db.org_connection(org_id) as conn:
await conn.execute(
"UPDATE course SET source = 'official', external_course_ref = 'tourn-slug:7' WHERE id = $1",
course_id,
)
await conn.execute(
"""
INSERT INTO teeoff_course_coordinate
(external_course_ref, hole_number, poi_type, location, side_fairway, latitude, longitude)
VALUES ('tourn-slug:7', 9, 'rock', 'front', 'center', 59.3, 10.6)
"""
)
for n in range(1, 19):
await create_org_hole(org_id, course_id, hole_number=n)
tournament_round_id = await create_tournament_round(org_id, tournament_id, course_id)
points = await get_tournament_round_hole_target_points(
tournament_id, tournament_round_id, 9, organization_id=org_id,
)
assert len(points) == 1
assert points[0].poi_type == "rock"
with pytest.raises(HTTPException) as exc_info:
await get_tournament_round_hole_target_points(
"00000000-0000-0000-0000-000000000000", tournament_round_id, 9, organization_id=org_id,
)
assert exc_info.value.status_code == 404
async def test_match_target_points_official_course_and_access_control(pool):
member_id = await create_user()
outsider_id = await create_user()
org_id = await create_org()
await add_membership(org_id, member_id, role="member")
tournament_id = await create_tournament(org_id)
course_id = await create_course(org_id)
async with app_db.org_connection(org_id) as conn:
await conn.execute(
"UPDATE course SET source = 'official', external_course_ref = 'match-slug:3' WHERE id = $1",
course_id,
)
await conn.execute(
"""
INSERT INTO teeoff_course_coordinate
(external_course_ref, hole_number, poi_type, location, side_fairway, latitude, longitude)
VALUES ('match-slug:3', 2, 'water', 'back', 'right', 59.4, 10.7)
"""
)
team_a = await create_team(org_id, tournament_id)
team_b = await create_team(org_id, tournament_id)
session_id = await create_session(org_id, tournament_id, course_id)
match_id = await create_match(org_id, session_id, team_a, team_b)
points = await get_match_hole_target_points(
match_id, 2, organization_id=org_id, user=CurrentUser(user_id=member_id),
)
assert len(points) == 1
assert points[0].poi_type == "water"
with pytest.raises(HTTPException) as exc_info:
await get_match_hole_target_points(
match_id, 2, organization_id=org_id, user=CurrentUser(user_id=outsider_id),
)
assert exc_info.value.status_code == 403
async def test_course_coordinates_write_path_requires_official_and_fully_replaces(pool):
org_id = await create_org()
course_id = await create_course(org_id) # 'custom' by default
# 400 på ikke-offisiell bane.
with pytest.raises(HTTPException) as exc_info:
await replace_course_coordinates(
course_id,
[CoursePointIn(hole_number=1, poi_type="green", location="middle", side_fairway="center",
latitude=59.0, longitude=10.0)],
organization_id=org_id,
)
assert exc_info.value.status_code == 400
# GET returnerer stille tom liste for ikke-offisiell bane (ikke feil).
assert await list_course_coordinates(course_id, organization_id=org_id) == []
async with app_db.org_connection(org_id) as conn:
await conn.execute(
"UPDATE course SET source = 'official', external_course_ref = 'write-slug:1' WHERE id = $1",
course_id,
)
first_set = [
CoursePointIn(hole_number=1, poi_type="green", location="front", side_fairway="center",
latitude=59.0, longitude=10.0),
CoursePointIn(hole_number=1, poi_type="water", location="back", side_fairway="left",
latitude=59.01, longitude=10.01),
]
result = await replace_course_coordinates(course_id, first_set, organization_id=org_id)
assert len(result) == 2
# Ny PUT ERSTATTER hele settet -- ikke et tillegg.
second_set = [
CoursePointIn(hole_number=2, poi_type="rock", location="middle", side_fairway="right",
latitude=59.02, longitude=10.02),
]
result = await replace_course_coordinates(course_id, second_set, organization_id=org_id)
assert len(result) == 1
assert result[0].hole_number == 2
assert result[0].poi_type == "rock"
read_back = await list_course_coordinates(course_id, organization_id=org_id)
assert len(read_back) == 1
assert read_back[0].poi_type == "rock"