Copre la cache delle presenze, le tabelle aperte e le letture server.
Chiude i tre buchi di copertura rimasti dalla rilettura della documentazione: comportamenti che la doc descrive come regole ma che nessun test verificava. - conRisposta() esce da useSalvaPresenza e diventa una funzione pura in presenze.ts. La mutation non rilegge dopo la scrittura, quindi la cache deve imitare il database: l'istante si scrive solo se manca (??=) e il ritiro della risposta lo cancella. Erano due dettagli che serie-presenze.md chiede di preservare e che un refactor poteva perdere in silenzio, falsando la serie "Conferme 24h" fino al refresh successivo. - permessi.test.ts copre ora anche il terzo gruppo di DD-023, le tabelle lasciate aperte di proposito: scout_sessioni, scout_live, scout_partite e push_subscriptions. Non dicono che sono sicure, dicono che sono aperte per scelta: se una prende un gate nell'interfaccia, le policy devono seguirlo e questi casi vanno cambiati con loro. - lettori-server.test.ts esegue leggiEventi() e leggiGiocatoriSquadra() sul database locale, mettendo le credenziali in process.env prima della prima chiamata perché supabaseAdmin nasce pigramente. Nessuna delle due controlla l'errore di PostgREST: una colonna rinominata darebbe zero righe, zero destinatari e nessuna push, senza che niente segnali il problema. Scrivendolo è emerso che eventi_app.note è NOT NULL, mentre RigaEvento lo dichiara nullable. PROJECT_STATE: m12_niente_autovoto è applicata in produzione dal 06/09/2026, verificata con `npx supabase migration list` (23 migration, local = remote). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -36,8 +36,8 @@ reali (M9).
|
|||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
- Schema v1.0 e migration da M1 a M11 applicate al nuovo Supabase; `m12_niente_autovoto`
|
- Schema v1.0 e migration da M1 a M12 applicate al nuovo Supabase (`m12_niente_autovoto`
|
||||||
applicata in locale, **da applicare in produzione** (`npx supabase db push`)
|
in produzione dal 06/09/2026, verificata con `npx supabase migration list`)
|
||||||
- `public.giocatori_squadra`: rosa iniziale di 17 giocatori (migration `m5_email_giocatori_squadra`)
|
- `public.giocatori_squadra`: rosa iniziale di 17 giocatori (migration `m5_email_giocatori_squadra`)
|
||||||
più quelli aggiunti da `/admin` a stagione in corso; da settembre 2026 tutti i giocatori
|
più quelli aggiunti da `/admin` a stagione in corso; da settembre 2026 tutti i giocatori
|
||||||
attivi hanno l'email registrata (colonna `email`, DD-018), impostabile da `/admin` senza
|
attivi hanno l'email registrata (colonna `email`, DD-018), impostabile da `/admin` senza
|
||||||
|
|||||||
+33
-17
@@ -152,6 +152,38 @@ export function usePresenzeEvento(eventoId: string) {
|
|||||||
return { ...resto, risposte: presenze[eventoId] ?? {} };
|
return { ...resto, risposte: presenze[eventoId] ?? {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La cache delle presenze dopo una risposta salvata, senza rileggere il database.
|
||||||
|
*
|
||||||
|
* Due dettagli non sono cosmetici e non vanno persi (vedi `docs/modules/serie-presenze.md`):
|
||||||
|
*
|
||||||
|
* - l'istante si scrive **solo se manca** (`??=`), come fa il database, dove `risposto_il`
|
||||||
|
* non viene inviato sull'upsert e un trigger lo congela: è la prima risposta, non l'ultima,
|
||||||
|
* e un ripensamento non deve far ripartire il cronometro della serie "Conferme 24h";
|
||||||
|
* - cancellare la risposta (`stato: null`) elimina **anche** l'istante, così se il giocatore
|
||||||
|
* risponde di nuovo il cronometro riparte davvero — ha ritirato la risposta.
|
||||||
|
*/
|
||||||
|
export function conRisposta(
|
||||||
|
prec: LetturaPresenze | undefined,
|
||||||
|
input: { eventoId: string; giocatoreId: string; stato: Stato | null },
|
||||||
|
adesso: string = new Date().toISOString(),
|
||||||
|
): LetturaPresenze {
|
||||||
|
const presenze: MappaPresenze = { ...(prec?.presenze ?? {}) };
|
||||||
|
const tempi: MappaTempiRisposta = { ...(prec?.tempi ?? {}) };
|
||||||
|
const stati = { ...(presenze[input.eventoId] ?? {}) };
|
||||||
|
const istanti = { ...(tempi[input.eventoId] ?? {}) };
|
||||||
|
if (input.stato === null) {
|
||||||
|
delete stati[input.giocatoreId];
|
||||||
|
delete istanti[input.giocatoreId];
|
||||||
|
} else {
|
||||||
|
stati[input.giocatoreId] = input.stato;
|
||||||
|
istanti[input.giocatoreId] ??= adesso;
|
||||||
|
}
|
||||||
|
presenze[input.eventoId] = stati;
|
||||||
|
tempi[input.eventoId] = istanti;
|
||||||
|
return { presenze, tempi };
|
||||||
|
}
|
||||||
|
|
||||||
export function useSalvaPresenza() {
|
export function useSalvaPresenza() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -179,23 +211,7 @@ export function useSalvaPresenza() {
|
|||||||
},
|
},
|
||||||
// Scrittura unica + aggiornamento cache locale, nessuna rilettura.
|
// Scrittura unica + aggiornamento cache locale, nessuna rilettura.
|
||||||
onSuccess: (input) => {
|
onSuccess: (input) => {
|
||||||
queryClient.setQueryData<LetturaPresenze>(PRESENZE_KEY, (prec) => {
|
queryClient.setQueryData<LetturaPresenze>(PRESENZE_KEY, (prec) => conRisposta(prec, input));
|
||||||
const presenze: MappaPresenze = { ...(prec?.presenze ?? {}) };
|
|
||||||
const tempi: MappaTempiRisposta = { ...(prec?.tempi ?? {}) };
|
|
||||||
const stati = { ...(presenze[input.eventoId] ?? {}) };
|
|
||||||
const istanti = { ...(tempi[input.eventoId] ?? {}) };
|
|
||||||
if (input.stato === null) {
|
|
||||||
delete stati[input.giocatoreId];
|
|
||||||
delete istanti[input.giocatoreId];
|
|
||||||
} else {
|
|
||||||
stati[input.giocatoreId] = input.stato;
|
|
||||||
// Come a database: l'istante è quello della prima risposta, non dei ripensamenti.
|
|
||||||
istanti[input.giocatoreId] ??= new Date().toISOString();
|
|
||||||
}
|
|
||||||
presenze[input.eventoId] = stati;
|
|
||||||
tempi[input.eventoId] = istanti;
|
|
||||||
return { presenze, tempi };
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -24,9 +24,9 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
|
|||||||
## Struttura
|
## Struttura
|
||||||
|
|
||||||
| Cartella | Cosa verifica | Serve rete? |
|
| Cartella | Cosa verifica | Serve rete? |
|
||||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||||
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route) | No |
|
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route) | No |
|
||||||
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11 e le deroghe dell'amministratore (`permessi`), accesso alle route di notifica (`permessi-route`) e semantica degli upsert (`scritture`) sul database locale | Sì |
|
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`) e le letture lato server delle route push (`lettori-server`) sul database locale | Sì |
|
||||||
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
||||||
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
||||||
|
|
||||||
@@ -44,7 +44,8 @@ npx supabase stop # spegne tutto
|
|||||||
|
|
||||||
bun test/integration/permessi.test.ts # permessi per ruolo sulle tabelle
|
bun test/integration/permessi.test.ts # permessi per ruolo sulle tabelle
|
||||||
bun test/integration/permessi-route.test.ts # chi può far partire le notifiche
|
bun test/integration/permessi-route.test.ts # chi può far partire le notifiche
|
||||||
bun test/integration/scritture.test.ts # semantica degli upsert
|
bun test/integration/scritture.test.ts # semantica degli upsert e vincoli
|
||||||
|
bun test/integration/lettori-server.test.ts # le letture server delle route push
|
||||||
```
|
```
|
||||||
|
|
||||||
`permessi-route` avvia il server di sviluppo **puntato al database locale** invece che al
|
`permessi-route` avvia il server di sviluppo **puntato al database locale** invece che al
|
||||||
@@ -55,6 +56,12 @@ Il primo `start` scarica le immagini (qualche minuto), i successivi partono in
|
|||||||
una decina di secondi. Le mail finiscono in Mailpit (http://127.0.0.1:54324),
|
una decina di secondi. Le mail finiscono in Mailpit (http://127.0.0.1:54324),
|
||||||
non escono dalla macchina.
|
non escono dalla macchina.
|
||||||
|
|
||||||
|
`lettori-server` fa la stessa cosa in un altro modo: mette le credenziali locali in
|
||||||
|
`process.env` prima della prima chiamata, perché `client.server.ts` costruisce
|
||||||
|
`supabaseAdmin` pigramente leggendo l'ambiente. Serve a eseguire per davvero le query di
|
||||||
|
`leggiEventi()`/`leggiGiocatoriSquadra()`: nessuna delle due controlla l'errore di PostgREST,
|
||||||
|
quindi una colonna rinominata darebbe zero righe e zero destinatari, in silenzio.
|
||||||
|
|
||||||
I test che scrivono **non leggono `.env`**: prendono URL e chiavi da
|
I test che scrivono **non leggono `.env`**: prendono URL e chiavi da
|
||||||
`supabase status` (helper `test/helpers/locale.ts`) e si fermano se l'URL non è
|
`supabase status` (helper `test/helpers/locale.ts`) e si fermano se l'URL non è
|
||||||
`127.0.0.1`. È una cintura di sicurezza, non una comodità: così un `.env`
|
`127.0.0.1`. È una cintura di sicurezza, non una comodità: così un `.env`
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* I due lettori lato server contro il database locale:
|
||||||
|
* `bun test/integration/lettori-server.test.ts`.
|
||||||
|
*
|
||||||
|
* `leggiEventi()` e `leggiGiocatoriSquadra()` sono le uniche letture delle tre route che
|
||||||
|
* mandano push: se una colonna elencata nelle costanti `COLONNE`/`COLONNE_SQUADRA` sparisse
|
||||||
|
* o cambiasse nome, PostgREST risponderebbe con un errore, `data` sarebbe `null` e le route
|
||||||
|
* manderebbero le notifiche a zero destinatari — senza che niente si accorga di niente,
|
||||||
|
* perché nessuna delle due controlla l'errore. Qui si esegue la query vera e si verifica che
|
||||||
|
* le righe arrivino mappate.
|
||||||
|
*
|
||||||
|
* Gira solo sullo stack locale (`npx supabase start`): le credenziali arrivano da
|
||||||
|
* `supabase status` e vengono messe in `process.env` **prima** della prima chiamata, perché
|
||||||
|
* `client.server.ts` costruisce il client pigramente leggendo l'ambiente. Così il test non
|
||||||
|
* può parlare con il progetto cloud nemmeno per sbaglio.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { statoLocale } from "../helpers/locale";
|
||||||
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
|
const locale = statoLocale();
|
||||||
|
|
||||||
|
if (!locale) {
|
||||||
|
salta("lettori server", "stack locale non attivo (npx supabase start)");
|
||||||
|
riepilogo("lettori server");
|
||||||
|
} else {
|
||||||
|
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||||
|
console.log(`lettori server su ${URL_BASE}`);
|
||||||
|
|
||||||
|
// Prima di qualsiasi import dei moduli server: è da qui che nasce `supabaseAdmin`.
|
||||||
|
process.env["SUPABASE_URL"] = URL_BASE;
|
||||||
|
process.env["SUPABASE_SERVICE_ROLE_KEY"] = SERVIZIO;
|
||||||
|
|
||||||
|
const { leggiEventi } = await import("@/lib/eventi.server");
|
||||||
|
const { leggiGiocatoriSquadra } = await import("@/lib/giocatori-squadra.server");
|
||||||
|
|
||||||
|
const PREFISSO = "test-lettori";
|
||||||
|
const EVENTO = `${PREFISSO}-e1`;
|
||||||
|
|
||||||
|
const rest = (percorso: string, init?: RequestInit) =>
|
||||||
|
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
apikey: SERVIZIO,
|
||||||
|
Authorization: `Bearer ${SERVIZIO}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
...(init?.headers ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const inserito = await rest("eventi_app", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: EVENTO,
|
||||||
|
tipo: "partita",
|
||||||
|
titolo: "Lettori server",
|
||||||
|
luogo: "Palestra",
|
||||||
|
data: "2026-01-02",
|
||||||
|
ora: "21:00",
|
||||||
|
note: "",
|
||||||
|
convocati: ["g1", "g2"],
|
||||||
|
campionato: true,
|
||||||
|
casa: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!inserito.ok) throw new Error(`preparazione fallita: ${await inserito.text()}`);
|
||||||
|
|
||||||
|
await prova("leggiEventi() mappa le colonne che le route si aspettano", async () => {
|
||||||
|
const eventi = await leggiEventi();
|
||||||
|
const evento = eventi.find((e) => e.id === EVENTO);
|
||||||
|
assert.ok(evento, "l'evento appena inserito arriva fino al chiamante");
|
||||||
|
assert.equal(evento.titolo, "Lettori server");
|
||||||
|
assert.equal(evento.tipo, "partita");
|
||||||
|
assert.equal(evento.ora, "21:00");
|
||||||
|
assert.equal(evento.casa, false, "la trasferta resta una trasferta");
|
||||||
|
assert.equal(evento.campionato, true);
|
||||||
|
assert.deepEqual(evento.convocati, ["g1", "g2"], "i convocati servono al sollecito");
|
||||||
|
assert.equal(evento.note, "", "le note vuote restano stringa vuota");
|
||||||
|
assert.equal(evento.pagelleChiuse, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("leggiGiocatoriSquadra() torna la rosa con i campi usati dalle route", async () => {
|
||||||
|
const squadra = await leggiGiocatoriSquadra();
|
||||||
|
assert.ok(squadra.length > 0, "la rosa del seed non è vuota");
|
||||||
|
const g1 = squadra.find((g) => g.id === "g1");
|
||||||
|
assert.ok(g1, "g1 esiste nel seed locale");
|
||||||
|
assert.equal(typeof g1.nome, "string");
|
||||||
|
assert.equal(typeof g1.cognome, "string");
|
||||||
|
assert.equal(typeof g1.numero, "number");
|
||||||
|
assert.equal(typeof g1.attivo, "boolean", "`attivo` filtra i destinatari delle push");
|
||||||
|
// Le colonne di M8: fanno parte di COLONNE_SQUADRA, quindi se mancassero la query
|
||||||
|
// fallirebbe per tutti, non solo per la dashboard tesseramenti.
|
||||||
|
assert.ok("numeroTessera" in g1 && "dataTessera" in g1, "le colonne di M8 sono lette");
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("l'ordine è quello dichiarato: cognome, poi nome", async () => {
|
||||||
|
const squadra = await leggiGiocatoriSquadra();
|
||||||
|
const chiavi = squadra.map((g) => `${g.cognome} ${g.nome}`);
|
||||||
|
assert.deepEqual(
|
||||||
|
chiavi,
|
||||||
|
[...chiavi].sort((a, b) => a.localeCompare(b)),
|
||||||
|
"gli elenchi mostrati alla squadra dipendono da questo ordinamento",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||||
|
riepilogo("lettori server");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -394,8 +394,12 @@ if (!locale) {
|
|||||||
assert.equal(await righeToccate(pagella), 1, "e per cancellare il voto di un altro");
|
assert.equal(await righeToccate(pagella), 1, "e per cancellare il voto di un altro");
|
||||||
});
|
});
|
||||||
|
|
||||||
// I turni palloni restano aperti di proposito: nell'interfaccia il turno se lo passa
|
// Il terzo gruppo di DD-023: tabelle lasciate aperte **di proposito**, perché
|
||||||
// chiunque, senza gate. Se un giorno arriva il gate, questo test va cambiato.
|
// 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 () => {
|
await prova("il turno palloni resta assegnabile da chiunque sia autenticato", async () => {
|
||||||
const res = await rest("turni_palloni", tokenGiocatore, {
|
const res = await rest("turni_palloni", tokenGiocatore, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -404,6 +408,79 @@ if (!locale) {
|
|||||||
});
|
});
|
||||||
assert.equal(await righeToccate(res), 1, "DD-023 lascia questa tabella invariata");
|
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 {
|
} finally {
|
||||||
// Ripristino: prima le righe create (la service role passa sopra alle policy di M11),
|
// 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
|
// poi gli slot (serve il JWT admin, il trigger rifiuta la service key), il telefono e
|
||||||
@@ -414,6 +491,11 @@ if (!locale) {
|
|||||||
for (const tabella of ["pagelle_voti", "mvp_voti", "badge_social_voti"]) {
|
for (const tabella of ["pagelle_voti", "mvp_voti", "badge_social_voti"]) {
|
||||||
await rest(`${tabella}?match_id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
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" });
|
await rest(`eventi_app?id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
||||||
|
|
||||||
for (const id of ["g1", "g2"]) {
|
for (const id of ["g1", "g2"]) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import type { Evento } from "@/lib/eventi";
|
import type { Evento } from "@/lib/eventi";
|
||||||
import {
|
import {
|
||||||
|
conRisposta,
|
||||||
contaPresenzeGiocatore,
|
contaPresenzeGiocatore,
|
||||||
destinatariSollecito,
|
destinatariSollecito,
|
||||||
serieConferme,
|
serieConferme,
|
||||||
@@ -153,4 +154,60 @@ assert.equal(
|
|||||||
);
|
);
|
||||||
assert.equal(serieConferme("g2", conConvocazione, tempi, OGGI), 0, "chi non risponde è a zero");
|
assert.equal(serieConferme("g2", conConvocazione, tempi, OGGI), 0, "chi non risponde è a zero");
|
||||||
|
|
||||||
|
// --- cache locale dopo una risposta: il cronometro non riparte ----------------
|
||||||
|
// Stessa regola del database: `risposto_il` è la PRIMA risposta e un trigger la congela.
|
||||||
|
// Qui la cache deve imitarla, altrimenti la serie "Conferme 24h" mente fino al refresh.
|
||||||
|
const PRIMA = "2026-08-01T10:00:00Z";
|
||||||
|
const POI = "2026-08-08T10:00:00Z";
|
||||||
|
|
||||||
|
const dopoPrimaRisposta = conRisposta(
|
||||||
|
undefined,
|
||||||
|
{ eventoId: "e1", giocatoreId: "g1", stato: "presente" },
|
||||||
|
PRIMA,
|
||||||
|
);
|
||||||
|
assert.deepEqual(dopoPrimaRisposta, {
|
||||||
|
presenze: { e1: { g1: "presente" } },
|
||||||
|
tempi: { e1: { g1: PRIMA } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const dopoRipensamento = conRisposta(
|
||||||
|
dopoPrimaRisposta,
|
||||||
|
{ eventoId: "e1", giocatoreId: "g1", stato: "assente" },
|
||||||
|
POI,
|
||||||
|
);
|
||||||
|
assert.equal(dopoRipensamento.presenze["e1"]?.["g1"], "assente", "vale l'ultima risposta");
|
||||||
|
assert.equal(
|
||||||
|
dopoRipensamento.tempi["e1"]?.["g1"],
|
||||||
|
PRIMA,
|
||||||
|
"l'istante resta quello della prima risposta: il ripensamento non fa ripartire il cronometro",
|
||||||
|
);
|
||||||
|
|
||||||
|
const dopoRitiro = conRisposta(
|
||||||
|
dopoRipensamento,
|
||||||
|
{ eventoId: "e1", giocatoreId: "g1", stato: null },
|
||||||
|
POI,
|
||||||
|
);
|
||||||
|
assert.deepEqual(dopoRitiro.presenze["e1"], {}, "ritirare la risposta la toglie");
|
||||||
|
assert.deepEqual(
|
||||||
|
dopoRitiro.tempi["e1"],
|
||||||
|
{},
|
||||||
|
"e toglie anche l'istante: chi risponde di nuovo riparte da capo",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
conRisposta(dopoRitiro, { eventoId: "e1", giocatoreId: "g1", stato: "presente" }, POI).tempi[
|
||||||
|
"e1"
|
||||||
|
]?.["g1"],
|
||||||
|
POI,
|
||||||
|
"dopo un ritiro il cronometro riparte davvero",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Gli altri giocatori e gli altri eventi non vengono toccati.
|
||||||
|
const conAltri2 = conRisposta(
|
||||||
|
conRisposta(undefined, { eventoId: "e1", giocatoreId: "g1", stato: "presente" }, PRIMA),
|
||||||
|
{ eventoId: "e2", giocatoreId: "g2", stato: "forse" },
|
||||||
|
POI,
|
||||||
|
);
|
||||||
|
assert.equal(conAltri2.presenze["e1"]?.["g1"], "presente", "l'altro evento resta in cache");
|
||||||
|
assert.equal(conAltri2.tempi["e2"]?.["g2"], POI);
|
||||||
|
|
||||||
console.log("presenze: ok");
|
console.log("presenze: ok");
|
||||||
|
|||||||
Reference in New Issue
Block a user