Integrazione (permessi, permessi-allenatore, mvp-badge, s-tiebreak-badge), unit (mvp-voti) e helper delle date di Roma. Con le parità l'MVP vale per tutti i pari merito. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1023 lines
43 KiB
TypeScript
1023 lines
43 KiB
TypeScript
/**
|
|
* Permessi per ruolo sul database locale: `bun test/integration/permessi.test.ts`.
|
|
*
|
|
* 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
|
|
* 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`:
|
|
* 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.
|
|
*
|
|
* Stato toccato e ripristinato alla fine: gli utenti creati (cancellati), gli slot
|
|
* reclamati in `giocatori_squadra`, il telefono del profilo g1 e le righe con il
|
|
* prefisso `test-permessi`.
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { statoLocale } from "../helpers/locale";
|
|
import { giornoSenzaOraAperto, romaTra } from "../helpers/roma";
|
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
|
|
|
const locale = statoLocale();
|
|
|
|
if (!locale) {
|
|
salta("permessi per ruolo", "stack locale non attivo (npx supabase start)");
|
|
riepilogo("permessi");
|
|
} else {
|
|
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
|
console.log(`permessi su ${URL_BASE}`);
|
|
|
|
const rest = (percorso: string, token: string, init?: RequestInit) =>
|
|
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
|
...init,
|
|
headers: {
|
|
apikey: token === SERVIZIO ? SERVIZIO : ANON,
|
|
Authorization: `Bearer ${token}`,
|
|
"content-type": "application/json",
|
|
...(init?.headers ?? {}),
|
|
},
|
|
});
|
|
|
|
/** Numero di righe toccate da una scrittura: con `return=representation` è il corpo. */
|
|
const righeToccate = async (res: Response): Promise<number> => {
|
|
if (!res.ok) return 0;
|
|
const corpo = (await res.json()) as unknown[];
|
|
return Array.isArray(corpo) ? corpo.length : 0;
|
|
};
|
|
|
|
async function creaUtente(email: string, password: string): Promise<string> {
|
|
const res = await fetch(`${URL_BASE}/auth/v1/admin/users`, {
|
|
method: "POST",
|
|
headers: {
|
|
apikey: SERVIZIO,
|
|
Authorization: `Bearer ${SERVIZIO}`,
|
|
"content-type": "application/json",
|
|
},
|
|
body: JSON.stringify({ email, password, email_confirm: true }),
|
|
});
|
|
const corpo = (await res.json()) as { id?: string; msg?: string };
|
|
if (!corpo.id) throw new Error(`creazione utente fallita: ${JSON.stringify(corpo)}`);
|
|
return corpo.id;
|
|
}
|
|
|
|
async function accedi(email: string, password: string): Promise<string> {
|
|
const res = await fetch(`${URL_BASE}/auth/v1/token?grant_type=password`, {
|
|
method: "POST",
|
|
headers: { apikey: ANON, "content-type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
const corpo = (await res.json()) as { access_token?: string };
|
|
if (!corpo.access_token) throw new Error(`accesso fallito: ${JSON.stringify(corpo)}`);
|
|
return corpo.access_token;
|
|
}
|
|
|
|
const eliminaUtente = (id: string) =>
|
|
fetch(`${URL_BASE}/auth/v1/admin/users/${id}`, {
|
|
method: "DELETE",
|
|
headers: { apikey: SERVIZIO, Authorization: `Bearer ${SERVIZIO}` },
|
|
});
|
|
|
|
const PASSWORD = "prova-permessi-123";
|
|
const PREFISSO = "test-permessi";
|
|
const idUtenti: string[] = [];
|
|
let telefonoOriginale: string | null = null;
|
|
let tokenAdmin = "";
|
|
|
|
try {
|
|
// --- preparazione: un giocatore collegato a g1, un amministratore --------------
|
|
const emailGiocatore = `test-giocatore-${Date.now()}@example.test`;
|
|
const emailAdmin = `test-admin-${Date.now()}@example.test`;
|
|
const idGiocatore = await creaUtente(emailGiocatore, PASSWORD);
|
|
const idAdmin = await creaUtente(emailAdmin, PASSWORD);
|
|
idUtenti.push(idGiocatore, idAdmin);
|
|
|
|
await rest("user_roles", SERVIZIO, {
|
|
method: "POST",
|
|
body: JSON.stringify({ user_id: idAdmin, role: "admin" }),
|
|
});
|
|
|
|
const tokenGiocatore = await accedi(emailGiocatore, PASSWORD);
|
|
tokenAdmin = await accedi(emailAdmin, PASSWORD);
|
|
|
|
// La rosa si tocca con il JWT dell'admin, non con la service key: per il trigger
|
|
// `enforce_giocatori_squadra_update` la service key non è un amministratore
|
|
// (`auth.uid()` è NULL) e vede rifiutato qualsiasi UPDATE.
|
|
const collega = (id: string, utente: string | null) =>
|
|
rest(`giocatori_squadra?id=eq.${id}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ auth_user_id: utente }),
|
|
});
|
|
|
|
assert.equal(await righeToccate(await collega("g1", idGiocatore)), 1, "g1 collegato al test");
|
|
assert.equal(await righeToccate(await collega("g2", idAdmin)), 1, "g2 collegato all'admin");
|
|
|
|
const primaProfilo = await rest(
|
|
"profili_giocatore?giocatore_id=eq.g1&select=telefono",
|
|
SERVIZIO,
|
|
);
|
|
telefonoOriginale =
|
|
((await primaProfilo.json()) as Array<{ telefono: string | null }>)[0]?.telefono ?? null;
|
|
|
|
// --- profili: dati personali, la RLS è l'unica barriera -----------------------
|
|
// Senza queste due il documento d'identità di un compagno sarebbe leggibile da
|
|
// chiunque abbia un account.
|
|
await prova("il giocatore vede solo il proprio profilo", async () => {
|
|
const res = await rest("profili_giocatore?select=giocatore_id", tokenGiocatore);
|
|
assert.equal(res.status, 200);
|
|
const righe = (await res.json()) as Array<{ giocatore_id: string }>;
|
|
assert.deepEqual(
|
|
righe.map((r) => r.giocatore_id),
|
|
["g1"],
|
|
"solo il proprio profilo, mai quello degli altri",
|
|
);
|
|
});
|
|
|
|
await prova("il giocatore non modifica il profilo di un altro", async () => {
|
|
const res = await rest("profili_giocatore?giocatore_id=eq.g2", tokenGiocatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ telefono: "999" }),
|
|
});
|
|
assert.equal(await righeToccate(res), 0, "nessuna riga altrui aggiornata");
|
|
const dopo = await rest("profili_giocatore?giocatore_id=eq.g2&select=telefono", SERVIZIO);
|
|
const righe = (await dopo.json()) as Array<{ telefono: string | null }>;
|
|
assert.notEqual(righe[0]?.telefono, "999", "il telefono di g2 è rimasto quello di prima");
|
|
});
|
|
|
|
// Controllo positivo: senza questo, un database completamente rotto passerebbe
|
|
// tutti i test di negazione qui sopra.
|
|
await prova("il giocatore aggiorna il proprio profilo", async () => {
|
|
const res = await rest("profili_giocatore?giocatore_id=eq.g1", tokenGiocatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ telefono: "3331234567" }),
|
|
});
|
|
assert.equal(await righeToccate(res), 1, "il proprio profilo si aggiorna");
|
|
});
|
|
|
|
await prova("il giocatore non cancella profili", async () => {
|
|
const res = await rest("profili_giocatore?giocatore_id=eq.g2", tokenGiocatore, {
|
|
method: "DELETE",
|
|
headers: { Prefer: "return=representation" },
|
|
});
|
|
assert.equal(await righeToccate(res), 0, "la cancellazione è riservata agli admin");
|
|
const dopo = await rest("profili_giocatore?giocatore_id=eq.g2&select=giocatore_id", SERVIZIO);
|
|
assert.equal(((await dopo.json()) as unknown[]).length, 1, "il profilo g2 esiste ancora");
|
|
});
|
|
|
|
await prova("l'amministratore vede tutti i profili", async () => {
|
|
const res = await rest("profili_giocatore?select=giocatore_id", tokenAdmin);
|
|
assert.equal(res.status, 200);
|
|
const righe = (await res.json()) as unknown[];
|
|
assert.ok(righe.length > 1, `l'admin vede l'intero elenco, ne ha visti ${righe.length}`);
|
|
});
|
|
|
|
// --- rosa: il trigger di DD-016 ------------------------------------------------
|
|
// La policy da sola lascerebbe passare un UPDATE che cambia anche numero e ruolo:
|
|
// a chiudere il buco è il trigger `enforce_giocatori_squadra_update`.
|
|
await prova(
|
|
"reclamando uno slot libero il giocatore non cambia anche i suoi dati",
|
|
async () => {
|
|
const res = await rest("giocatori_squadra?id=eq.g3", tokenGiocatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ auth_user_id: idGiocatore, numero: 99 }),
|
|
});
|
|
assert.ok(!res.ok, `il trigger deve rifiutare, invece ha risposto ${res.status}`);
|
|
const dopo = await rest("giocatori_squadra?id=eq.g3&select=numero,auth_user_id", SERVIZIO);
|
|
const righe = (await dopo.json()) as Array<{ numero: number; auth_user_id: string | null }>;
|
|
assert.notEqual(righe[0]?.numero, 99, "il numero di maglia non è cambiato");
|
|
assert.equal(righe[0]?.auth_user_id ?? null, null, "lo slot g3 è rimasto libero");
|
|
},
|
|
);
|
|
|
|
await prova("il giocatore non prende lo slot già assegnato a un altro", async () => {
|
|
const res = await rest("giocatori_squadra?id=eq.g2", tokenGiocatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ auth_user_id: idGiocatore }),
|
|
});
|
|
assert.equal(await righeToccate(res), 0, "gli slot già occupati non si rivendicano");
|
|
const dopo = await rest("giocatori_squadra?id=eq.g2&select=auth_user_id", SERVIZIO);
|
|
const righe = (await dopo.json()) as Array<{ auth_user_id: string | null }>;
|
|
assert.equal(righe[0]?.auth_user_id, idAdmin, "g2 è rimasto del suo proprietario");
|
|
});
|
|
|
|
// --- ruoli: la scalata di privilegi --------------------------------------------
|
|
await prova("il giocatore non si assegna il ruolo admin", async () => {
|
|
const res = await rest("user_roles", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ user_id: idGiocatore, role: "admin" }),
|
|
});
|
|
assert.ok(!res.ok, `l'auto-promozione deve fallire, invece ha risposto ${res.status}`);
|
|
const dopo = await rest(`user_roles?user_id=eq.${idGiocatore}&select=role`, SERVIZIO);
|
|
assert.equal(((await dopo.json()) as unknown[]).length, 0, "nessun ruolo assegnato");
|
|
});
|
|
|
|
await prova("il giocatore non vede i ruoli degli altri", async () => {
|
|
const res = await rest("user_roles?select=user_id,role", tokenGiocatore);
|
|
assert.equal(res.status, 200);
|
|
const righe = (await res.json()) as Array<{ user_id: string }>;
|
|
assert.ok(
|
|
righe.every((r) => r.user_id === idGiocatore),
|
|
"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})`);
|
|
});
|
|
|
|
// --- Voto MVP: definitivo (M35), solo tra presenti e dopo 2 ore dall'inizio (M36) ----------
|
|
let contatoreMvp = 0;
|
|
/** Crea una partita (o altro tipo) con le presenze indicate; ritorna l'id. */
|
|
async function eventoMvp(
|
|
quando: { data: string; ora: string },
|
|
presenze: Record<string, string>,
|
|
tipo = "partita",
|
|
): Promise<string> {
|
|
const id = `${PREFISSO}-mvp-${++contatoreMvp}`;
|
|
const creato = await rest("eventi_app", tokenAdmin, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ id, tipo, titolo: "Voto MVP", luogo: "Palestra", ...quando }),
|
|
});
|
|
assert.equal(await righeToccate(creato), 1, "l'evento di prova si crea");
|
|
for (const [giocatore_id, stato] of Object.entries(presenze)) {
|
|
const r = await rest("risposte_presenze", SERVIZIO, {
|
|
method: "POST",
|
|
body: JSON.stringify({ evento_id: id, giocatore_id, stato }),
|
|
});
|
|
assert.ok(r.ok, `presenza di prova ${giocatore_id}: ${r.status}`);
|
|
}
|
|
return id;
|
|
}
|
|
const votaMvp = (token: string, match_id: string, votante_id: string, votato_id: string) =>
|
|
rest("mvp_voti", token, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ match_id, votante_id, votato_id, votato_nome: votato_id }),
|
|
});
|
|
const passata = romaTra(-3);
|
|
|
|
await prova("il voto MVP è uno solo, definitivo e firmato (M35)", async () => {
|
|
const EVENTO_MVP = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
|
const mioMvp = await rest("mvp_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_MVP,
|
|
votante_id: "g1",
|
|
votato_id: "g5",
|
|
votato_nome: "Cinque",
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(mioMvp), 1, "il proprio voto MVP si registra");
|
|
|
|
// Il voto MVP è definitivo (M35): né un secondo voto, né una modifica, né la cancellazione.
|
|
const secondo = await rest("mvp_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_MVP,
|
|
votante_id: "g1",
|
|
votato_id: "g6",
|
|
votato_nome: "Sei",
|
|
}),
|
|
});
|
|
assert.ok(
|
|
!secondo.ok,
|
|
`un secondo voto MVP sulla stessa partita è rifiutato (${secondo.status})`,
|
|
);
|
|
const modifica = await rest(
|
|
`mvp_voti?match_id=eq.${EVENTO_MVP}&votante_id=eq.g1`,
|
|
tokenGiocatore,
|
|
{
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ votato_id: "g6", votato_nome: "Sei" }),
|
|
},
|
|
);
|
|
assert.equal(await righeToccate(modifica), 0, "il proprio voto MVP non si modifica");
|
|
const cancella = await rest(
|
|
`mvp_voti?match_id=eq.${EVENTO_MVP}&votante_id=eq.g1`,
|
|
tokenGiocatore,
|
|
{
|
|
method: "DELETE",
|
|
headers: { Prefer: "return=representation" },
|
|
},
|
|
);
|
|
assert.equal(await righeToccate(cancella), 0, "il proprio voto MVP non si cancella");
|
|
const dopo = await rest(
|
|
`mvp_voti?match_id=eq.${EVENTO_MVP}&votante_id=eq.g1&select=votato_id`,
|
|
SERVIZIO,
|
|
);
|
|
assert.deepEqual(
|
|
await dopo.json(),
|
|
[{ votato_id: "g5" }],
|
|
"il voto è rimasto quello iniziale",
|
|
);
|
|
|
|
const mvp = await rest("mvp_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_MVP,
|
|
votante_id: "g5",
|
|
votato_id: "g1",
|
|
votato_nome: "Uno",
|
|
}),
|
|
});
|
|
assert.ok(!mvp.ok, `vale anche per l'MVP (${mvp.status})`);
|
|
});
|
|
|
|
await prova("il voto MVP vale solo tra presenti (M36)", async () => {
|
|
const q = passata;
|
|
// Controllo positivo: presente e in ritardo possono votare e farsi votare.
|
|
const ok = await eventoMvp(q, { g1: "presente", g5: "ritardo" });
|
|
assert.equal(await righeToccate(await votaMvp(tokenGiocatore, ok, "g1", "g5")), 1);
|
|
const okRitardo = await eventoMvp(q, { g1: "ritardo", g5: "presente" });
|
|
assert.equal(await righeToccate(await votaMvp(tokenGiocatore, okRitardo, "g1", "g5")), 1);
|
|
|
|
for (const [stato, nome] of [
|
|
["forse", "forse"],
|
|
["assente", "assente"],
|
|
["infortunato", "infortunato"],
|
|
[null, "senza risposta"],
|
|
] as const) {
|
|
const votatoNo = await eventoMvp(q, {
|
|
g1: "presente",
|
|
...(stato ? { g5: stato } : {}),
|
|
});
|
|
const r = await votaMvp(tokenGiocatore, votatoNo, "g1", "g5");
|
|
assert.ok(!r.ok, `un votato ${nome} non si può votare (${r.status})`);
|
|
|
|
const votanteNo = await eventoMvp(q, {
|
|
g5: "presente",
|
|
...(stato ? { g1: stato } : {}),
|
|
});
|
|
const r2 = await votaMvp(tokenGiocatore, votanteNo, "g1", "g5");
|
|
assert.ok(!r2.ok, `un votante ${nome} non può votare (${r2.status})`);
|
|
}
|
|
});
|
|
|
|
await prova(
|
|
"il voto MVP apre 2 ore dopo l'inizio e chiude 24 ore dopo (M36, M37)",
|
|
async () => {
|
|
const presenti = { g1: "presente", g5: "presente" };
|
|
const tre = await eventoMvp(romaTra(-3), presenti);
|
|
assert.equal(
|
|
await righeToccate(await votaMvp(tokenGiocatore, tre, "g1", "g5")),
|
|
1,
|
|
"3 ore dopo",
|
|
);
|
|
|
|
const una = await eventoMvp(romaTra(-1), presenti);
|
|
const rUna = await votaMvp(tokenGiocatore, una, "g1", "g5");
|
|
assert.ok(!rUna.ok, `1 ora dopo l'inizio: ancora chiuso (${rUna.status})`);
|
|
|
|
const venticinque = await eventoMvp(romaTra(-25), presenti);
|
|
assert.equal(
|
|
await righeToccate(await votaMvp(tokenGiocatore, venticinque, "g1", "g5")),
|
|
1,
|
|
"25 ore dopo l'inizio: ancora aperto (chiude a 26)",
|
|
);
|
|
const ventisette = await eventoMvp(romaTra(-27), presenti);
|
|
const rChiuso = await votaMvp(tokenGiocatore, ventisette, "g1", "g5");
|
|
assert.ok(!rChiuso.ok, `27 ore dopo l'inizio: chiuso (${rChiuso.status})`);
|
|
|
|
const futura = await eventoMvp(romaTra(48), presenti);
|
|
const rFutura = await votaMvp(tokenGiocatore, futura, "g1", "g5");
|
|
assert.ok(!rFutura.ok, `partita non ancora giocata (${rFutura.status})`);
|
|
|
|
// Un'ora vuota vale mezzanotte, come nell'app.
|
|
const senzaOra = await eventoMvp({ data: giornoSenzaOraAperto(), ora: "" }, presenti);
|
|
assert.equal(
|
|
await righeToccate(await votaMvp(tokenGiocatore, senzaOra, "g1", "g5")),
|
|
1,
|
|
"senza ora vale mezzanotte",
|
|
);
|
|
},
|
|
);
|
|
|
|
await prova("il voto MVP esiste solo per le partite (M36)", async () => {
|
|
const presenti = { g1: "presente", g5: "presente" };
|
|
for (const tipo of ["allenamento", "evento"]) {
|
|
const e = await eventoMvp(passata, presenti, tipo);
|
|
const r = await votaMvp(tokenGiocatore, e, "g1", "g5");
|
|
assert.ok(!r.ok, `nessun MVP per ${tipo} (${r.status})`);
|
|
}
|
|
});
|
|
|
|
await prova("l'admin può registrare un voto MVP anche fuori da queste regole", async () => {
|
|
const futura = await eventoMvp(romaTra(48), { g1: "assente" });
|
|
const r = await votaMvp(tokenAdmin, futura, "g1", "g5");
|
|
assert.equal(await righeToccate(r), 1, "l'admin corregge/inserisce senza vincoli di M36");
|
|
const chiusa = await eventoMvp(romaTra(-48), { g1: "assente" });
|
|
const rc = await votaMvp(tokenAdmin, chiusa, "g1", "g5");
|
|
assert.equal(await righeToccate(rc), 1, "l'admin può votare anche a votazione chiusa");
|
|
});
|
|
|
|
await prova("il voto MVP è segreto: nessuno legge chi ha votato chi (M37)", async () => {
|
|
const e = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
|
assert.equal(await righeToccate(await votaMvp(tokenGiocatore, e, "g1", "g5")), 1);
|
|
// Un secondo voto di un altro votante (inserito dall'admin) per avere due righe.
|
|
assert.equal(await righeToccate(await votaMvp(tokenAdmin, e, "g5", "g1")), 1);
|
|
|
|
const tabella = await rest(
|
|
`mvp_voti?match_id=eq.${e}&select=votante_id,votato_id`,
|
|
tokenGiocatore,
|
|
);
|
|
assert.deepEqual(
|
|
await tabella.json(),
|
|
[{ votante_id: "g1", votato_id: "g5" }],
|
|
"dalla tabella un giocatore legge solo il proprio voto",
|
|
);
|
|
|
|
const vista = await rest(`mvp_voti_anonimi?match_id=eq.${e}&select=*`, tokenGiocatore);
|
|
assert.ok(vista.ok, `la vista dei conteggi si legge (${vista.status})`);
|
|
const righe = (await vista.json()) as Record<string, string>[];
|
|
assert.deepEqual(
|
|
righe.map((r) => r["votato_id"]).sort(),
|
|
["g1", "g5"],
|
|
"ci sono tutti i voti",
|
|
);
|
|
assert.ok(
|
|
righe.every((r) => !("votante_id" in r)),
|
|
"ma la vista non dice chi li ha dati",
|
|
);
|
|
|
|
const anonimo = await rest(`mvp_voti_anonimi?match_id=eq.${e}`, ANON);
|
|
assert.ok(!anonimo.ok, `senza account non si legge nemmeno la vista (${anonimo.status})`);
|
|
const anonimoTabella = await rest(`mvp_voti?match_id=eq.${e}`, ANON);
|
|
const corpo = anonimoTabella.ok ? ((await anonimoTabella.json()) as unknown[]) : [];
|
|
assert.equal(corpo.length, 0, "né la tabella");
|
|
|
|
const admin = await rest(`mvp_voti?match_id=eq.${e}&select=votante_id`, tokenAdmin);
|
|
assert.equal(
|
|
((await admin.json()) as unknown[]).length,
|
|
2,
|
|
"l'admin vede i voti dalla tabella: gli servono per correggerli",
|
|
);
|
|
});
|
|
|
|
/** Nome in rosa di uno slot, come lo compone il trigger (nome + cognome, senza spazi in testa). */
|
|
async function nomeInRosa(id: string): Promise<string> {
|
|
const r = await rest(`giocatori_squadra?id=eq.${id}&select=nome,cognome`, SERVIZIO);
|
|
const [g] = (await r.json()) as { nome: string; cognome: string }[];
|
|
return `${g!.nome} ${g!.cognome}`.trim();
|
|
}
|
|
const votoNome = (token: string, match_id: string, votato_id: string, resto: object) =>
|
|
rest("mvp_voti", token, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ match_id, votante_id: "g1", votato_id, ...resto }),
|
|
});
|
|
const nomeRiga = async (res: Response) =>
|
|
((await res.json()) as { votato_nome: string }[])[0]?.votato_nome;
|
|
|
|
await prova("MVP: il nome del votato lo mette il database, non il client (M37)", async () => {
|
|
const g5 = await nomeInRosa("g5");
|
|
|
|
// Nome inventato: ignorato.
|
|
const falso = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
|
assert.equal(
|
|
await nomeRiga(
|
|
await votoNome(tokenGiocatore, falso, "g5", { votato_nome: "Nome Inventato" }),
|
|
),
|
|
g5,
|
|
);
|
|
const visto = await rest(
|
|
`mvp_voti_anonimi?match_id=eq.${falso}&select=votato_nome`,
|
|
tokenGiocatore,
|
|
);
|
|
assert.deepEqual(await visto.json(), [{ votato_nome: g5 }], "e così lo vede la squadra");
|
|
|
|
// Nome omesso (la colonna è NOT NULL): lo riempie il trigger, il voto passa.
|
|
const omesso = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
|
assert.equal(
|
|
await nomeRiga(await votoNome(tokenGiocatore, omesso, "g5", {})),
|
|
g5,
|
|
"nome omesso",
|
|
);
|
|
|
|
// Nome vuoto, con spazi, con un altro giocatore: sempre quello della rosa.
|
|
for (const [i, strano] of ["", " ", await nomeInRosa("g1"), "<b>x</b>"].entries()) {
|
|
const e = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
|
assert.equal(
|
|
await nomeRiga(await votoNome(tokenGiocatore, e, "g5", { votato_nome: strano })),
|
|
g5,
|
|
`caso ${i}`,
|
|
);
|
|
}
|
|
|
|
// Nome già corretto: resta lo stesso.
|
|
const giusto = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
|
assert.equal(
|
|
await nomeRiga(await votoNome(tokenGiocatore, giusto, "g5", { votato_nome: g5 })),
|
|
g5,
|
|
);
|
|
});
|
|
|
|
await prova(
|
|
"MVP: l'admin che corregge un voto non può far divergere il nome (M37)",
|
|
async () => {
|
|
const [g5, g6] = [await nomeInRosa("g5"), await nomeInRosa("g6")];
|
|
const e = await eventoMvp(passata, { g1: "presente", g5: "presente", g6: "presente" });
|
|
await votoNome(tokenGiocatore, e, "g5", {});
|
|
const filtro = `mvp_voti?match_id=eq.${e}&votante_id=eq.g1`;
|
|
|
|
// Cambia solo il nome: torna quello della rosa.
|
|
const soloNome = await rest(filtro, tokenAdmin, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ votato_nome: "Falso" }),
|
|
});
|
|
assert.equal(await nomeRiga(soloNome), g5, "nome riscritto da solo");
|
|
|
|
// Cambia il votato: il nome lo segue.
|
|
const altro = await rest(filtro, tokenAdmin, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ votato_id: "g6", votato_nome: "Falso" }),
|
|
});
|
|
assert.equal(await nomeRiga(altro), g6, "nome che segue il nuovo votato");
|
|
},
|
|
);
|
|
|
|
await prova("MVP: un id fuori rosa conserva il nome ricevuto (M37)", async () => {
|
|
// Solo la service role (come i test sui badge) può scrivere id che non sono in rosa.
|
|
const e = `${PREFISSO}-mvp-fuori-rosa`;
|
|
const r = await rest("mvp_voti", SERVIZIO, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: e,
|
|
votante_id: "xx1",
|
|
votato_id: "xx2",
|
|
votato_nome: "Ospite",
|
|
}),
|
|
});
|
|
assert.equal(await nomeRiga(r), "Ospite", "niente da sostituire: resta il nome ricevuto");
|
|
});
|
|
|
|
await prova("badge social: il nome del votato lo mette il database (M38)", async () => {
|
|
const g5 = await nomeInRosa("g5");
|
|
const e = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
|
const base = { match_id: e, categoria: "sorriso", votante_id: "g1", votato_id: "g5" };
|
|
const post = (resto: object) =>
|
|
rest("badge_social_voti?on_conflict=match_id,categoria,votante_id", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({ ...base, ...resto }),
|
|
});
|
|
assert.equal(
|
|
await nomeRiga(await post({ votato_nome: "Nome Inventato" })),
|
|
g5,
|
|
"nome inventato",
|
|
);
|
|
// Stesso voto rinviato con un altro nome (upsert): il nome resta quello della rosa.
|
|
assert.equal(
|
|
await nomeRiga(await post({ votato_nome: "Un Altro" })),
|
|
g5,
|
|
"upsert con altro nome",
|
|
);
|
|
const omesso = await rest("badge_social_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ ...base, categoria: "meme" }),
|
|
});
|
|
assert.equal(await nomeRiga(omesso), g5, "nome omesso");
|
|
});
|
|
|
|
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})`);
|
|
});
|
|
|
|
await prova("anche i badge social si firmano con il proprio nome", async () => {
|
|
const mio = await rest("badge_social_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO,
|
|
categoria: "sorriso",
|
|
votante_id: "g1",
|
|
votato_id: "g5",
|
|
votato_nome: "Cinque",
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(mio), 1, "il proprio voto social si registra");
|
|
|
|
const falso = await rest("badge_social_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO,
|
|
categoria: "sorriso",
|
|
votante_id: "g5",
|
|
votato_id: "g1",
|
|
votato_nome: "Uno",
|
|
}),
|
|
});
|
|
assert.ok(!falso.ok, `non si vota a nome di un altro (${falso.status})`);
|
|
});
|
|
|
|
// L'altra metà di M11: le policy `Gli admin gestiscono tutti/e …`. Senza queste
|
|
// l'amministratore non potrebbe correggere una risposta sbagliata né ripulire i voti
|
|
// di una partita, e la rotta /eventi sarebbe monca.
|
|
await prova("l'amministratore corregge i dati degli altri", async () => {
|
|
const presenza = await rest("risposte_presenze", tokenAdmin, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: "g5", stato: "assente" }),
|
|
});
|
|
assert.equal(await righeToccate(presenza), 1, "l'admin risponde anche per un altro");
|
|
|
|
const cacca = await rest("cacche_partita", tokenAdmin, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: "g5", quantita: 1 }),
|
|
});
|
|
assert.equal(await righeToccate(cacca), 1, "vale anche per le cacche");
|
|
|
|
const pagella = await rest(
|
|
`pagelle_voti?match_id=eq.${EVENTO}&votante_id=eq.g1`,
|
|
tokenAdmin,
|
|
{ method: "DELETE", headers: { Prefer: "return=representation" } },
|
|
);
|
|
assert.equal(await righeToccate(pagella), 1, "e per cancellare il voto di un altro");
|
|
});
|
|
|
|
// M13: le tabelle di voto controllano anche a database chi può votare chi, non solo
|
|
// chi firma il voto. Prima di M13 un convocato poteva votare/essere votato in un
|
|
// evento a cui non aveva partecipato, e un voto pagella restava possibile anche a
|
|
// `pagelle_chiuse` — entrambi filtri solo applicativi (segnalati in `badge.md`).
|
|
// Nota: `tokenAdmin` non va usato per queste prove, la policy admin di M11 non ha il
|
|
// controllo sui convocati (l'admin corregge anche dati fuori convocazione di
|
|
// proposito) e farebbe passare tutto a prescindere, senza provare niente sulla nuova
|
|
// policy. Si usa solo `tokenGiocatore` (g1), un votante non-admin vero.
|
|
const EVENTO_SENZA_G1 = `${PREFISSO}-evento-senza-g1`;
|
|
const EVENTO_CON_G1 = `${PREFISSO}-evento-con-g1`;
|
|
const EVENTO_CHIUSO = `${PREFISSO}-evento-chiuso`;
|
|
|
|
await prova("un votante non convocato non può votare", async () => {
|
|
const creato = await rest("eventi_app", tokenAdmin, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
id: EVENTO_SENZA_G1,
|
|
tipo: "partita",
|
|
titolo: "Partita senza g1 tra i convocati",
|
|
data: "2026-01-02",
|
|
ora: "20:00",
|
|
luogo: "Palestra",
|
|
convocati: ["g2", "g5"],
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(creato), 1, "l'evento con convocati si crea");
|
|
|
|
const votanteEscluso = await rest("pagelle_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_SENZA_G1,
|
|
votante_id: "g1",
|
|
votato_id: "g2",
|
|
voto: 7,
|
|
}),
|
|
});
|
|
assert.ok(!votanteEscluso.ok, `g1 non era convocato, non vota (${votanteEscluso.status})`);
|
|
});
|
|
|
|
await prova("un votante convocato non può votare chi non lo era", async () => {
|
|
const creato = await rest("eventi_app", tokenAdmin, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
id: EVENTO_CON_G1,
|
|
tipo: "partita",
|
|
titolo: "Partita con g1 convocato, g2 no",
|
|
data: romaTra(-3).data,
|
|
ora: romaTra(-3).ora,
|
|
luogo: "Palestra",
|
|
convocati: ["g1", "g5"],
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(creato), 1, "l'evento con convocati si crea");
|
|
|
|
const votatoEscluso = await rest("badge_social_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_CON_G1,
|
|
categoria: "cuore",
|
|
votante_id: "g1",
|
|
votato_id: "g2",
|
|
votato_nome: "Due",
|
|
}),
|
|
});
|
|
assert.ok(
|
|
!votatoEscluso.ok,
|
|
`g2 non era convocato, non è votabile (${votatoEscluso.status})`,
|
|
);
|
|
|
|
// Controllo positivo sullo stesso evento: g1 è convocato e vota g5, anche lui
|
|
// convocato — senza questo, il test sopra potrebbe fallire per un altro motivo
|
|
// (es. un evento inesistente) e sembrare comunque corretto.
|
|
// M36: il voto MVP richiede che votante e votato siano presenti.
|
|
for (const g of ["g1", "g5"]) {
|
|
await rest("risposte_presenze", SERVIZIO, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates" },
|
|
body: JSON.stringify({ evento_id: EVENTO_CON_G1, giocatore_id: g, stato: "presente" }),
|
|
});
|
|
}
|
|
const votoValido = await rest("mvp_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_CON_G1,
|
|
votante_id: "g1",
|
|
votato_id: "g5",
|
|
votato_nome: "Cinque",
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(votoValido), 1, "votante e votato convocati: il voto passa");
|
|
});
|
|
|
|
await prova("pagelle_chiuse blocca anche a database, non solo in UI", async () => {
|
|
const creato = await rest("eventi_app", tokenAdmin, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
id: EVENTO_CHIUSO,
|
|
tipo: "partita",
|
|
titolo: "Partita con pagelle chiuse",
|
|
data: romaTra(-3).data,
|
|
ora: romaTra(-3).ora,
|
|
luogo: "Palestra",
|
|
pagelle_chiuse: true,
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(creato), 1, "l'evento con pagelle chiuse si crea");
|
|
|
|
const pagellaFuoriTempo = await rest("pagelle_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_CHIUSO,
|
|
votante_id: "g1",
|
|
votato_id: "g5",
|
|
voto: 7,
|
|
}),
|
|
});
|
|
assert.ok(
|
|
!pagellaFuoriTempo.ok,
|
|
`pagelle chiuse: voto rifiutato (${pagellaFuoriTempo.status})`,
|
|
);
|
|
|
|
// Il flag riguarda solo le pagelle: MVP e badge social non hanno un concetto di
|
|
// "chiusura" (mvp.md lo segnala esplicitamente come limite noto), quindi restano
|
|
// votabili sullo stesso evento.
|
|
// M36: il voto MVP richiede che votante e votato siano presenti.
|
|
for (const g of ["g1", "g5"]) {
|
|
await rest("risposte_presenze", SERVIZIO, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates" },
|
|
body: JSON.stringify({ evento_id: EVENTO_CHIUSO, giocatore_id: g, stato: "presente" }),
|
|
});
|
|
}
|
|
const mvpAncoraAperto = await rest("mvp_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
match_id: EVENTO_CHIUSO,
|
|
votante_id: "g1",
|
|
votato_id: "g5",
|
|
votato_nome: "Cinque",
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(mvpAncoraAperto), 1, "l'MVP non ha un flag di chiusura");
|
|
});
|
|
|
|
// Il terzo gruppo di DD-023: tabelle lasciate aperte **di proposito**, perché
|
|
// nell'interfaccia non hanno nessun gate — il turno palloni se lo passa chiunque, e lo
|
|
// Scout Live lo apre chiunque, con il solo lock di sessione a tenere l'ordine.
|
|
// Questi casi non dicono che sono sicure: dicono che sono aperte per scelta. Se un
|
|
// giorno una di loro prende un gate nell'interfaccia, le policy devono seguirlo e
|
|
// questi test vanno cambiati insieme.
|
|
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");
|
|
});
|
|
|
|
await prova("lo Scout Live resta aperto a chiunque sia autenticato", async () => {
|
|
const sessione = await rest("scout_sessioni", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({
|
|
evento_id: EVENTO,
|
|
giocatore_id: "g5",
|
|
giocatore_nome: "Cinque",
|
|
aggiornato_il: new Date().toISOString(),
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(sessione), 1, "il lock lo prende chiunque");
|
|
|
|
const stato = await rest("scout_live", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({ evento_id: EVENTO, stato: { set: 1 } }),
|
|
});
|
|
assert.equal(await righeToccate(stato), 1, "e lo stato in corso lo scrive chiunque");
|
|
|
|
const archivio = await rest("scout_partite", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
id: `${PREFISSO}-match`,
|
|
evento_id: EVENTO,
|
|
data: "2026-01-01",
|
|
avversario: "Prova",
|
|
casa: true,
|
|
set_nostri: 3,
|
|
set_loro: 0,
|
|
parziali: [],
|
|
azioni: [],
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(archivio), 1, "come l'archivio di fine partita");
|
|
|
|
const cancella = await rest(`scout_partite?id=eq.${PREFISSO}-match`, tokenGiocatore, {
|
|
method: "DELETE",
|
|
headers: { Prefer: "return=representation" },
|
|
});
|
|
assert.equal(await righeToccate(cancella), 1, "e chiunque può anche cancellarlo");
|
|
});
|
|
|
|
// Le iscrizioni push non passano dalla RLS per identificare il dispositivo: la chiave è
|
|
// l'endpoint, che il browser conosce solo per sé. Restano scrivibili da chiunque sia
|
|
// autenticato, ed è il motivo per cui `push_subscriptions` non contiene dati personali
|
|
// oltre all'endpoint e alle sue chiavi.
|
|
await prova("l'iscrizione alle notifiche la registra qualsiasi autenticato", async () => {
|
|
const endpoint = `https://esempio.test/${PREFISSO}-push`;
|
|
const res = await rest("push_subscriptions", tokenGiocatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
|
body: JSON.stringify({
|
|
giocatore_id: "g1",
|
|
endpoint,
|
|
p256dh: "chiave-di-prova",
|
|
auth: "auth-di-prova",
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(res), 1, "il dispositivo si registra da solo");
|
|
|
|
const via = await rest(
|
|
`push_subscriptions?endpoint=eq.${encodeURIComponent(endpoint)}`,
|
|
tokenGiocatore,
|
|
{
|
|
method: "DELETE",
|
|
headers: { Prefer: "return=representation" },
|
|
},
|
|
);
|
|
assert.equal(await righeToccate(via), 1, "e si cancella quando le notifiche si spengono");
|
|
});
|
|
} finally {
|
|
// Ripristino: prima le righe create (la service role passa sopra alle policy di M11),
|
|
// 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", "badge_social_voti"]) {
|
|
await rest(`${tabella}?match_id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
|
}
|
|
for (const tabella of ["scout_sessioni", "scout_live"]) {
|
|
await rest(`${tabella}?evento_id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
|
}
|
|
await rest(`scout_partite?id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
|
await rest(`push_subscriptions?endpoint=like.*${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
|
await rest(`eventi_app?id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
|
|
|
for (const id of ["g1", "g2"]) {
|
|
if (tokenAdmin) {
|
|
await rest(`giocatori_squadra?id=eq.${id}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ auth_user_id: null }),
|
|
});
|
|
}
|
|
}
|
|
await rest("profili_giocatore?giocatore_id=eq.g1", SERVIZIO, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ telefono: telefonoOriginale }),
|
|
});
|
|
for (const id of idUtenti) {
|
|
await rest(`user_roles?user_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
|
await eliminaUtente(id);
|
|
}
|
|
riepilogo("permessi");
|
|
}
|
|
}
|