Flight/gruppe-scoping i ScoreTab (ADR-099) -- steg 1 av parity-planen
ScoreTab viste hele turneringsfeltet i stedet for brukerens egen utslagsgruppe. Defaulter nå til egen flight med "vis hele feltet"- veksling og fallback når gruppe mangler. is_self beregnes server-side (aldri rå user_id). Lukker et reelt RLS-hull på tournament_round_group funnet underveis (migrasjon 089). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
dd974678fa
commit
4587503d03
7 changed files with 314 additions and 7 deletions
27
089_tournament_round_group_rls.sql
Normal file
27
089_tournament_round_group_rls.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-- =====================================================================
|
||||
-- TeeCup — migrasjon 089
|
||||
-- RLS på tournament_round_group (lukker et hull fra migrasjon 084)
|
||||
-- =====================================================================
|
||||
-- Oppdaget under kartlegging til parity-arbeidet (score-føring i
|
||||
-- turnering vs. frittstående runde, 2026-08-21): tournament_round_group
|
||||
-- (opprettet i 084_tournament_round_groups.sql, utvidet i
|
||||
-- 088_shotgun_start.sql) fikk ALDRI standard org_isolation-RLS-policyen
|
||||
-- som ellers gjelder alle domenetabeller (ADR-003) -- i strid med
|
||||
-- arkitektur-invarianten i CLAUDE.md. Ingen begrunnet unntak (i
|
||||
-- motsetning til f.eks. round_shot/round_hole/round_message, som
|
||||
-- bevisst står utenfor RLS per ADR-033 Beslutning A). I praksis kun
|
||||
-- beskyttet av WHERE-ledd i applikasjonskoden frem til nå.
|
||||
--
|
||||
-- Ingen dataendring, kun policy -- alle eksisterende spørringer går
|
||||
-- allerede via org_connection() (setter app.current_org), så dette er
|
||||
-- additivt, ikke en atferdsendring for korrekt scopet kode.
|
||||
-- =====================================================================
|
||||
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
ALTER TABLE tournament_round_group ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tournament_round_group FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY org_isolation ON tournament_round_group
|
||||
USING (organization_id = current_setting('app.current_org', true)::uuid)
|
||||
WITH CHECK (organization_id = current_setting('app.current_org', true)::uuid);
|
||||
|
|
@ -10005,6 +10005,69 @@ Ingen migrasjon.
|
|||
|
||||
---
|
||||
|
||||
## ADR-099: Flight/gruppe-scoping i ScoreTab (2026-08-21)
|
||||
|
||||
**Kontekst:** bruker ba om full opplevelses-paritet mellom score-føring i
|
||||
en org-turnering og en frittstående individuell runde ("Jeg skal i
|
||||
utgangspunktet ha en identisk opplevelse når jeg fører score for meg (og
|
||||
eventuelt de andre i flighten min)..."). Grundig undersøkelse fant det
|
||||
mest strukturelle gapet: `ScoreTab` viste HELE turneringsfeltet
|
||||
(alfabetisk, alle deltakere) i stedet for kun brukerens egen flight/
|
||||
gruppe -- `tournament_round_group` (bygget for utslags-/tee-sheet-paring,
|
||||
ADR-096) ble KUN lest av administrator-parings-skjermen, aldri av selve
|
||||
score-føringsskjermen. Første punkt i en avtalt 8-punkts parity-
|
||||
rekkefølge; auto-advance mellom spillere i flighten er et EGET, senere
|
||||
steg -- denne runden endrer kun HVEM som vises/velges som standard.
|
||||
|
||||
**Beslutning A -- visningsendring, IKKE en ny autorisasjonsmodell.**
|
||||
`update_hole` er allerede self-only (eller org-admin) via
|
||||
`user_is_own_tournament_participant` (`team_authz.py`, innstrammet
|
||||
2026-07-30). Denne runden endrer kun `ScoreTab`s participant-VELGER
|
||||
(pill-listen) og standardvalg av `participantId` -- en org-admin kan
|
||||
fortsatt bla til og rette en hvilken som helst deltaker via "Vis hele
|
||||
feltet". Lesetilgang på andres hull (`list_round_participant_holes`) og
|
||||
BBB-scoring (bevisst hele-feltet, egen docstring-begrunnelse) er UENDRET.
|
||||
|
||||
**Beslutning B -- fallback til hele feltet når gruppe mangler, ikke en
|
||||
blokkerende tilstand.** Ikke alle runder har kjørt "Grupper og
|
||||
startliste" ennå. `ScoreTab` faller da tilbake til dagens oppførsel (hele
|
||||
feltet) med en synlig inline-merknad om hvorfor, i stedet for å skjule
|
||||
scoring-muligheten. Verifisert eksplisitt i scratch (se Status).
|
||||
|
||||
**Beslutning C -- `is_self` beregnes server-side, ikke rå `user_id` til
|
||||
klienten.** `list_round_participants`/`add_round_participant`/
|
||||
`update_round_participant_tee` (`individual_tournaments.py`) utvidet med
|
||||
`(p.user_id::text = $2)`-sammenligning (samme `is_mine`-mønster som
|
||||
`rounds.py`), IKKE et rått `user_id`-felt -- unngår å lekke andre
|
||||
spilleres kontokobling til enhver org-medlem-klient. `player.user_id` er
|
||||
nullable (spillere uten konto) -- NULL-verdien måtte eksplisitt
|
||||
`COALESCE`-es til `false` (fanget av en dedikert ny test,
|
||||
`test_round_participant_flight_scoping.py`, ellers 500 pga.
|
||||
Pydantic-validering av et NULL-boolsk felt).
|
||||
|
||||
**Beslutning D -- lukket et reelt RLS-hull funnet underveis.**
|
||||
`tournament_round_group` (migrasjon 084/088) manglet HELT standard
|
||||
org_isolation-RLS (bekreftet: ingen `ENABLE ROW LEVEL SECURITY`/
|
||||
`CREATE POLICY` i noen av de to migrasjonene, uten den typen begrunnede
|
||||
unntak `round_shot`/`round_hole` har per ADR-033). Lukket med ny
|
||||
migrasjon `089_tournament_round_group_rls.sql`, samme mønster som
|
||||
`001_initial_schema.sql`. I praksis var isolasjonen tidligere kun
|
||||
app-lagets WHERE-ledd -- additiv, ikke en atferdsendring.
|
||||
|
||||
**Status:** 164/164 pytest (inkl. ny dedikert test), `tsc --noEmit` rent.
|
||||
RLS-policyen verifisert direkte mot en engangs scratch-database (SQL som
|
||||
`teecup_app`-rollen med feil `app.current_org` satt returnerte 0 rader,
|
||||
riktig org 1 rad). Full-stack scratch-verifisering (egen scratch-db, egen
|
||||
API-container, egen MinIO, ekte innlogging via `/auth/login-password`,
|
||||
ingen fetch-mocking) bekreftet: gruppert runde defaulter til egen flight
|
||||
(3 av 6 vist), "Vis hele feltet"/"Vis kun min flight" veksler korrekt
|
||||
begge veier, ugruppert runde faller korrekt tilbake til hele feltet med
|
||||
riktig merknad, mørk modus og smal mobilskjerm uendret lesbare. Alle
|
||||
scratch-ressurser (container/db/rolle) ryddet opp etter verifisering.
|
||||
IKKE bygget/rullet ut mot ekte containere ennå.
|
||||
|
||||
---
|
||||
|
||||
## Utviklingsplan (rekkefølge)
|
||||
|
||||
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
||||
|
|
|
|||
68
CHANGELOG.md
68
CHANGELOG.md
|
|
@ -14233,3 +14233,71 @@ Neste steg:
|
|||
`/public/tournaments/{id}` returnerer `format_type: "individual"`,
|
||||
`/t/{id}/live` svarer 200. Scratch-ressursene (containere, dev-
|
||||
server) ryddet opp og bekreftet stoppet etterpå.
|
||||
|
||||
154. **Rett Thru-kolonnens vertikale justering i offentlig leaderboard,
|
||||
2026-08-21 -- ekte bruk-funnet bug etter #153 sin utrulling.** Bruker
|
||||
rapporterte fra faktisk mobilbruk (teecup.golf, live): THRU-etiketten
|
||||
sto lavere enn I DAG/TOTAL. Årsak: Thru-verdien manglet samme
|
||||
`min-h-10` som `ScoreShape` (brukt av I dag/Total) gir de andre
|
||||
kolonnene -- hele Thru-kolonnen ble dermed lavere, senterte etiketten
|
||||
ut av linje med søsknene sine. Rettet i
|
||||
`public-individual-live.tsx`. Rullet ut sammen med punkt 155.
|
||||
|
||||
155. **Skjul HCP på offentlig leaderboard for rene bruttoslagspill-
|
||||
turneringer, 2026-08-21.** Bruker: "når det er brutto som er
|
||||
hovedkonkurransen trenger vi ikke se hcp i leaderboarden" -- HCP er
|
||||
kun meningsfullt der den faktisk brukes til slagfordeling (netto/
|
||||
stableford/Københavner/BBB), ikke ren brutto/eclectic-gross.
|
||||
`PublicTournamentInfo` += `scoring_method` (manglet før på den
|
||||
offentlige modellen), ny `shouldShowHcp(scoringMethod)`-hjelper i
|
||||
`public-individual-live.tsx` (`GROSS_ONLY_METHODS = {"stroke_gross",
|
||||
"eclectic_gross"}`). Bruker stilte oppfølgingsspørsmål om dette var en
|
||||
systemendring (ja) -- eskalerte til den bredere parity-diskusjonen som
|
||||
punkt 156 svarer på. Rullet ut sammen med punkt 154 (`docker compose
|
||||
build teecup_api teecup_frontend && up -d`).
|
||||
|
||||
156. **Flight/gruppe-scoping i ScoreTab (ADR-099), 2026-08-21 -- steg 1
|
||||
av en avtalt 8-punkts parity-plan.** Bruker: "Jeg skal i utgangspunktet
|
||||
ha en identisk opplevelse når jeg fører score for meg (og eventuelt de
|
||||
andre i flighten min) i en org-turnering som det jeg har når jeg fører
|
||||
en individuell runde." Grundig Explore-undersøkelse kartla hele gapet
|
||||
mellom org-turneringers scoring og frittstående runders scoring
|
||||
(rapportert til bruker, som ba om rekkefølgen). Denne runden bygger
|
||||
punkt 1: `ScoreTab` viste tidligere HELE turneringsfeltet i stedet for
|
||||
brukerens egen flight -- `tournament_round_group` ble kun lest av
|
||||
admin-parings-skjermen. Se ADR-099 for de fire beslutningene (visning
|
||||
ikke autorisasjon; fallback til hele feltet uten gruppe; `is_self`
|
||||
server-side, aldri rå `user_id`; et reelt RLS-hull på
|
||||
`tournament_round_group` funnet og lukket underveis, migrasjon 089).
|
||||
|
||||
**Backend:** `RoundParticipantOut` += `tournament_round_group_id`,
|
||||
`is_self` (tre spørringer i `individual_tournaments.py` utvidet:
|
||||
`add_round_participant`, `list_round_participants`,
|
||||
`update_round_participant_tee`). Ny migrasjon
|
||||
`089_tournament_round_group_rls.sql`.
|
||||
|
||||
**Frontend:** `ScoreTab` (`individual-tournament-detail.tsx`)
|
||||
defaulter pill-listen og `participantId`-valget til egen gruppe når
|
||||
en finnes, med "Vis hele feltet"/"Vis kun min flight"-veksling og en
|
||||
inline-merknad når runden mangler gruppeinndeling. Ingen ny V0-runde
|
||||
-- filter+lenke lagt til en allerede eksisterende, V0-bygget skjerm.
|
||||
|
||||
**Verifisert:** 164/164 pytest (inkl. ny
|
||||
`test_round_participant_flight_scoping.py` -- fanget en reell NULL-
|
||||
håndteringsbug: `p.user_id::text = $2` ga SQL NULL, ikke `false`, for
|
||||
spillere uten konto, rettet med `COALESCE`), `tsc --noEmit` rent.
|
||||
RLS-policyen verifisert direkte (engangs scratch-db: `teecup_app`-
|
||||
rollen med feil `app.current_org` satt fikk 0 rader, riktig org 1
|
||||
rad). Full-stack scratch-verifisering (egen scratch-db, egen
|
||||
API-container, egen isolert MinIO, ekte innlogging via
|
||||
`/auth/login-password`, INGEN fetch-mocking -- et tidligere forsøk med
|
||||
`window.fetch`-mocking i chrome-devtools-MCP-miljøet brøt selve
|
||||
sidenavigasjonen på en måte som ikke var reproduserbar med ekte
|
||||
nettverkstrafikk, byttet derfor til en ekte scratch-backend i stedet
|
||||
for å grave videre i et miljøspesifikt verktøyproblem) bekreftet:
|
||||
gruppert runde defaulter riktig til 3 av 6 (egen flight), veksling
|
||||
fungerer begge veier, ugruppert runde faller korrekt tilbake med
|
||||
riktig merknad, mørk modus og 390px mobilskjerm uendret lesbare.
|
||||
Alle scratch-ressurser (API-/MinIO-container, db, rolle) ryddet opp
|
||||
etter verifisering. IKKE bygget/rullet ut mot ekte containere ennå --
|
||||
venter på bekreftelse.
|
||||
|
|
|
|||
|
|
@ -520,6 +520,12 @@ class RoundParticipantOut(BaseModel):
|
|||
# level), speilet ut her slik at frontend vet om HoleGrid-cellen skal
|
||||
# åpne det utvidede HoleStatsSheet-et for denne deltakeren.
|
||||
stat_level: str
|
||||
# Flight-scoping i ScoreTab (ADR-099) -- gruppe-id fra migrasjon 084
|
||||
# (NULL = ikke gruppert ennå) og om raden er den innloggede brukeren
|
||||
# selv (aldri rå user_id til klienten -- samme is_mine-mønster som
|
||||
# rounds.py).
|
||||
tournament_round_group_id: str | None
|
||||
is_self: bool
|
||||
|
||||
|
||||
async def _compute_round_participant_handicap(conn, round_participant_id: str) -> None:
|
||||
|
|
@ -637,6 +643,7 @@ async def add_round_participant(
|
|||
round_id: str,
|
||||
body: RoundParticipantCreate,
|
||||
organization_id: str = Depends(get_authorized_org),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> RoundParticipantOut:
|
||||
async with org_connection(organization_id) as conn, translate_db_errors():
|
||||
await _validate_round_tee_choice(conn, tournament_id, round_id, body.tournament_participant_id, body.tee_id)
|
||||
|
|
@ -664,7 +671,9 @@ async def add_round_participant(
|
|||
"""
|
||||
SELECT trp.id::text AS id, trp.tournament_participant_id::text AS tournament_participant_id,
|
||||
p.display_name AS player_name, trp.tee_id::text AS tee_id, tee.name AS tee_name,
|
||||
trp.course_handicap, trp.playing_handicap, tp.stat_level
|
||||
trp.course_handicap, trp.playing_handicap, tp.stat_level,
|
||||
trp.tournament_round_group_id::text AS tournament_round_group_id,
|
||||
COALESCE(p.user_id::text = $2, false) AS is_self
|
||||
FROM tournament_round_participant trp
|
||||
JOIN tournament_participant tp ON tp.id = trp.tournament_participant_id
|
||||
JOIN player p ON p.id = tp.player_id
|
||||
|
|
@ -672,6 +681,7 @@ async def add_round_participant(
|
|||
WHERE trp.id = $1
|
||||
""",
|
||||
round_participant_id,
|
||||
user.user_id,
|
||||
)
|
||||
return RoundParticipantOut(**dict(out))
|
||||
|
||||
|
|
@ -684,13 +694,16 @@ async def list_round_participants(
|
|||
tournament_id: str,
|
||||
round_id: str,
|
||||
organization_id: str = Depends(get_authorized_org),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> list[RoundParticipantOut]:
|
||||
async with org_connection(organization_id) as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT trp.id::text AS id, trp.tournament_participant_id::text AS tournament_participant_id,
|
||||
p.display_name AS player_name, trp.tee_id::text AS tee_id, tee.name AS tee_name,
|
||||
trp.course_handicap, trp.playing_handicap, tp.stat_level
|
||||
trp.course_handicap, trp.playing_handicap, tp.stat_level,
|
||||
trp.tournament_round_group_id::text AS tournament_round_group_id,
|
||||
COALESCE(p.user_id::text = $2, false) AS is_self
|
||||
FROM tournament_round_participant trp
|
||||
JOIN tournament_participant tp ON tp.id = trp.tournament_participant_id
|
||||
JOIN player p ON p.id = tp.player_id
|
||||
|
|
@ -699,6 +712,7 @@ async def list_round_participants(
|
|||
ORDER BY p.display_name
|
||||
""",
|
||||
round_id,
|
||||
user.user_id,
|
||||
)
|
||||
return [RoundParticipantOut(**dict(r)) for r in rows]
|
||||
|
||||
|
|
@ -733,6 +747,7 @@ async def update_round_participant_tee(
|
|||
round_participant_id: str,
|
||||
body: RoundParticipantTeeUpdate,
|
||||
organization_id: str = Depends(get_authorized_org),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> RoundParticipantOut:
|
||||
async with org_connection(organization_id) as conn, translate_db_errors():
|
||||
existing = await conn.fetchrow(
|
||||
|
|
@ -759,7 +774,9 @@ async def update_round_participant_tee(
|
|||
"""
|
||||
SELECT trp.id::text AS id, trp.tournament_participant_id::text AS tournament_participant_id,
|
||||
p.display_name AS player_name, trp.tee_id::text AS tee_id, tee.name AS tee_name,
|
||||
trp.course_handicap, trp.playing_handicap, tp.stat_level
|
||||
trp.course_handicap, trp.playing_handicap, tp.stat_level,
|
||||
trp.tournament_round_group_id::text AS tournament_round_group_id,
|
||||
COALESCE(p.user_id::text = $2, false) AS is_self
|
||||
FROM tournament_round_participant trp
|
||||
JOIN tournament_participant tp ON tp.id = trp.tournament_participant_id
|
||||
JOIN player p ON p.id = tp.player_id
|
||||
|
|
@ -767,6 +784,7 @@ async def update_round_participant_tee(
|
|||
WHERE trp.id = $1
|
||||
""",
|
||||
round_participant_id,
|
||||
user.user_id,
|
||||
)
|
||||
return RoundParticipantOut(**dict(out))
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,9 @@ type ApiRoundParticipant = {
|
|||
// "strokes_and_putts" / "full", styrer om HoleGrid sin celle åpner det
|
||||
// enkle inline-tallfeltet (uendret oppførsel) eller HoleStatsSheet.
|
||||
stat_level: "strokes_only" | "strokes_and_putts" | "full"
|
||||
// Flight-scoping i ScoreTab (ADR-099).
|
||||
tournament_round_group_id: string | null
|
||||
is_self: boolean
|
||||
}
|
||||
|
||||
type ApiHole = {
|
||||
|
|
@ -2965,6 +2968,18 @@ function ScoreTab({
|
|||
// fetch) vet de skal hente på nytt.
|
||||
const [refreshTick, setRefreshTick] = useState(0)
|
||||
|
||||
// Flight-scoping (ADR-099) -- standard er å vise KUN egen
|
||||
// utslagsgruppe i velgeren under, med en lenke for å se hele feltet.
|
||||
// roundParticipants selv holdes ALLTID ufiltrert (BBB/Flag-panelene
|
||||
// under bruker den direkte, uavhengig av dette).
|
||||
const [showFullField, setShowFullField] = useState(false)
|
||||
const myParticipant = roundParticipants.find((rp) => rp.is_self) ?? null
|
||||
const myGroupId = myParticipant?.tournament_round_group_id ?? null
|
||||
const visibleParticipants =
|
||||
showFullField || !myGroupId
|
||||
? roundParticipants
|
||||
: roundParticipants.filter((rp) => rp.tournament_round_group_id === myGroupId)
|
||||
|
||||
useEffect(() => {
|
||||
if (!roundId) return
|
||||
let cancelled = false
|
||||
|
|
@ -2972,7 +2987,11 @@ function ScoreTab({
|
|||
if (cancelled) return
|
||||
const list = data ?? []
|
||||
setRoundParticipants(list)
|
||||
setParticipantId(list[0]?.id ?? "")
|
||||
setShowFullField(false)
|
||||
const self = list.find((rp) => rp.is_self)
|
||||
const selfGroupId = self?.tournament_round_group_id ?? null
|
||||
const defaultList = selfGroupId ? list.filter((rp) => rp.tournament_round_group_id === selfGroupId) : list
|
||||
setParticipantId(self?.id ?? defaultList[0]?.id ?? "")
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
|
|
@ -3068,7 +3087,7 @@ function ScoreTab({
|
|||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{roundParticipants.map((rp) => (
|
||||
{visibleParticipants.map((rp) => (
|
||||
<button
|
||||
key={rp.id}
|
||||
type="button"
|
||||
|
|
@ -3086,6 +3105,20 @@ function ScoreTab({
|
|||
))}
|
||||
</div>
|
||||
|
||||
{myGroupId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFullField((v) => !v)}
|
||||
className="self-start text-sm font-bold text-info underline-offset-4 hover:underline"
|
||||
>
|
||||
{showFullField ? "Vis kun min flight" : `Vis hele feltet (${roundParticipants.length})`}
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Ingen gruppeinndeling er satt for denne runden ennå -- viser hele feltet.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{round && holes && (
|
||||
<HoleGrid
|
||||
base={base}
|
||||
|
|
|
|||
93
tests/test_round_participant_flight_scoping.py
Normal file
93
tests/test_round_participant_flight_scoping.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
Flight/gruppe-scoping i ScoreTab (ADR-099, 2026-08-21). list_round_participants
|
||||
speiler nå ut tournament_round_group_id + is_self per rad, slik at frontend kan
|
||||
la score-føringsskjermen default til brukerens egen utslagsgruppe i stedet for
|
||||
hele feltet. is_self beregnes server-side (ikke rå user_id, samme is_mine-
|
||||
mønster som rounds.py) for ikke å lekke andre spilleres kontokobling.
|
||||
"""
|
||||
|
||||
from app.auth import CurrentUser
|
||||
from app.routers.individual_tournaments import (
|
||||
RoundGroupIn,
|
||||
SaveRoundGroupsIn,
|
||||
list_round_participants,
|
||||
save_round_groups,
|
||||
)
|
||||
|
||||
from tests.conftest import (
|
||||
create_org,
|
||||
create_course,
|
||||
create_player,
|
||||
create_tee,
|
||||
create_tournament,
|
||||
create_tournament_participant,
|
||||
create_tournament_round,
|
||||
create_tournament_round_participant,
|
||||
create_user,
|
||||
)
|
||||
|
||||
import app.db as app_db
|
||||
|
||||
|
||||
async def _setup(org_id):
|
||||
tournament_id = await create_tournament(org_id, name="Flight-scoping-turnering")
|
||||
course_id = await create_course(org_id, name="Flight Links")
|
||||
tee_id = await create_tee(org_id, course_id, name="Gul")
|
||||
async with app_db.org_connection(org_id) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE tournament SET format_type = 'individual', scoring_method = 'stroke_gross' WHERE id = $1",
|
||||
tournament_id,
|
||||
)
|
||||
round_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
||||
return tournament_id, round_id, tee_id
|
||||
|
||||
|
||||
async def test_is_self_and_group_id_reflect_viewer_and_grouping(pool):
|
||||
org_id = await create_org()
|
||||
tournament_id, round_id, tee_id = await _setup(org_id)
|
||||
|
||||
me_user_id = await create_user()
|
||||
other_user_id = await create_user()
|
||||
|
||||
me_player_id = await create_player(org_id, display_name="Meg Selv", user_id=me_user_id)
|
||||
other_player_id = await create_player(org_id, display_name="En Annen", user_id=other_user_id)
|
||||
|
||||
me_tp_id = await create_tournament_participant(org_id, tournament_id, me_player_id)
|
||||
other_tp_id = await create_tournament_participant(org_id, tournament_id, other_player_id)
|
||||
|
||||
me_rp_id = await create_tournament_round_participant(org_id, round_id, me_tp_id, tee_id)
|
||||
other_rp_id = await create_tournament_round_participant(org_id, round_id, other_tp_id, tee_id)
|
||||
|
||||
# Ingen gruppe satt ennå -- begge rader skal ha tournament_round_group_id=None.
|
||||
ungrouped = await list_round_participants(
|
||||
tournament_id, round_id, organization_id=org_id, user=CurrentUser(user_id=me_user_id),
|
||||
)
|
||||
by_id = {rp.id: rp for rp in ungrouped}
|
||||
assert by_id[me_rp_id].tournament_round_group_id is None
|
||||
assert by_id[other_rp_id].tournament_round_group_id is None
|
||||
assert by_id[me_rp_id].is_self is True
|
||||
assert by_id[other_rp_id].is_self is False
|
||||
|
||||
# Grupper begge i samme gruppe -- gruppe-id skal nå matche på begge rader,
|
||||
# og is_self skal fortsatt kun være sann for den innloggede brukeren.
|
||||
await save_round_groups(
|
||||
tournament_id, round_id,
|
||||
SaveRoundGroupsIn(groups=[RoundGroupIn(sequence=1, round_participant_ids=[me_rp_id, other_rp_id])]),
|
||||
organization_id=org_id,
|
||||
)
|
||||
grouped = await list_round_participants(
|
||||
tournament_id, round_id, organization_id=org_id, user=CurrentUser(user_id=me_user_id),
|
||||
)
|
||||
by_id = {rp.id: rp for rp in grouped}
|
||||
assert by_id[me_rp_id].tournament_round_group_id is not None
|
||||
assert by_id[me_rp_id].tournament_round_group_id == by_id[other_rp_id].tournament_round_group_id
|
||||
assert by_id[me_rp_id].is_self is True
|
||||
assert by_id[other_rp_id].is_self is False
|
||||
|
||||
# Sett fra den ANDRE brukerens perspektiv: is_self bytter side.
|
||||
as_other = await list_round_participants(
|
||||
tournament_id, round_id, organization_id=org_id, user=CurrentUser(user_id=other_user_id),
|
||||
)
|
||||
by_id = {rp.id: rp for rp in as_other}
|
||||
assert by_id[me_rp_id].is_self is False
|
||||
assert by_id[other_rp_id].is_self is True
|
||||
|
|
@ -13,6 +13,7 @@ får alltid tournament_round_group_id=NULL).
|
|||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.auth import CurrentUser
|
||||
from app.routers.individual_tournaments import (
|
||||
RoundGroupIn,
|
||||
RoundParticipantTeeUpdate,
|
||||
|
|
@ -30,6 +31,7 @@ from tests.conftest import (
|
|||
create_tournament_participant,
|
||||
create_tournament_round,
|
||||
create_tournament_round_participant,
|
||||
create_user,
|
||||
)
|
||||
|
||||
import app.db as app_db
|
||||
|
|
@ -69,8 +71,10 @@ async def test_changing_tee_preserves_group_membership(pool):
|
|||
)
|
||||
assert group_id_before is not None
|
||||
|
||||
user_id = await create_user()
|
||||
updated = await update_round_participant_tee(
|
||||
tournament_id, round_id, rp_id, RoundParticipantTeeUpdate(tee_id=tee_b), organization_id=org_id
|
||||
tournament_id, round_id, rp_id, RoundParticipantTeeUpdate(tee_id=tee_b),
|
||||
organization_id=org_id, user=CurrentUser(user_id=user_id),
|
||||
)
|
||||
assert updated.tee_id == tee_b
|
||||
assert updated.id == rp_id # samme rad, ikke slett+gjenopprett
|
||||
|
|
@ -88,11 +92,12 @@ async def test_changing_tee_to_one_outside_course_is_rejected(pool):
|
|||
other_course_id = await create_course(org_id, name="En annen bane")
|
||||
foreign_tee_id = await create_tee(org_id, other_course_id, name="Blå")
|
||||
|
||||
user_id = await create_user()
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_round_participant_tee(
|
||||
tournament_id, round_id, rp_id,
|
||||
RoundParticipantTeeUpdate(tee_id=foreign_tee_id),
|
||||
organization_id=org_id,
|
||||
organization_id=org_id, user=CurrentUser(user_id=user_id),
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.detail["code"] == "OUT_OF_SCOPE"
|
||||
|
|
|
|||
Loading…
Reference in a new issue