2026-09-29 14:19:54 +02:00
|
|
|
/**
|
|
|
|
|
* Promemoria evento che non ricompaiono (M23, DD-037): `bun test/integration/promemoria-eventi.test.ts`.
|
|
|
|
|
*
|
|
|
|
|
* Prima di M23 l'unica traccia che un promemoria fosse già stato generato era la riga in
|
|
|
|
|
* `notifiche_utente`: eliminarla dal centro notifiche faceva rigenerare il promemoria (e
|
|
|
|
|
* riaccodare la mail) al giro successivo del job. Qui si verifica che il registro
|
|
|
|
|
* `promemoria_eventi_generati` lo eviti, senza rompere la generazione normale.
|
|
|
|
|
*
|
|
|
|
|
* Gira solo sullo stack locale (`npx supabase start`). Usa eventi con il prefisso
|
|
|
|
|
* `test-promemoria-eventi` e gli slot `g4`, `g5`, `g6` della rosa seed come convocati; non
|
|
|
|
|
* serve un account collegato perché le notifiche si eliminano con la service role.
|
|
|
|
|
*/
|
|
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { statoLocale } from "../helpers/locale";
|
|
|
|
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
|
|
|
|
|
|
|
|
|
const locale = statoLocale();
|
|
|
|
|
|
|
|
|
|
if (!locale) {
|
|
|
|
|
salta("promemoria eventi", "stack locale non attivo (npx supabase start)");
|
|
|
|
|
riepilogo("promemoria-eventi");
|
|
|
|
|
} else {
|
|
|
|
|
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
|
|
|
|
console.log(`promemoria eventi su ${URL_BASE}`);
|
|
|
|
|
|
|
|
|
|
const PREFISSO = "test-promemoria-eventi";
|
|
|
|
|
const EVENTO = `${PREFISSO}-a`;
|
|
|
|
|
const EVENTO_LONTANO = `${PREFISSO}-lontano`;
|
|
|
|
|
const EVENTO_CASCATA = `${PREFISSO}-cascata`;
|
2026-09-30 14:03:50 +02:00
|
|
|
const EVENTO_DOMANI = `${PREFISSO}-domani`;
|
|
|
|
|
const EVENTO_OGGI = `${PREFISSO}-oggi`;
|
|
|
|
|
const EVENTO_SENZA_LUOGO = `${PREFISSO}-senza-luogo`;
|
2026-09-29 14:19:54 +02:00
|
|
|
const G4 = "g4";
|
|
|
|
|
const G5 = "g5";
|
|
|
|
|
const G6 = "g6";
|
|
|
|
|
|
|
|
|
|
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 ?? {}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function leggi<T>(percorso: string): Promise<T[]> {
|
|
|
|
|
const res = await rest(percorso, SERVIZIO);
|
|
|
|
|
if (!res.ok) throw new Error(`select su ${percorso}: ${res.status} ${await res.text()}`);
|
|
|
|
|
return (await res.json()) as T[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function scrivi(percorso: string, metodo: string, corpo?: unknown) {
|
|
|
|
|
const res = await rest(percorso, SERVIZIO, {
|
|
|
|
|
method: metodo,
|
|
|
|
|
body: corpo === undefined ? null : JSON.stringify(corpo),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`${metodo} su ${percorso}: ${res.status} ${await res.text()}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function cron(tipo: "evento_promemoria_24h" | "evento_promemoria_3h") {
|
|
|
|
|
const res = await fetch(`${URL_BASE}/rest/v1/rpc/genera_promemoria_eventi`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
apikey: SERVIZIO,
|
|
|
|
|
Authorization: `Bearer ${SERVIZIO}`,
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
p_tipo: tipo,
|
|
|
|
|
p_finestra: tipo === "evento_promemoria_24h" ? "24 hours" : "3 hours",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`genera_promemoria_eventi: ${res.status} ${await res.text()}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Data e ora dell'inizio tra `ore` ore, nel fuso Europe/Rome (convenzione di `eventi_app`). */
|
|
|
|
|
function tra(ore: number): { data: string; ora: string } {
|
|
|
|
|
const quando = new Date(Date.now() + ore * 60 * 60 * 1000);
|
|
|
|
|
const data = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(quando);
|
|
|
|
|
const ora = new Intl.DateTimeFormat("it-IT", {
|
|
|
|
|
timeZone: "Europe/Rome",
|
|
|
|
|
hour: "2-digit",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
hour12: false,
|
|
|
|
|
}).format(quando);
|
|
|
|
|
return { data, ora };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const notifiche = (evento: string, tipo: string) =>
|
|
|
|
|
leggi<{ id: string; giocatore_id: string; letta: boolean }>(
|
|
|
|
|
`notifiche_utente?evento_id=eq.${evento}&tipo=eq.${tipo}&select=id,giocatore_id,letta&order=giocatore_id`,
|
|
|
|
|
);
|
|
|
|
|
const registro = (evento: string, tipo?: string) =>
|
|
|
|
|
leggi<{ giocatore_id: string; tipo: string }>(
|
|
|
|
|
`promemoria_eventi_generati?evento_id=eq.${evento}${tipo ? `&tipo=eq.${tipo}` : ""}&select=giocatore_id,tipo`,
|
|
|
|
|
);
|
|
|
|
|
const inCoda = async (ids: string[]) =>
|
|
|
|
|
ids.length === 0
|
|
|
|
|
? []
|
|
|
|
|
: leggi<{ notifica_id: string }>(
|
|
|
|
|
`notifiche_email_coda?notifica_id=in.(${ids.join(",")})&select=notifica_id`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await scrivi("eventi_app", "POST", {
|
|
|
|
|
id: EVENTO,
|
|
|
|
|
tipo: "allenamento",
|
|
|
|
|
titolo: `${PREFISSO} evento`,
|
|
|
|
|
...tra(2),
|
|
|
|
|
convocati: [G4, G5],
|
|
|
|
|
});
|
|
|
|
|
await scrivi("eventi_app", "POST", {
|
|
|
|
|
id: EVENTO_LONTANO,
|
|
|
|
|
tipo: "allenamento",
|
|
|
|
|
titolo: `${PREFISSO} lontano`,
|
|
|
|
|
...tra(72),
|
|
|
|
|
convocati: [G4],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("il job genera il promemoria per i convocati e lo registra", async () => {
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
const n = await notifiche(EVENTO, "evento_promemoria_24h");
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
n.map((r) => r.giocatore_id),
|
|
|
|
|
[G4, G5],
|
|
|
|
|
);
|
|
|
|
|
assert.equal((await registro(EVENTO, "evento_promemoria_24h")).length, 2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("un secondo giro del job non duplica né riaccoda", async () => {
|
|
|
|
|
const prima = await notifiche(EVENTO, "evento_promemoria_24h");
|
|
|
|
|
const codaPrima = await inCoda(prima.map((r) => r.id));
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
const dopo = await notifiche(EVENTO, "evento_promemoria_24h");
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
dopo.map((r) => r.id),
|
|
|
|
|
prima.map((r) => r.id),
|
|
|
|
|
);
|
|
|
|
|
assert.equal((await inCoda(dopo.map((r) => r.id))).length, codaPrima.length);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova(
|
|
|
|
|
"eliminata la notifica, il promemoria NON ricompare al giro successivo",
|
|
|
|
|
async () => {
|
|
|
|
|
const [mia] = await notifiche(EVENTO, "evento_promemoria_24h");
|
|
|
|
|
assert.ok(mia);
|
|
|
|
|
await scrivi(`notifiche_utente?id=eq.${mia.id}`, "DELETE");
|
|
|
|
|
assert.equal((await notifiche(EVENTO, "evento_promemoria_24h")).length, 1);
|
|
|
|
|
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
const dopo = await notifiche(EVENTO, "evento_promemoria_24h");
|
|
|
|
|
assert.equal(dopo.length, 1, "solo la notifica dell'altro convocato, non quella eliminata");
|
|
|
|
|
assert.ok(!dopo.some((r) => r.giocatore_id === mia.giocatore_id));
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await prova("nessuna nuova mail in coda per il promemoria eliminato", async () => {
|
|
|
|
|
const righe = await leggi<{ id: string; giocatore_id: string }>(
|
|
|
|
|
`notifiche_utente?evento_id=eq.${EVENTO}&tipo=eq.evento_promemoria_24h&select=id,giocatore_id`,
|
|
|
|
|
);
|
|
|
|
|
const coda = await inCoda(righe.map((r) => r.id));
|
|
|
|
|
assert.equal(coda.length, righe.length, "una sola riga in coda per notifica rimasta");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("il promemoria a 3 ore è indipendente da quello a 24 ore", async () => {
|
|
|
|
|
await cron("evento_promemoria_3h");
|
|
|
|
|
const n = await notifiche(EVENTO, "evento_promemoria_3h");
|
|
|
|
|
assert.equal(n.length, 2, "l'evento parte tra 2 ore: rientra nella finestra di 3");
|
|
|
|
|
const [mia] = n;
|
|
|
|
|
assert.ok(mia);
|
|
|
|
|
await scrivi(`notifiche_utente?id=eq.${mia.id}`, "DELETE");
|
|
|
|
|
await cron("evento_promemoria_3h");
|
|
|
|
|
assert.equal((await notifiche(EVENTO, "evento_promemoria_3h")).length, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("un convocato aggiunto dopo riceve comunque il suo promemoria", async () => {
|
|
|
|
|
await scrivi(`eventi_app?id=eq.${EVENTO}`, "PATCH", { convocati: [G4, G5, G6] });
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
const n = await notifiche(EVENTO, "evento_promemoria_24h");
|
|
|
|
|
assert.ok(
|
|
|
|
|
n.some((r) => r.giocatore_id === G6),
|
|
|
|
|
"g6 ha ricevuto il promemoria",
|
|
|
|
|
);
|
|
|
|
|
assert.equal(n.filter((r) => r.giocatore_id === G6).length, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("un evento fuori dalla finestra non genera né registra nulla", async () => {
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
await cron("evento_promemoria_3h");
|
|
|
|
|
assert.equal((await notifiche(EVENTO_LONTANO, "evento_promemoria_24h")).length, 0);
|
|
|
|
|
assert.equal((await registro(EVENTO_LONTANO)).length, 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("il registro non si vede né si scrive da un client", async () => {
|
|
|
|
|
const lettura = await rest("promemoria_eventi_generati?select=*", ANON);
|
|
|
|
|
assert.ok(!lettura.ok, "anon non può leggere il registro");
|
|
|
|
|
const inserimento = await rest("promemoria_eventi_generati", ANON, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ giocatore_id: G4, evento_id: EVENTO, tipo: "evento_promemoria_3h" }),
|
|
|
|
|
});
|
|
|
|
|
assert.ok(!inserimento.ok, "anon non può scrivere nel registro");
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-30 14:03:50 +02:00
|
|
|
// --- Testo (DD-040) ------------------------------------------------------------------
|
|
|
|
|
// «domani» se la data dell'evento è quella di domani a Roma, «oggi» altrimenti. Per non
|
|
|
|
|
// dipendere dall'ora in cui gira il test: un evento domani a mezzanotte cade sempre nella
|
|
|
|
|
// finestra delle 24 ore, uno oggi alle 23:59 pure.
|
|
|
|
|
const oggiRoma = tra(0).data;
|
|
|
|
|
const domaniRoma = new Date(new Date(`${oggiRoma}T12:00:00Z`).getTime() + 24 * 3600 * 1000)
|
|
|
|
|
.toISOString()
|
|
|
|
|
.slice(0, 10);
|
|
|
|
|
const gg = (iso: string) => `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}`;
|
|
|
|
|
const testoDi = async (evento: string, tipo: string) => {
|
|
|
|
|
const [n] = await leggi<{ titolo: string; corpo: string }>(
|
|
|
|
|
`notifiche_utente?evento_id=eq.${evento}&tipo=eq.${tipo}&giocatore_id=eq.${G4}&select=titolo,corpo`,
|
|
|
|
|
);
|
|
|
|
|
assert.ok(n, `promemoria ${tipo} di ${evento} non generato`);
|
|
|
|
|
return n;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await prova("evento di domani: titolo e data dicono «domani», con ora e luogo", async () => {
|
|
|
|
|
await scrivi("eventi_app", "POST", {
|
|
|
|
|
id: EVENTO_DOMANI,
|
|
|
|
|
tipo: "partita",
|
|
|
|
|
titolo: `${PREFISSO} cena`,
|
|
|
|
|
data: domaniRoma,
|
|
|
|
|
ora: "00:00",
|
|
|
|
|
luogo: " PalaCRAP ",
|
|
|
|
|
convocati: [G4],
|
|
|
|
|
});
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
const n = await testoDi(EVENTO_DOMANI, "evento_promemoria_24h");
|
|
|
|
|
assert.equal(n.titolo, `Promemoria evento di domani: ${PREFISSO} cena`);
|
|
|
|
|
assert.equal(
|
|
|
|
|
n.corpo,
|
|
|
|
|
`Data: domani, ${gg(domaniRoma)}\nOra: 00:00\nLuogo: PalaCRAP`,
|
|
|
|
|
"il luogo è ripulito dagli spazi",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("evento di oggi: titolo e data dicono «oggi»", async () => {
|
|
|
|
|
await scrivi("eventi_app", "POST", {
|
|
|
|
|
id: EVENTO_OGGI,
|
|
|
|
|
tipo: "allenamento",
|
|
|
|
|
titolo: `${PREFISSO} oggi`,
|
|
|
|
|
data: oggiRoma,
|
|
|
|
|
ora: "23:59",
|
|
|
|
|
luogo: "Palestra",
|
|
|
|
|
convocati: [G4],
|
|
|
|
|
});
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
const n = await testoDi(EVENTO_OGGI, "evento_promemoria_24h");
|
|
|
|
|
assert.equal(n.titolo, `Promemoria evento di oggi: ${PREFISSO} oggi`);
|
|
|
|
|
assert.equal(n.corpo, `Data: oggi, ${gg(oggiRoma)}\nOra: 23:59\nLuogo: Palestra`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova("lo stesso testo vale per il promemoria a 3 ore", async () => {
|
|
|
|
|
const { data, ora } = tra(2);
|
|
|
|
|
await scrivi("eventi_app", "POST", {
|
|
|
|
|
id: EVENTO_SENZA_LUOGO,
|
|
|
|
|
tipo: "evento",
|
|
|
|
|
titolo: `${PREFISSO} senza luogo`,
|
|
|
|
|
data,
|
|
|
|
|
ora,
|
|
|
|
|
convocati: [G4],
|
|
|
|
|
});
|
|
|
|
|
await cron("evento_promemoria_3h");
|
|
|
|
|
const n = await testoDi(EVENTO_SENZA_LUOGO, "evento_promemoria_3h");
|
|
|
|
|
const parola = data === domaniRoma ? "domani" : "oggi";
|
|
|
|
|
assert.equal(
|
|
|
|
|
n.titolo,
|
|
|
|
|
`Promemoria evento di ${parola}: ${PREFISSO} senza luogo`,
|
|
|
|
|
"anche il promemoria a 3 ore di un evento dopo mezzanotte dice «domani»",
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
n.corpo,
|
|
|
|
|
`Data: ${parola}, ${gg(data)}\nOra: ${ora}`,
|
|
|
|
|
"senza luogo la riga «Luogo» manca",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prova(
|
|
|
|
|
"il promemoria nuovo parte come push e come email, con lo stesso testo",
|
|
|
|
|
async () => {
|
|
|
|
|
const [n] = await leggi<{ id: string }>(
|
|
|
|
|
`notifiche_utente?evento_id=eq.${EVENTO_DOMANI}&tipo=eq.evento_promemoria_24h&giocatore_id=eq.${G4}&select=id`,
|
|
|
|
|
);
|
|
|
|
|
assert.ok(n);
|
|
|
|
|
assert.equal((await inCoda([n.id])).length, 1, "email in coda");
|
|
|
|
|
const push = await leggi<{ notifica_id: string }>(
|
|
|
|
|
`notifiche_push_coda?notifica_id=eq.${n.id}&select=notifica_id`,
|
|
|
|
|
);
|
|
|
|
|
assert.equal(push.length, 1, "push in coda");
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-09-29 14:19:54 +02:00
|
|
|
await prova("cancellare l'evento toglie anche il registro", async () => {
|
|
|
|
|
const { data, ora } = { data: tra(2).data, ora: tra(2).ora };
|
|
|
|
|
await scrivi("eventi_app", "POST", {
|
|
|
|
|
id: EVENTO_CASCATA,
|
|
|
|
|
tipo: "partita",
|
|
|
|
|
titolo: `${PREFISSO} cascata`,
|
|
|
|
|
data,
|
|
|
|
|
ora,
|
|
|
|
|
convocati: [G4],
|
|
|
|
|
});
|
|
|
|
|
await cron("evento_promemoria_24h");
|
|
|
|
|
assert.equal((await registro(EVENTO_CASCATA)).length, 1);
|
|
|
|
|
await scrivi(`eventi_app?id=eq.${EVENTO_CASCATA}`, "DELETE");
|
|
|
|
|
assert.equal((await registro(EVENTO_CASCATA)).length, 0);
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
2026-09-30 14:03:50 +02:00
|
|
|
for (const id of [
|
|
|
|
|
EVENTO,
|
|
|
|
|
EVENTO_LONTANO,
|
|
|
|
|
EVENTO_CASCATA,
|
|
|
|
|
EVENTO_DOMANI,
|
|
|
|
|
EVENTO_OGGI,
|
|
|
|
|
EVENTO_SENZA_LUOGO,
|
|
|
|
|
]) {
|
2026-09-29 14:19:54 +02:00
|
|
|
await rest(`eventi_app?id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
|
|
|
|
}
|
|
|
|
|
riepilogo("promemoria-eventi");
|
|
|
|
|
}
|
|
|
|
|
}
|