teecup/tests/test_hole_history.py

257 lines
10 KiB
Python
Raw Normal View History

"""
Spillerens per-hull-historikk (ADR-072). To lag med tester:
1. Rene enhetstester av summarize_hole_history() -- ingen db, bevis at
GIR%/fairway%/gjennomsnitt regnes riktig og hopper over rader uten data.
2. Integrasjonstester mot ekte router-funksjoner (samme mønster som
test_tournament_hole_stats.py/test_concurrency_version_check.py) --
beviser selve bane-broen: samme fysiske (teeoff-)bane spilt både som
frittstående runde OG i en org-turnering i en ANNEN organisasjon enn
spilleren selv eier noe i, skal telles sammen. Custom-baner og
gjestespillere skal BEVISST ekskluderes (bruker-bekreftet scope).
"""
import uuid
from datetime import date
from app.auth import CurrentUser
from app.hole_history import (
HoleInstance,
hole_history_for_user,
resolve_personal_round_course_key,
summarize_hole_history,
)
from app.routers.individual_tournaments import (
HoleUpdate as TournamentHoleUpdate,
update_hole as tournament_update_hole,
)
from app.routers.rounds import HoleUpdate as RoundHoleUpdate, get_hole_history, update_hole as round_update_hole
from app.routers.individual_tournaments import get_round_participant_hole_history
from tests.conftest import (
add_membership,
create_hole,
create_org,
create_org_hole,
create_course,
create_participant,
create_player,
create_round,
create_tee,
create_tournament,
create_tournament_participant,
create_tournament_round,
create_tournament_round_participant,
create_user,
)
import app.db as app_db
# ---------------------------------------------------------------------------
# 1. summarize_hole_history() -- ren funksjon, ingen db.
# ---------------------------------------------------------------------------
def _instance(**overrides) -> HoleInstance:
defaults = dict(
source="personal",
played_at=date(2026, 1, 1),
label="Runde",
score=4,
par=4,
putts=None,
tee_shot_result=None,
approach_result=None,
chip_count=None,
bunker_shot_count=None,
penalty_strokes=None,
first_putt_distance_bucket=None,
)
defaults.update(overrides)
return HoleInstance(**defaults)
def test_summarize_empty_returns_none():
assert summarize_hole_history([]) is None
def test_summarize_counts_and_averages():
instances = [
_instance(source="personal", score=4, par=4),
_instance(source="tournament", score=6, par=4),
]
out = summarize_hole_history(instances)
assert out.times_played == 2
assert out.times_personal == 1
assert out.times_tournament == 1
assert out.average_score == 5.0
assert out.average_score_vs_par == 1.0
assert out.best_score == 4
assert out.worst_score == 6
def test_summarize_gir_percent_only_over_rows_with_putts():
instances = [
_instance(score=4, par=4, putts=2), # 4-2=2 <= 4-2=2 -> GIR
_instance(score=5, par=4, putts=3), # 5-3=2 <= 2 -> GIR
_instance(score=6, par=4, putts=None), # ingen putter registrert -- ekskludert fra GIR%
]
out = summarize_hole_history(instances)
assert out.gir_percent == 100.0
assert out.average_putts == 2.5 # snitt kun over de to MED putter registrert
def test_summarize_gir_percent_none_when_no_putts_data_anywhere():
instances = [_instance(score=4, par=4, putts=None)]
out = summarize_hole_history(instances)
assert out.gir_percent is None
assert out.average_putts is None
def test_summarize_fairway_percent_excludes_par_3_and_missing_data():
instances = [
_instance(par=4, tee_shot_result="fairway"),
_instance(par=4, tee_shot_result="left"),
_instance(par=3, tee_shot_result="fairway"), # par 3 -- ekskludert
_instance(par=5, tee_shot_result=None), # ingen utslagsdata -- ekskludert
]
out = summarize_hole_history(instances)
assert out.fairway_hit_percent == 50.0 # 1 av 2 par-4/5-rader MED data
def test_summarize_orders_most_recent_first_and_undated_last():
instances = [
_instance(played_at=date(2026, 1, 1), score=4),
_instance(played_at=date(2026, 6, 1), score=5),
_instance(played_at=None, score=6),
]
out = summarize_hole_history(instances)
assert [i.score for i in out.instances] == [5, 4, 6]
# ---------------------------------------------------------------------------
# 2. Integrasjon: bane-bro + eksklusjoner.
# ---------------------------------------------------------------------------
async def _score_personal_hole(round_id, participant_id, user_id, hole_number, score, putts=None):
user = CurrentUser(user_id=user_id)
await round_update_hole(
round_id, participant_id, hole_number,
RoundHoleUpdate(score=score, putts=putts, expected_version=None),
user=user,
)
async def _score_tournament_hole(tournament_id, round_id, rp_id, org_id, user_id, hole_number, score, putts=None):
user = CurrentUser(user_id=user_id)
await tournament_update_hole(
tournament_id, round_id, rp_id, hole_number,
TournamentHoleUpdate(gross_strokes=score, putts=putts, expected_version=None),
organization_id=org_id, user=user,
)
async def test_teeoff_bridge_combines_personal_and_tournament_history(pool):
"""Samme spiller, samme fysiske (teeoff-)bane -- én frittstående runde
OG én turneringsrunde i en organisasjon spilleren IKKE eier noe i selv
(kun har en player-rad koblet til app_user der). Historikken vise
BEGGE, uansett hvilken side spørringen kommer fra."""
user_id = await create_user()
# Frittstående runde -- create_round() sin faste teeoff-bane-fixture
# (facility='test-facility', course='test-course').
async with app_db.plain_connection() as conn:
round_id = await create_round(conn, user_id)
participant_id = await create_participant(conn, round_id, user_id=user_id, is_owner=True)
await create_hole(conn, participant_id, hole_number=7, par=4, stroke_index=9)
await _score_personal_hole(round_id, participant_id, user_id, hole_number=7, score=4, putts=2)
# Org-turnering på en bane importert med SAMME teeoff-identitet.
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 = 'test-facility:test-course' WHERE id = $1",
course_id,
)
for n in range(1, 19):
await create_org_hole(org_id, course_id, hole_number=n, par=4 if n != 7 else 4, stroke_index=n)
tee_id = await create_tee(org_id, course_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)
await _score_tournament_hole(tournament_id, tournament_round_id, rp_id, org_id, user_id, hole_number=7, score=5, putts=1)
key = ("teeoff", "test-facility", "test-course")
combined = await hole_history_for_user(user_id, key, 7)
assert combined.times_played == 2
assert combined.times_personal == 1
assert combined.times_tournament == 1
assert sorted(i.score for i in combined.instances) == [4, 5]
# Begge endepunktene skal returnere SAMME kombinerte historikk.
from_personal_endpoint = await get_hole_history(
round_id, participant_id, 7, user=CurrentUser(user_id=user_id)
)
assert from_personal_endpoint.times_played == 2
from_tournament_endpoint = await get_round_participant_hole_history(
tournament_id, tournament_round_id, rp_id, 7, organization_id=org_id
)
assert from_tournament_endpoint.times_played == 2
async def test_custom_personal_course_is_excluded(pool):
"""Håndlaget bane (course_source='custom', ingen GolfAPI-kobling) --
resolve_personal_round_course_key skal returnere None, IKKE en snublende
match mot noe annet."""
user_id = await create_user()
async with app_db.plain_connection() as conn:
personal_course_id = str(uuid.uuid4())
await conn.execute(
"INSERT INTO personal_course (id, created_by_user_id, name) VALUES ($1, $2, 'Hjemmebanen')",
personal_course_id, user_id,
)
for n in range(1, 19):
await conn.execute(
"INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index) "
"VALUES ($1, $2, 4, $3)",
personal_course_id, n, n,
)
round_id = str(uuid.uuid4())
await conn.execute(
"""
INSERT INTO round (id, owner_user_id, course_source, personal_course_id,
course_name_snapshot, tee_name_snapshot, played_at)
VALUES ($1, $2, 'custom', $3, 'Hjemmebanen', 'Gul', CURRENT_DATE)
""",
round_id, user_id, personal_course_id,
)
key = await resolve_personal_round_course_key(conn, round_id)
assert key is None
async def test_guest_tournament_participant_has_no_history(pool):
"""player.user_id IS NULL (gjest, aldri registrert/lenket) -- historikk-
endepunktet skal svare None (ikke feile), samme "skjul panelet"-
kontrakt som en custom bane."""
org_id = await create_org()
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)
tournament_round_id = await create_tournament_round(org_id, tournament_id, course_id)
# user_id utelates bevisst -- create_player() sin default er NULL.
player_id = await create_player(org_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)
out = await get_round_participant_hole_history(
tournament_id, tournament_round_id, rp_id, 1, organization_id=org_id
)
assert out is None