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>
299 lines
12 KiB
Python
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"
|