/** * 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 { 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 => { 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 { 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 { 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})`); const mioMvp = await rest("mvp_voti", tokenGiocatore, { method: "POST", headers: { Prefer: "resolution=merge-duplicates,return=representation" }, body: JSON.stringify({ match_id: EVENTO, votante_id: "g1", votato_id: "g5", votato_nome: "Cinque", }), }); assert.equal(await righeToccate(mioMvp), 1, "il proprio voto MVP si registra"); 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})`); }); 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: "2026-01-02", ora: "20:00", 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. const votoValido = await rest("mvp_voti", tokenGiocatore, { method: "POST", headers: { Prefer: "resolution=merge-duplicates,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: "2026-01-03", ora: "20:00", 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. const mvpAncoraAperto = await rest("mvp_voti", tokenGiocatore, { method: "POST", headers: { Prefer: "resolution=merge-duplicates,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"); } }