teecup/tests/test_tournament_round_shots.py
Erol Haagenrud 885dcac4e5 Slag-for-slag GPS-avstandsmåling for org-turneringer (ADR-103)
Ny tournament_round_shot-tabell (org-scopet RLS, FK-et til
tournament_round_participant + hole_number siden tournament_round_hole
kanskje ikke finnes ennå). ShotMeasurementSheet/MapPointPicker gjenbrukt
uendret; ShotMeasurementEntry generalisert fra en lokal round-detail.tsx-
funksjon til en delt, URL-parameterisert komponent. Deling går til den
rundespesifikke kommentartråden (ADR-102) i stedet for Banter Board.
Autorisasjon speiler update_hole (self/org-admin), strengere enn ADR-102s
kommentar-nivå.

Migrasjon 091 IKKE kjørt mot ekte teecup_db ennå -- venter på
brukerbekreftelse før utrulling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 07:16:14 +02:00

264 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Slag-for-slag GPS-avstandsmåling for org-turneringer (ADR-103). Speiler
ADR-048s round_shot-autorisasjon (self ELLER org-admin, samme klasse som
update_hole), men org-scopet (RLS) og FK-et til
tournament_round_participant + hole_number (ikke tournament_round_hole_id
-- den raden finnes kanskje ikke ennå).
"""
import pytest
from fastapi import HTTPException
from app.auth import CurrentUser
from app.routers.tournament_round_shots import (
ShotIn,
ShotShareIn,
create_tournament_round_shot,
delete_tournament_round_shot,
list_tournament_round_shots,
share_tournament_round_shot,
)
from tests.conftest import (
add_membership,
create_course,
create_org,
create_player,
create_tee,
create_tournament,
create_tournament_participant,
create_tournament_round,
create_tournament_round_participant,
create_user,
)
_SHOT = ShotIn(
club="Driver",
distance_meters=210.5,
start_method="gps",
start_lat=59.1,
start_lng=10.5,
end_method="gps",
end_lat=59.101,
end_lng=10.501,
)
async def _setup(org_id):
tournament_id = await create_tournament(org_id, name="Slagmåling-turnering")
course_id = await create_course(org_id, name="Slagmålingsbanen")
tee_id = await create_tee(org_id, course_id, name="Gul")
round_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
return tournament_id, round_id, tee_id
async def _make_participant(org_id, tournament_id, round_id, tee_id, user_id):
player_id = await create_player(org_id, display_name="Slag Testesen", 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, round_id, tp_id, tee_id)
return rp_id
async def test_create_and_list_shot(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
user_id = await create_user()
await add_membership(org_id, user_id)
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, user_id)
created = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT,
organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert created.shot_number == 1
assert created.club == "Driver"
assert created.shared_tournament_round_message_id is None
listed = await list_tournament_round_shots(
tournament_id, round_id, rp_id, 1, organization_id=org_id,
)
assert [s.id for s in listed] == [created.id]
async def test_shot_number_increments_per_hole(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
user_id = await create_user()
await add_membership(org_id, user_id)
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, user_id)
first = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=user_id),
)
second = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert first.shot_number == 1
assert second.shot_number == 2
# Et annet hull starter på nytt fra 1.
other_hole = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 2, _SHOT, organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert other_hole.shot_number == 1
async def test_can_measure_before_hole_score_exists(pool):
"""Kjernebegrunnelsen for datamodellen: tournament_round_hole
opprettes FØRST ved score-innsending -- måling må fungere FØR det."""
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
user_id = await create_user()
await add_membership(org_id, user_id)
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, user_id)
# Ingen tournament_round_hole-rad finnes -- måling skal likevel virke.
created = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 5, _SHOT, organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert created.shot_number == 1
async def test_non_self_non_admin_rejected(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
owner_id = await create_user()
other_id = await create_user()
await add_membership(org_id, owner_id, role="member")
await add_membership(org_id, other_id, role="member")
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, owner_id)
with pytest.raises(HTTPException) as exc_info:
await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=other_id),
)
assert exc_info.value.status_code == 403
async def test_org_admin_can_measure_for_others(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
owner_id = await create_user()
admin_id = await create_user()
await add_membership(org_id, owner_id, role="member")
await add_membership(org_id, admin_id, role="admin")
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, owner_id)
created = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=admin_id),
)
assert created.shot_number == 1
async def test_delete_self_only(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
owner_id = await create_user()
other_id = await create_user()
await add_membership(org_id, owner_id, role="member")
await add_membership(org_id, other_id, role="member")
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, owner_id)
created = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=owner_id),
)
with pytest.raises(HTTPException) as exc_info:
await delete_tournament_round_shot(
tournament_id, round_id, created.id, organization_id=org_id, user=CurrentUser(user_id=other_id),
)
assert exc_info.value.status_code == 403
await delete_tournament_round_shot(
tournament_id, round_id, created.id, organization_id=org_id, user=CurrentUser(user_id=owner_id),
)
listed = await list_tournament_round_shots(tournament_id, round_id, rp_id, 1, organization_id=org_id)
assert listed == []
def test_invalid_distance_rejected():
with pytest.raises(Exception):
ShotIn(
club="Driver", distance_meters=0, start_method="gps",
start_lat=59.1, start_lng=10.5, end_lat=59.101, end_lng=10.501,
)
async def test_wrong_tournament_id_404(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
other_tournament_id = await create_tournament(org_id, name="En annen turnering")
user_id = await create_user()
await add_membership(org_id, user_id)
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, user_id)
with pytest.raises(HTTPException) as exc_info:
await create_tournament_round_shot(
other_tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert exc_info.value.status_code == 404
async def test_rls_blocks_cross_org_access(pool):
org_a = await create_org()
org_b = await create_org()
tournament_id, round_id, tee_id = await _setup(org_a)
user_id = await create_user()
await add_membership(org_a, user_id)
await add_membership(org_b, user_id)
rp_id = await _make_participant(org_a, tournament_id, round_id, tee_id, user_id)
await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_a, user=CurrentUser(user_id=user_id),
)
with pytest.raises(HTTPException) as exc_info:
await list_tournament_round_shots(tournament_id, round_id, rp_id, 1, organization_id=org_b)
assert exc_info.value.status_code == 404
async def test_share_creates_tournament_round_message(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
user_id = await create_user()
await add_membership(org_id, user_id)
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, user_id)
created = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert created.shared_tournament_round_message_id is None
shared = await share_tournament_round_shot(
tournament_id, round_id, created.id, ShotShareIn(body="⛳ Driver, 210.5 m hull 1"),
organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert shared.shared_tournament_round_message_id is not None
# Kommentartråden (ADR-102) skal nå ha en ekte melding.
from app.routers.tournament_round_messages import list_tournament_round_messages
messages = await list_tournament_round_messages(
tournament_id, round_id, organization_id=org_id, user=CurrentUser(user_id=user_id),
)
assert any(m.id == shared.shared_tournament_round_message_id for m in messages)
assert any(m.body == "⛳ Driver, 210.5 m hull 1" for m in messages)
async def test_share_rejected_for_non_owner(pool):
org_id = await create_org()
tournament_id, round_id, tee_id = await _setup(org_id)
owner_id = await create_user()
other_id = await create_user()
await add_membership(org_id, owner_id, role="member")
await add_membership(org_id, other_id, role="member")
rp_id = await _make_participant(org_id, tournament_id, round_id, tee_id, owner_id)
created = await create_tournament_round_shot(
tournament_id, round_id, rp_id, 1, _SHOT, organization_id=org_id, user=CurrentUser(user_id=owner_id),
)
with pytest.raises(HTTPException) as exc_info:
await share_tournament_round_shot(
tournament_id, round_id, created.id, ShotShareIn(body="Test"),
organization_id=org_id, user=CurrentUser(user_id=other_id),
)
assert exc_info.value.status_code == 403