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>
264 lines
10 KiB
Python
264 lines
10 KiB
Python
"""
|
||
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
|