Limita le scritture del database a chi le fa (M11, DD-023).
Le tabelle della v1.0 sono nate con policy USING (true) per anon e authenticated. M4 ha tolto il GRANT ad anon e la cosa è passata per "ora è chiuso", ma per gli autenticati non era rimasto nessun limite. Verificato sul database locale con un utente appena creato, senza ruolo e senza slot nella rosa: POST su eventi_app risponde 201, DELETE risponde 200. Qualsiasi giocatore loggato poteva svuotare il calendario o riscrivere il voto di un altro parlando direttamente con PostgREST, saltando l'interfaccia che quei pulsanti glieli nasconde. Il permesso viveva solo nei componenti, cioè nel posto che un attaccante non usa. M11 fa dire alle policy quello che l'interfaccia già fa: eventi_app agli admin, risposte_presenze e cacche_partita alla propria riga, i tre voti al proprio votante_id. L'admin resta incluso ovunque, perché DD-017 gli riconosce già il diritto di agire al posto del giocatore. turni_palloni e le tabelle scout restano aperte di proposito: nell'interfaccia non hanno nessun gate, quindi stringerle sarebbe una funzionalità nuova e non una messa in sicurezza. Un test lo fissa, così se il gate arriva qualcuno se ne accorge. L'identità è lo slot di giocatori_squadra collegato all'account, con lo stesso EXISTS delle policy dei profili: mio_giocatore_id() di M2 era già stata rimossa dalla migration di correzione e non va reintrodotta. I cinque test nuovi in permessi.test.ts hanno ognuno il proprio controllo positivo — l'admin crea l'evento, il giocatore salva la propria presenza — perché un database che rifiuta tutto passerebbe qualsiasi test di sola negazione. Provata con db reset da zero; non applicata in produzione. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+23
-4
@@ -5,6 +5,25 @@ sono le migration in `supabase/migrations/`: **una tabella nuova va documentata
|
|||||||
stessa modifica che la crea**. Le funzionalità future stanno in [ROADMAP.md](ROADMAP.md),
|
stessa modifica che la crea**. Le funzionalità future stanno in [ROADMAP.md](ROADMAP.md),
|
||||||
non in questo file.
|
non in questo file.
|
||||||
|
|
||||||
|
## Permessi di scrittura
|
||||||
|
|
||||||
|
Chi può scrivere cosa, dopo la migration `m11_scritture_per_ruolo` (DD-023). La **lettura**
|
||||||
|
resta aperta a tutti gli autenticati su ogni tabella di questo elenco; `anon` non arriva a
|
||||||
|
nessuna di esse da M4 (DD-011).
|
||||||
|
|
||||||
|
| Tabella | Chi può scrivere |
|
||||||
|
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||||
|
| `eventi_app` | solo admin (nell'app li gestisce la rotta `/eventi`, già riservata) |
|
||||||
|
| `risposte_presenze`, `cacche_partita` | il giocatore sulla propria riga (`giocatore_id`), più gli admin |
|
||||||
|
| `pagelle_voti`, `mvp_voti`, `badge_social_voti` | il votante sui propri voti (`votante_id`), più gli admin |
|
||||||
|
| `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` | qualsiasi autenticato: nell'interfaccia non hanno gate |
|
||||||
|
| `profili_giocatore` | il giocatore sul proprio profilo, admin su tutti (DD-016, DD-017) |
|
||||||
|
| `giocatori_squadra` | admin; il giocatore può solo reclamare uno slot libero (DD-016) |
|
||||||
|
| `user_roles` | solo admin |
|
||||||
|
|
||||||
|
L'identità del giocatore è lo slot di `giocatori_squadra` con `auth_user_id = auth.uid()`.
|
||||||
|
La tabella è verificata da `test/integration/permessi.test.ts` contro il database locale.
|
||||||
|
|
||||||
## Anagrafica e utenti
|
## Anagrafica e utenti
|
||||||
|
|
||||||
| Tabella | Scopo | Note |
|
| Tabella | Scopo | Note |
|
||||||
@@ -50,11 +69,11 @@ non in questo file.
|
|||||||
|
|
||||||
## Turni e notifiche
|
## Turni e notifiche
|
||||||
|
|
||||||
| Tabella | Scopo | Note |
|
| Tabella | Scopo | Note |
|
||||||
| -------------------- | --------------------------------------------- | ---- |
|
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. |
|
| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. |
|
||||||
| `push_subscriptions` | Dispositivi registrati per le notifiche Push. | |
|
| `push_subscriptions` | Dispositivi registrati per le notifiche Push. | |
|
||||||
| `promemoria_push` | Storico dei promemoria inviati. | |
|
| `promemoria_push` | Storico dei promemoria inviati. | |
|
||||||
|
|
||||||
## Funzioni speciali
|
## Funzioni speciali
|
||||||
|
|
||||||
|
|||||||
@@ -748,3 +748,63 @@ CrAPP è un'app solo chiara. Il blocco `.dark` è rimosso e `:root` dichiara
|
|||||||
**Riesame**
|
**Riesame**
|
||||||
Se arriva una richiesta reale dalla squadra, o se si gioca in palestre al buio abbastanza
|
Se arriva una richiesta reale dalla squadra, o se si gioca in palestre al buio abbastanza
|
||||||
spesso da rendere il tema scuro una funzione e non un vezzo.
|
spesso da rendere il tema scuro una funzione e non un vezzo.
|
||||||
|
|
||||||
|
### DD-023 — Ogni scrittura è limitata a chi la fa
|
||||||
|
|
||||||
|
**Data:** 6 settembre 2026
|
||||||
|
**Stato:** Accettata
|
||||||
|
|
||||||
|
**Contesto**
|
||||||
|
Le tabelle della v1.0 sono nate con policy `USING (true)` per `anon, authenticated`. M4
|
||||||
|
(DD-011) ha tolto il GRANT ad `anon`, e la cosa è stata letta come «ora è chiuso». Non lo
|
||||||
|
era: per gli autenticati non c'era rimasto nessun limite. Verificato sul database locale con
|
||||||
|
un utente appena creato, senza ruolo e senza slot nella rosa: `POST /rest/v1/eventi_app` →
|
||||||
|
201, `DELETE` → 200. Qualsiasi giocatore loggato poteva svuotare il calendario della squadra
|
||||||
|
o riscrivere il voto pagella di un altro, parlando direttamente con PostgREST — senza
|
||||||
|
passare dall'interfaccia, che quei pulsanti glieli nasconde.
|
||||||
|
|
||||||
|
Il permesso viveva quindi solo nei componenti (`useIsAdmin()`, `io.id`), cioè nel posto che
|
||||||
|
un attaccante non usa.
|
||||||
|
|
||||||
|
**Decisione**
|
||||||
|
Le policy rispecchiano quello che l'interfaccia già fa. Tre gruppi:
|
||||||
|
|
||||||
|
- **solo admin**: `eventi_app`. Nell'app li scrive unicamente la rotta `/eventi`, che è già
|
||||||
|
riservata agli amministratori.
|
||||||
|
- **solo la propria riga**: `risposte_presenze`, `cacche_partita` (per `giocatore_id`),
|
||||||
|
`pagelle_voti`, `mvp_voti`, `badge_social_voti` (per `votante_id`). L'admin resta incluso,
|
||||||
|
perché DD-017 gli riconosce già il diritto di agire al posto del giocatore.
|
||||||
|
- **invariate**: `turni_palloni` e le tre tabelle scout. Nell'interfaccia non hanno nessun
|
||||||
|
gate — il turno palloni se lo passa chiunque, e chiunque può aprire lo Scout Live —
|
||||||
|
quindi stringerle sarebbe una funzionalità nuova, non una messa in sicurezza.
|
||||||
|
|
||||||
|
L'identità è lo slot di `giocatori_squadra` collegato all'account, espresso con lo stesso
|
||||||
|
`EXISTS` che usano le policy dei profili — la funzione `mio_giocatore_id()` di M2 è stata
|
||||||
|
rimossa dalla migration di correzione e non si reintroduce. Regge perché `io.id` non è una
|
||||||
|
scelta libera: dopo DD-011 e DD-018 il `localStorage` viene forzato sullo slot collegato
|
||||||
|
all'account (`benvenuto.tsx`), e senza slot non si entra.
|
||||||
|
|
||||||
|
**Alternative scartate**
|
||||||
|
|
||||||
|
- Lasciare tutto aperto e documentarlo → si può difendere per una squadra di venti persone
|
||||||
|
che si conoscono, ma non regge il primo account che passa di mano o il primo telefono
|
||||||
|
perso, e rende ogni bug indistinguibile da un dispetto.
|
||||||
|
- Controllare i permessi nelle route server → CrAPP scrive dal client con supabase-js. Ci
|
||||||
|
vorrebbe un livello API che oggi non esiste, per ottenere quello che la RLS fa da sola.
|
||||||
|
- Stringere anche palloni e scout → cambierebbe come funziona la squadra, e nessuno l'ha
|
||||||
|
chiesto.
|
||||||
|
|
||||||
|
**Conseguenze**
|
||||||
|
|
||||||
|
- Un giocatore che non ha ancora collegato lo slot non scrive più niente: l'`EXISTS` non
|
||||||
|
trova nessuna riga. È lo stesso muro di `/benvenuto`, ora applicato anche al database.
|
||||||
|
- La lettura resta aperta a tutti gli autenticati, pagelle comprese: chi interroga PostgREST
|
||||||
|
può ancora vedere **chi** ha votato cosa. L'anonimato delle pagelle è una scelta di
|
||||||
|
interfaccia, non una garanzia del database, e questa migration non lo cambia.
|
||||||
|
- I test in `test/integration/permessi.test.ts` diventano la definizione eseguibile di questa
|
||||||
|
tabella dei permessi.
|
||||||
|
|
||||||
|
**Riesame**
|
||||||
|
Se l'anonimato delle pagelle deve diventare reale (servirebbe una vista aggregata e la
|
||||||
|
chiusura della lettura riga per riga), o se turni e scout acquistano un gate
|
||||||
|
nell'interfaccia: allora le loro policy devono seguirlo.
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
-- M11 — Le scritture seguono i permessi dell'interfaccia (DD-023).
|
||||||
|
--
|
||||||
|
-- Le tabelle della v1.0 avevano policy `USING (true)` per anon e authenticated. M4 ha
|
||||||
|
-- tolto il GRANT ad anon, ma per gli autenticati non era rimasto nessun limite: un
|
||||||
|
-- giocatore qualsiasi poteva cancellare tutti gli eventi o riscrivere il voto di un altro
|
||||||
|
-- parlando direttamente con PostgREST, senza passare dall'interfaccia che quei pulsanti
|
||||||
|
-- glieli nasconde.
|
||||||
|
--
|
||||||
|
-- Da qui in poi le policy dicono quello che i componenti già fanno:
|
||||||
|
-- eventi_app -> solo admin (la rotta /eventi è loro)
|
||||||
|
-- risposte_presenze, cacche_partita -> la propria riga (giocatore_id)
|
||||||
|
-- pagelle_voti, mvp_voti, badge_social_voti -> il proprio voto (votante_id)
|
||||||
|
-- turni_palloni, scout_* -> invariate: nell'app non hanno gate
|
||||||
|
--
|
||||||
|
-- La lettura non cambia: resta aperta a tutti gli autenticati.
|
||||||
|
--
|
||||||
|
-- L'identità è lo slot di `giocatori_squadra` collegato all'account, con lo stesso
|
||||||
|
-- EXISTS usato dalle policy dei profili (migration di correzione, DD-017): la funzione
|
||||||
|
-- `mio_giocatore_id()` di M2 lì è stata rimossa e non va reintrodotta. Regge perché dopo
|
||||||
|
-- DD-018 il giocatore selezionato sul dispositivo è sempre lo slot dell'account.
|
||||||
|
|
||||||
|
-- --- eventi: li gestisce chi ha /eventi, cioè un amministratore ---------------------
|
||||||
|
DROP POLICY "Chiunque puo creare eventi" ON public.eventi_app;
|
||||||
|
DROP POLICY "Chiunque puo modificare eventi" ON public.eventi_app;
|
||||||
|
DROP POLICY "Chiunque puo eliminare eventi" ON public.eventi_app;
|
||||||
|
|
||||||
|
CREATE POLICY "Gli admin gestiscono gli eventi" ON public.eventi_app
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (public.has_role(auth.uid(), 'admin'::public.app_role))
|
||||||
|
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
|
||||||
|
|
||||||
|
-- --- presenze: ognuno risponde per sé ----------------------------------------------
|
||||||
|
DROP POLICY "Chiunque puo salvare la propria presenza" ON public.risposte_presenze;
|
||||||
|
DROP POLICY "Chiunque puo aggiornare la propria presenza" ON public.risposte_presenze;
|
||||||
|
DROP POLICY "Chiunque puo togliere la propria presenza" ON public.risposte_presenze;
|
||||||
|
|
||||||
|
CREATE POLICY "Ognuno gestisce la propria presenza" ON public.risposte_presenze
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Gli admin gestiscono tutte le presenze" ON public.risposte_presenze
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (public.has_role(auth.uid(), 'admin'::public.app_role))
|
||||||
|
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
|
||||||
|
|
||||||
|
-- --- cacche: ognuno dichiara le proprie --------------------------------------------
|
||||||
|
DROP POLICY "Chiunque puo dichiarare le cacche" ON public.cacche_partita;
|
||||||
|
DROP POLICY "Chiunque puo aggiornare le cacche" ON public.cacche_partita;
|
||||||
|
DROP POLICY "Chiunque puo cancellare le cacche" ON public.cacche_partita;
|
||||||
|
|
||||||
|
CREATE POLICY "Ognuno dichiara le proprie cacche" ON public.cacche_partita
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Gli admin gestiscono tutte le cacche" ON public.cacche_partita
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (public.has_role(auth.uid(), 'admin'::public.app_role))
|
||||||
|
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
|
||||||
|
|
||||||
|
-- --- pagelle: ognuno firma i propri voti -------------------------------------------
|
||||||
|
DROP POLICY "Chiunque puo votare le pagelle" ON public.pagelle_voti;
|
||||||
|
DROP POLICY "Chiunque puo cambiare il proprio voto pagella" ON public.pagelle_voti;
|
||||||
|
DROP POLICY "Chiunque puo togliere il proprio voto pagella" ON public.pagelle_voti;
|
||||||
|
|
||||||
|
CREATE POLICY "Ognuno gestisce i propri voti pagella" ON public.pagelle_voti
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Gli admin gestiscono tutte le pagelle" ON public.pagelle_voti
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (public.has_role(auth.uid(), 'admin'::public.app_role))
|
||||||
|
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
|
||||||
|
|
||||||
|
-- --- MVP ----------------------------------------------------------------------------
|
||||||
|
DROP POLICY "Chiunque puo votare l MVP" ON public.mvp_voti;
|
||||||
|
DROP POLICY "Chiunque puo cambiare il proprio voto" ON public.mvp_voti;
|
||||||
|
DROP POLICY "Chiunque puo togliere il proprio voto" ON public.mvp_voti;
|
||||||
|
|
||||||
|
CREATE POLICY "Ognuno gestisce il proprio voto MVP" ON public.mvp_voti
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Gli admin gestiscono tutti i voti MVP" ON public.mvp_voti
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (public.has_role(auth.uid(), 'admin'::public.app_role))
|
||||||
|
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
|
||||||
|
|
||||||
|
-- --- badge social -------------------------------------------------------------------
|
||||||
|
DROP POLICY "Chiunque puo votare i badge social" ON public.badge_social_voti;
|
||||||
|
DROP POLICY "Chiunque puo cambiare il proprio voto social" ON public.badge_social_voti;
|
||||||
|
DROP POLICY "Chiunque puo togliere il proprio voto social" ON public.badge_social_voti;
|
||||||
|
|
||||||
|
CREATE POLICY "Ognuno gestisce i propri voti social" ON public.badge_social_voti
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.giocatori_squadra g
|
||||||
|
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Gli admin gestiscono tutti i voti social" ON public.badge_social_voti
|
||||||
|
FOR ALL TO authenticated
|
||||||
|
USING (public.has_role(auth.uid(), 'admin'::public.app_role))
|
||||||
|
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
|
||||||
@@ -3,14 +3,16 @@
|
|||||||
*
|
*
|
||||||
* A differenza di `schema-profili`, che si limita a provare l'utente anonimo, qui si
|
* A differenza di `schema-profili`, che si limita a provare l'utente anonimo, qui si
|
||||||
* creano utenti veri e si interroga il database *come loro*: è l'unico modo per
|
* creano utenti veri e si interroga il database *come loro*: è l'unico modo per
|
||||||
* verificare le policy scritte su `auth.uid()` e la funzione `mio_giocatore_id()`.
|
* verificare policy scritte su `auth.uid()`. È anche la definizione eseguibile della
|
||||||
|
* tabella dei permessi di DD-023 (migration M11).
|
||||||
*
|
*
|
||||||
* Il test **scrive**, quindi gira solo contro l'istanza locale di `npx supabase start`:
|
* Il test **scrive**, quindi gira solo contro l'istanza locale di `npx supabase start`:
|
||||||
* le credenziali le legge da `supabase status`, non da `.env`, così non può puntare per
|
* le credenziali le legge da `supabase status`, non da `.env`, così non può puntare per
|
||||||
* sbaglio alla produzione. Senza stack locale si salta con il motivo.
|
* sbaglio alla produzione. Senza stack locale si salta con il motivo.
|
||||||
*
|
*
|
||||||
* Stato toccato e ripristinato alla fine: gli utenti creati (cancellati), lo slot
|
* Stato toccato e ripristinato alla fine: gli utenti creati (cancellati), gli slot
|
||||||
* reclamato in `giocatori_squadra` e il telefono del profilo g1.
|
* reclamati in `giocatori_squadra`, il telefono del profilo g1 e le righe con il
|
||||||
|
* prefisso `test-permessi`.
|
||||||
*/
|
*/
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { statoLocale } from "../helpers/locale";
|
import { statoLocale } from "../helpers/locale";
|
||||||
@@ -76,6 +78,7 @@ if (!locale) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PASSWORD = "prova-permessi-123";
|
const PASSWORD = "prova-permessi-123";
|
||||||
|
const PREFISSO = "test-permessi";
|
||||||
const idUtenti: string[] = [];
|
const idUtenti: string[] = [];
|
||||||
let telefonoOriginale: string | null = null;
|
let telefonoOriginale: string | null = null;
|
||||||
let tokenAdmin = "";
|
let tokenAdmin = "";
|
||||||
@@ -222,9 +225,143 @@ if (!locale) {
|
|||||||
"l'elenco degli amministratori non è pubblico",
|
"l'elenco degli amministratori non è pubblico",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- M11: le scritture seguono i permessi dell'interfaccia (DD-023) -------------
|
||||||
|
// Prima di M11 ognuna di queste andava a buon fine: le policy della v1.0 erano
|
||||||
|
// `USING (true)` per chiunque fosse autenticato.
|
||||||
|
const EVENTO = `${PREFISSO}-evento`;
|
||||||
|
|
||||||
|
await prova("gli eventi li crea e li cancella solo un amministratore", async () => {
|
||||||
|
const daGiocatore = await rest("eventi_app", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "return=representation" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: `${PREFISSO}-abusivo`,
|
||||||
|
tipo: "allenamento",
|
||||||
|
titolo: "Non deve esistere",
|
||||||
|
data: "2026-01-01",
|
||||||
|
ora: "20:00",
|
||||||
|
luogo: "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.ok(!daGiocatore.ok, `creazione da giocatore rifiutata (${daGiocatore.status})`);
|
||||||
|
|
||||||
|
// Controllo positivo: l'admin deve poterlo fare, altrimenti l'app è rotta.
|
||||||
|
const daAdmin = await rest("eventi_app", tokenAdmin, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "return=representation" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: EVENTO,
|
||||||
|
tipo: "allenamento",
|
||||||
|
titolo: "Allenamento di prova",
|
||||||
|
data: "2026-01-01",
|
||||||
|
ora: "20:00",
|
||||||
|
luogo: "Palestra",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(await righeToccate(daAdmin), 1, "l'admin crea gli eventi");
|
||||||
|
|
||||||
|
const cancella = await rest(`eventi_app?id=eq.${EVENTO}`, tokenGiocatore, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Prefer: "return=representation" },
|
||||||
|
});
|
||||||
|
assert.equal(await righeToccate(cancella), 0, "il giocatore non svuota il calendario");
|
||||||
|
const dopo = await rest(`eventi_app?id=eq.${EVENTO}&select=id`, SERVIZIO);
|
||||||
|
assert.equal(((await dopo.json()) as unknown[]).length, 1, "l'evento è ancora lì");
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("ognuno risponde alla convocazione solo per sé", async () => {
|
||||||
|
const mia = await rest("risposte_presenze", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: "g1", stato: "presente" }),
|
||||||
|
});
|
||||||
|
assert.equal(await righeToccate(mia), 1, "la propria risposta si salva");
|
||||||
|
|
||||||
|
const altrui = await rest("risposte_presenze", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: "g5", stato: "assente" }),
|
||||||
|
});
|
||||||
|
assert.ok(!altrui.ok, `nessuno risponde al posto di un altro (${altrui.status})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("i voti si firmano con il proprio nome", async () => {
|
||||||
|
const mio = await rest("pagelle_voti", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
match_id: EVENTO,
|
||||||
|
votante_id: "g1",
|
||||||
|
votato_id: "g5",
|
||||||
|
voto: 7,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(await righeToccate(mio), 1, "il proprio voto si registra");
|
||||||
|
|
||||||
|
const falso = await rest("pagelle_voti", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
match_id: EVENTO,
|
||||||
|
votante_id: "g5",
|
||||||
|
votato_id: "g1",
|
||||||
|
voto: 10,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.ok(!falso.ok, `non si vota a nome di un altro (${falso.status})`);
|
||||||
|
|
||||||
|
const mvp = await rest("mvp_voti", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
match_id: EVENTO,
|
||||||
|
votante_id: "g5",
|
||||||
|
votato_id: "g1",
|
||||||
|
votato_nome: "Uno",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.ok(!mvp.ok, `vale anche per l'MVP (${mvp.status})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("le cacche le dichiara il diretto interessato", async () => {
|
||||||
|
const mie = await rest("cacche_partita", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: "g1", quantita: 2 }),
|
||||||
|
});
|
||||||
|
assert.equal(await righeToccate(mie), 1, "le proprie si dichiarano");
|
||||||
|
|
||||||
|
const altrui = await rest("cacche_partita", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: "g5", quantita: 9 }),
|
||||||
|
});
|
||||||
|
assert.ok(!altrui.ok, `quelle di un altro no (${altrui.status})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// I turni palloni restano aperti di proposito: nell'interfaccia il turno se lo passa
|
||||||
|
// chiunque, senza gate. Se un giorno arriva il gate, questo test va cambiato.
|
||||||
|
await prova("il turno palloni resta assegnabile da chiunque sia autenticato", async () => {
|
||||||
|
const res = await rest("turni_palloni", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||||
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: "g5", aggiornato_da: "g1" }),
|
||||||
|
});
|
||||||
|
assert.equal(await righeToccate(res), 1, "DD-023 lascia questa tabella invariata");
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// Ripristino: prima gli slot (serve il JWT admin, il trigger rifiuta la service key),
|
// Ripristino: prima le righe create (la service role passa sopra alle policy di M11),
|
||||||
// poi il telefono, infine gli utenti.
|
// poi gli slot (serve il JWT admin, il trigger rifiuta la service key), il telefono e
|
||||||
|
// infine gli utenti.
|
||||||
|
for (const tabella of ["risposte_presenze", "cacche_partita", "turni_palloni"]) {
|
||||||
|
await rest(`${tabella}?evento_id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
for (const tabella of ["pagelle_voti", "mvp_voti"]) {
|
||||||
|
await rest(`${tabella}?match_id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
await rest(`eventi_app?id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
||||||
|
|
||||||
for (const id of ["g1", "g2"]) {
|
for (const id of ["g1", "g2"]) {
|
||||||
if (tokenAdmin) {
|
if (tokenAdmin) {
|
||||||
await rest(`giocatori_squadra?id=eq.${id}`, tokenAdmin, {
|
await rest(`giocatori_squadra?id=eq.${id}`, tokenAdmin, {
|
||||||
|
|||||||
Reference in New Issue
Block a user