I pulsanti «Avvisa chi è di turno» e «Sollecita» usano gli stessi testi dei job del database. Il sollecito manuale segue i convocati e dice la risposta attuale e chi lo ha chiesto. L'ordine degli eventi dei palloni diventa data, ora, id. Le righe del corpo di una notifica vanno a capo nel centro notifiche. Test unitari dei testi, dei destinatari e delle date; il test delle route verifica testi, convocati e assenza di email per i palloni. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
545 lines
22 KiB
TypeScript
545 lines
22 KiB
TypeScript
/**
|
|
* Destinatari delle notifiche per ruolo: `bun test/integration/destinatari-notifiche.test.ts`.
|
|
*
|
|
* Verifica, chiamando le route vere, le regole del catalogo delle notifiche
|
|
* (docs/modules/notifiche.md) che finora erano coperte solo dalla lettura del codice:
|
|
*
|
|
* - i promemoria a 24 e 3 ore arrivano ai convocati (a tutti i giocatori attivi se `convocati`
|
|
* è vuoto), mai all'allenatore (DD-039);
|
|
* - la push del sondaggio pre-partita non arriva ai dispositivi dell'allenatore, quella del
|
|
* messaggio dello staff sì (DD-034, DD-038);
|
|
* - il sollecito presenze arriva a chi non ha risposto o ha risposto «forse», mai all'allenatore,
|
|
* ed è permesso ad allenatore e admin ma non a un giocatore;
|
|
* - il turno palloni arriva all'incaricato e a chi li aveva prima, solo per allenamenti e partite,
|
|
* ed è riservato agli admin.
|
|
*
|
|
* Le push vanno a un servizio push finto in locale: nessuna richiesta esce dalla macchina.
|
|
* Gira solo sullo stack locale (`npx supabase start`). Il server di sviluppo che riceve le
|
|
* chiamate deve avere chiavi VAPID (il test le imposta se avvia lui il server; con `BASE_URL`
|
|
* valgono quelle di quel server) e usa gli slot `g1`, `g2`, `g4`, `g5` e `g6` della rosa seed,
|
|
* ripristinati alla fine.
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { createServer, type Server } from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
import { statoLocale } from "../helpers/locale";
|
|
import { avviaServer } from "../helpers/server";
|
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
|
|
|
const locale = statoLocale();
|
|
|
|
if (!locale) {
|
|
salta("destinatari notifiche", "stack locale non attivo (npx supabase start)");
|
|
riepilogo("destinatari-notifiche");
|
|
} else {
|
|
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
|
console.log(`destinatari notifiche su ${URL_BASE}`);
|
|
|
|
// Il server di sviluppo eredita queste: senza, le route punterebbero al progetto cloud.
|
|
process.env["SUPABASE_URL"] = URL_BASE;
|
|
process.env["SUPABASE_PUBLISHABLE_KEY"] = ANON;
|
|
process.env["SUPABASE_SERVICE_ROLE_KEY"] = SERVIZIO;
|
|
|
|
const b64u = (dati: ArrayBuffer | Uint8Array) =>
|
|
Buffer.from(dati as ArrayBuffer).toString("base64url");
|
|
|
|
// Chiavi VAPID di prova (P-256), nel formato di `webpush.server.ts`: la chiave privata reale
|
|
// non deve mai servire ai test.
|
|
const vapid = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
"sign",
|
|
]);
|
|
const jwk = await crypto.subtle.exportKey("jwk", vapid.privateKey);
|
|
process.env["VAPID_PUBLIC_KEY"] = b64u(
|
|
new Uint8Array([
|
|
4,
|
|
...Buffer.from(jwk.x ?? "", "base64url"),
|
|
...Buffer.from(jwk.y ?? "", "base64url"),
|
|
]),
|
|
);
|
|
process.env["VAPID_PRIVATE_KEY"] = jwk.d ?? "";
|
|
|
|
async function chiaviDispositivo() {
|
|
const k = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, [
|
|
"deriveBits",
|
|
]);
|
|
return {
|
|
p256dh: b64u(await crypto.subtle.exportKey("raw", k.publicKey)),
|
|
auth: b64u(crypto.getRandomValues(new Uint8Array(16))),
|
|
};
|
|
}
|
|
|
|
const PREFISSO = "test-destinatari-notifiche";
|
|
const PASSWORD = "prova-destinatari-notifiche-123";
|
|
const G_UNO = "g1";
|
|
const G_DUE = "g2";
|
|
const G_QUATTRO = "g4";
|
|
const G_CINQUE = "g5";
|
|
const ALLENATORE = "g6";
|
|
const PARTITA = `${PREFISSO}-partita`;
|
|
const PRESENZE = `${PREFISSO}-presenze`;
|
|
const PRESENZE_CONVOCATI = `${PREFISSO}-presenze-convocati`;
|
|
const PALLONI_1 = `${PREFISSO}-palloni-1`;
|
|
const PALLONI_2 = `${PREFISSO}-palloni-2`;
|
|
const GENERICO = `${PREFISSO}-generico`;
|
|
const PROMEMORIA_CONVOCATI = `${PREFISSO}-promemoria-convocati`;
|
|
const PROMEMORIA_TUTTI = `${PREFISSO}-promemoria-tutti`;
|
|
const TITOLO_STAFF = `${PREFISSO} messaggio ${Date.now()}`;
|
|
|
|
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, token: string, metodo: string, corpo?: unknown) {
|
|
const res = await rest(percorso, token, {
|
|
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 creaUtente(email: string): Promise<{ id: string; token: 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: PASSWORD, email_confirm: true }),
|
|
});
|
|
const corpo = (await res.json()) as { id?: string };
|
|
if (!corpo.id) throw new Error(`creazione utente fallita: ${JSON.stringify(corpo)}`);
|
|
const login = await fetch(`${URL_BASE}/auth/v1/token?grant_type=password`, {
|
|
method: "POST",
|
|
headers: { apikey: ANON, "content-type": "application/json" },
|
|
body: JSON.stringify({ email, password: PASSWORD }),
|
|
});
|
|
const t = (await login.json()) as { access_token?: string };
|
|
if (!t.access_token) throw new Error("accesso fallito");
|
|
return { id: corpo.id, token: t.access_token };
|
|
}
|
|
|
|
// --- Servizio push finto: registra a quale dispositivo arriva ogni push ------------------
|
|
const ricevute: string[] = [];
|
|
const servizioPush: Server = createServer((req, res) => {
|
|
req.resume();
|
|
req.on("end", () => {
|
|
ricevute.push((req.url ?? "").replace(/^\//, ""));
|
|
res.statusCode = 201;
|
|
res.end();
|
|
});
|
|
});
|
|
await new Promise<void>((ok) => servizioPush.listen(0, "127.0.0.1", ok));
|
|
const porta = (servizioPush.address() as AddressInfo).port;
|
|
const endpoint = (slot: string) => `http://127.0.0.1:${porta}/${slot}`;
|
|
const svuota = () => {
|
|
ricevute.length = 0;
|
|
};
|
|
/** A quali slot è arrivata una push dall'ultimo `svuota()`, senza ripetizioni e in ordine. */
|
|
const destinatariPush = () => [...new Set(ricevute)].sort();
|
|
|
|
type Notifica = { giocatore_id: string; titolo: string; corpo: string; tipo: string };
|
|
const notifiche = (filtro: string) =>
|
|
leggi<Notifica>(
|
|
`notifiche_utente?${filtro}&select=giocatore_id,titolo,corpo,tipo&order=giocatore_id`,
|
|
);
|
|
const slot = (righe: Notifica[]) => righe.map((r) => r.giocatore_id);
|
|
|
|
const idUtenti: string[] = [];
|
|
let server: { baseUrl: string; stop: () => void } | null = null;
|
|
let tokenAdmin = "";
|
|
const originali = new Map<string, { tipo: string; auth_user_id: string | null }>();
|
|
|
|
try {
|
|
const admin = await creaUtente(`${PREFISSO}-admin-${Date.now()}@example.test`);
|
|
const allenatore = await creaUtente(`${PREFISSO}-allenatore-${Date.now()}@example.test`);
|
|
const giocatore = await creaUtente(`${PREFISSO}-giocatore-${Date.now()}@example.test`);
|
|
idUtenti.push(admin.id, allenatore.id, giocatore.id);
|
|
await scrivi("user_roles", SERVIZIO, "POST", { user_id: admin.id, role: "admin" });
|
|
tokenAdmin = admin.token;
|
|
|
|
const [g6] = await leggi<{ tipo: string; auth_user_id: string | null }>(
|
|
`giocatori_squadra?id=eq.${ALLENATORE}&select=tipo,auth_user_id`,
|
|
);
|
|
originali.set(ALLENATORE, g6 ?? { tipo: "giocatore", auth_user_id: null });
|
|
// Il trigger di `giocatori_squadra` rifiuta la service key: serve il JWT dell'admin. Il
|
|
// trigger di M21 assegna il ruolo `allenatore` all'account collegato a uno slot allenatore.
|
|
await scrivi(`giocatori_squadra?id=eq.${ALLENATORE}`, tokenAdmin, "PATCH", {
|
|
tipo: "allenatore",
|
|
auth_user_id: allenatore.id,
|
|
});
|
|
|
|
for (const s of [G_QUATTRO, G_CINQUE, ALLENATORE]) {
|
|
await scrivi("push_subscriptions", SERVIZIO, "POST", {
|
|
giocatore_id: s,
|
|
endpoint: endpoint(s),
|
|
...(await chiaviDispositivo()),
|
|
});
|
|
}
|
|
for (const [id, tipo, data] of [
|
|
[PARTITA, "partita", "2031-04-01"],
|
|
[PRESENZE, "allenamento", "2031-04-02"],
|
|
[PRESENZE_CONVOCATI, "partita", "2031-04-03"],
|
|
[PALLONI_1, "allenamento", "2031-05-01"],
|
|
[PALLONI_2, "allenamento", "2031-05-08"],
|
|
[GENERICO, "evento", "2031-05-09"],
|
|
] as const) {
|
|
await scrivi("eventi_app", SERVIZIO, "POST", {
|
|
id,
|
|
tipo,
|
|
titolo: `${PREFISSO} ${id}`,
|
|
data,
|
|
ora: "20:00",
|
|
luogo: id === PRESENZE ? "PalaCRAP" : "",
|
|
convocati: id === PRESENZE_CONVOCATI ? [G_QUATTRO, G_CINQUE] : [],
|
|
});
|
|
}
|
|
|
|
// Promemoria: eventi che iniziano tra 2 ore, dentro le finestre dei job a 24 e a 3 ore.
|
|
const tra2Ore = new Date(Date.now() + 2 * 3_600_000);
|
|
const dataRoma = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(tra2Ore);
|
|
const oraRoma = new Intl.DateTimeFormat("it-IT", {
|
|
timeZone: "Europe/Rome",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
}).format(tra2Ore);
|
|
for (const [id, convocati] of [
|
|
[PROMEMORIA_CONVOCATI, [G_QUATTRO]],
|
|
[PROMEMORIA_TUTTI, []],
|
|
] as const) {
|
|
await scrivi("eventi_app", SERVIZIO, "POST", {
|
|
id,
|
|
tipo: "allenamento",
|
|
titolo: `${PREFISSO} ${id}`,
|
|
data: dataRoma,
|
|
ora: oraRoma,
|
|
convocati: [...convocati],
|
|
});
|
|
}
|
|
|
|
server = await avviaServer();
|
|
console.log(`route su ${server.baseUrl}`);
|
|
const chiama = (percorso: string, token: string, corpo: unknown) =>
|
|
fetch(`${server!.baseUrl}/api/public/${percorso}`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
body: JSON.stringify(corpo),
|
|
});
|
|
|
|
const attivi = (
|
|
await leggi<{ id: string; tipo: string }>("giocatori_squadra?attivo=eq.true&select=id,tipo")
|
|
).sort((a, b) => a.id.localeCompare(b.id));
|
|
const giocatoriAttivi = attivi.filter((g) => g.tipo === "giocatore").map((g) => g.id);
|
|
assert.ok(attivi.some((g) => g.id === ALLENATORE && g.tipo === "allenatore"));
|
|
|
|
// --- 0. Promemoria a 24 e 3 ore (generati dal database) --------------------------------
|
|
await prova(
|
|
"il promemoria arriva ai soli convocati: l'allenatore non lo riceve (DD-039)",
|
|
async () => {
|
|
for (const [tipo, finestra] of [
|
|
["evento_promemoria_24h", "24 hours"],
|
|
["evento_promemoria_3h", "3 hours"],
|
|
] as const) {
|
|
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: finestra }),
|
|
});
|
|
assert.ok(res.ok, `genera_promemoria_eventi ${tipo}`);
|
|
const righe = await notifiche(`evento_id=eq.${PROMEMORIA_CONVOCATI}&tipo=eq.${tipo}`);
|
|
assert.deepEqual(slot(righe), [G_QUATTRO], tipo);
|
|
}
|
|
},
|
|
);
|
|
|
|
await prova(
|
|
"con convocati vuoto il promemoria arriva a tutti i giocatori attivi, mai all'allenatore",
|
|
async () => {
|
|
for (const tipo of ["evento_promemoria_24h", "evento_promemoria_3h"]) {
|
|
const righe = await notifiche(`evento_id=eq.${PROMEMORIA_TUTTI}&tipo=eq.${tipo}`);
|
|
assert.deepEqual(slot(righe), giocatoriAttivi, tipo);
|
|
}
|
|
},
|
|
);
|
|
|
|
// --- 1. Messaggio dello staff ---------------------------------------------------------
|
|
await prova(
|
|
"controllo: la push arriva al servizio finto (il server ha le chiavi VAPID)",
|
|
async () => {
|
|
svuota();
|
|
const res = await chiama("notifica-personalizzata", tokenAdmin, {
|
|
messaggio: `${TITOLO_STAFF} controllo`,
|
|
giocatoreId: G_QUATTRO,
|
|
});
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual(
|
|
destinatariPush(),
|
|
[G_QUATTRO],
|
|
"il server di prova non manda push: servono VAPID_PUBLIC_KEY e VAPID_PRIVATE_KEY, oppure non usare BASE_URL",
|
|
);
|
|
},
|
|
);
|
|
|
|
await prova(
|
|
"il messaggio dello staff a tutti raggiunge anche i dispositivi dell'allenatore",
|
|
async () => {
|
|
svuota();
|
|
const res = await chiama("notifica-personalizzata", tokenAdmin, {
|
|
messaggio: TITOLO_STAFF,
|
|
});
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual(destinatariPush(), [ALLENATORE, G_QUATTRO, G_CINQUE].sort());
|
|
const righe = await notifiche(`corpo=eq.${encodeURIComponent(TITOLO_STAFF)}&tipo=eq.admin`);
|
|
assert.deepEqual(
|
|
slot(righe),
|
|
attivi.map((g) => g.id),
|
|
"in-app: tutta la rosa attiva, allenatore compreso",
|
|
);
|
|
},
|
|
);
|
|
|
|
await prova("il messaggio a un solo giocatore non arriva agli altri dispositivi", async () => {
|
|
svuota();
|
|
await chiama("notifica-personalizzata", tokenAdmin, {
|
|
messaggio: `${TITOLO_STAFF} solo g5`,
|
|
giocatoreId: G_CINQUE,
|
|
});
|
|
assert.deepEqual(destinatariPush(), [G_CINQUE]);
|
|
});
|
|
|
|
await prova(
|
|
"un allenatore o un giocatore non possono mandare il messaggio dello staff",
|
|
async () => {
|
|
for (const token of [allenatore.token, giocatore.token]) {
|
|
const res = await chiama("notifica-personalizzata", token, { messaggio: "no" });
|
|
assert.ok(!res.ok, `stato ${res.status}`);
|
|
}
|
|
},
|
|
);
|
|
|
|
// --- 2. Sondaggio pre-partita ---------------------------------------------------------
|
|
await prova("il sondaggio non manda la push ai dispositivi dell'allenatore", async () => {
|
|
svuota();
|
|
const res = await chiama("apri-sondaggio", tokenAdmin, { eventoId: PARTITA });
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual(destinatariPush(), [G_QUATTRO, G_CINQUE].sort());
|
|
});
|
|
|
|
await prova("il sondaggio non crea l'avviso in-app per l'allenatore", async () => {
|
|
const righe = await notifiche(`evento_id=eq.${PARTITA}&tipo=eq.sondaggio_cacche`);
|
|
assert.deepEqual(slot(righe), giocatoriAttivi);
|
|
});
|
|
|
|
await prova("un allenatore non può aprire il sondaggio", async () => {
|
|
const res = await chiama("apri-sondaggio", allenatore.token, { eventoId: PARTITA });
|
|
assert.ok(!res.ok, `stato ${res.status}`);
|
|
});
|
|
|
|
// --- 3. Sollecito presenze ------------------------------------------------------------
|
|
await scrivi("risposte_presenze", SERVIZIO, "POST", [
|
|
{ evento_id: PRESENZE, giocatore_id: G_QUATTRO, stato: "presente" },
|
|
{ evento_id: PRESENZE, giocatore_id: G_CINQUE, stato: "forse" },
|
|
]);
|
|
|
|
await prova(
|
|
"l'allenatore può sollecitare: arriva a chi non ha risposto e a chi ha detto «forse»",
|
|
async () => {
|
|
svuota();
|
|
const res = await chiama("sollecita-presenze", allenatore.token, { eventoId: PRESENZE });
|
|
assert.equal(res.status, 200);
|
|
const righe = await notifiche(`evento_id=eq.${PRESENZE}&tipo=eq.sollecita_presenze`);
|
|
assert.deepEqual(
|
|
slot(righe),
|
|
giocatoriAttivi.filter((id) => id !== G_QUATTRO),
|
|
"esclusi chi ha già risposto e l'allenatore, incluso chi ha risposto «forse»",
|
|
);
|
|
assert.ok(slot(righe).includes(G_CINQUE));
|
|
assert.deepEqual(destinatariPush(), [G_CINQUE], "push solo ai dispositivi dei sollecitati");
|
|
},
|
|
);
|
|
|
|
await prova(
|
|
"il sollecito manuale ha il testo nuovo, con chi lo chiede e la risposta attuale",
|
|
async () => {
|
|
svuota();
|
|
const res = await chiama("sollecita-presenze", tokenAdmin, {
|
|
eventoId: PRESENZE,
|
|
da: "Mario Rossi",
|
|
});
|
|
assert.equal(res.status, 200);
|
|
const righe = await notifiche(`evento_id=eq.${PRESENZE}&tipo=eq.sollecita_presenze`);
|
|
const cinque = righe.find((r) => r.giocatore_id === G_CINQUE);
|
|
const altro = righe.find((r) => r.giocatore_id !== G_CINQUE);
|
|
assert.equal(
|
|
cinque?.titolo,
|
|
`Conferma di partecipazione richiesta: ${PREFISSO} ${PRESENZE}`,
|
|
);
|
|
assert.equal(
|
|
cinque?.corpo,
|
|
[
|
|
"Data: 02/04/2031",
|
|
"Ora: 20:00",
|
|
"Luogo: PalaCRAP",
|
|
"Richiesta di: Mario Rossi",
|
|
"Risposta attuale: forse",
|
|
"Azione: indicare presente, assente o in ritardo",
|
|
].join("\n"),
|
|
);
|
|
assert.match(altro?.corpo ?? "", /Risposta attuale: nessuna/, "chi non ha risposto");
|
|
const conId = await leggi<{ id: string }>(
|
|
`notifiche_utente?evento_id=eq.${PRESENZE}&tipo=eq.sollecita_presenze&select=id`,
|
|
);
|
|
const email = await leggi<{ notifica_id: string }>(
|
|
`notifiche_email_coda?notifica_id=in.(${conId.map((r) => r.id).join(",")})&select=notifica_id`,
|
|
);
|
|
assert.equal(email.length, conId.length, "il sollecito manuale parte anche per email");
|
|
assert.equal(
|
|
righe.filter((r) => r.giocatore_id === G_CINQUE).length,
|
|
1,
|
|
"ripremere il pulsante riscrive la notifica, non la duplica",
|
|
);
|
|
assert.deepEqual(destinatariPush(), [G_CINQUE]);
|
|
},
|
|
);
|
|
|
|
await prova(
|
|
"il sollecito manuale segue i convocati, come quello automatico (DD-040)",
|
|
async () => {
|
|
const res = await chiama("sollecita-presenze", tokenAdmin, {
|
|
eventoId: PRESENZE_CONVOCATI,
|
|
});
|
|
assert.equal(res.status, 200);
|
|
const righe = await notifiche(
|
|
`evento_id=eq.${PRESENZE_CONVOCATI}&tipo=eq.sollecita_presenze`,
|
|
);
|
|
assert.deepEqual(
|
|
slot(righe),
|
|
[G_QUATTRO, G_CINQUE],
|
|
"solo i convocati: i non convocati non ricevono nulla, nemmeno senza risposta",
|
|
);
|
|
assert.ok(
|
|
!righe[0]!.corpo.includes("Richiesta di"),
|
|
"senza nome, senza la riga «Richiesta di»",
|
|
);
|
|
},
|
|
);
|
|
|
|
await prova("un giocatore non può sollecitare le presenze", async () => {
|
|
const res = await chiama("sollecita-presenze", giocatore.token, { eventoId: PRESENZE });
|
|
assert.ok(!res.ok, `stato ${res.status}`);
|
|
});
|
|
|
|
// --- 4. Turno palloni -----------------------------------------------------------------
|
|
await scrivi("turni_palloni", SERVIZIO, "POST", [
|
|
{ evento_id: PALLONI_1, giocatore_id: G_UNO },
|
|
{ evento_id: PALLONI_2, giocatore_id: G_DUE },
|
|
]);
|
|
|
|
await prova("il turno palloni arriva all'incaricato e a chi li aveva prima", async () => {
|
|
const res = await chiama("promemoria-palloni", tokenAdmin, { eventoId: PALLONI_2 });
|
|
assert.equal(res.status, 200);
|
|
const righe = await notifiche(`evento_id=eq.${PALLONI_2}&tipo=eq.turno_palloni`);
|
|
assert.deepEqual(slot(righe), [G_UNO, G_DUE]);
|
|
const per = new Map(righe.map((r) => [r.giocatore_id, r.titolo]));
|
|
assert.equal(per.get(G_DUE), "Turno palloni: incarico assegnato");
|
|
assert.equal(per.get(G_UNO), "Turno palloni: riconsegna");
|
|
const corpoDue = righe.find((r) => r.giocatore_id === G_DUE)?.corpo ?? "";
|
|
assert.match(
|
|
corpoDue,
|
|
/^Evento: test-destinatari-notifiche test-destinatari-notifiche-palloni-2\n/,
|
|
);
|
|
assert.match(corpoDue, /\nData: 08\/05\/2031, ore 20:00\n/);
|
|
assert.match(corpoDue, /Incarico: custodia dei palloni al termine dell'evento/);
|
|
|
|
// DD-040: il turno palloni arriva come push e in-app, mai per email.
|
|
const conId = await leggi<{ id: string }>(
|
|
`notifiche_utente?evento_id=eq.${PALLONI_2}&tipo=eq.turno_palloni&select=id`,
|
|
);
|
|
assert.equal(conId.length, righe.length);
|
|
const email = await leggi<{ notifica_id: string }>(
|
|
`notifiche_email_coda?notifica_id=in.(${conId.map((r) => r.id).join(",")})&select=notifica_id`,
|
|
);
|
|
assert.equal(email.length, 0, "nessuna email per il turno palloni");
|
|
});
|
|
|
|
await prova(
|
|
"per un evento che non è allenamento né partita non parte nessun avviso",
|
|
async () => {
|
|
await scrivi("turni_palloni", SERVIZIO, "POST", {
|
|
evento_id: GENERICO,
|
|
giocatore_id: G_QUATTRO,
|
|
});
|
|
const res = await chiama("promemoria-palloni", tokenAdmin, { eventoId: GENERICO });
|
|
assert.equal(res.status, 200);
|
|
assert.equal((await notifiche(`evento_id=eq.${GENERICO}&tipo=eq.turno_palloni`)).length, 0);
|
|
},
|
|
);
|
|
|
|
await prova("un allenatore non può far partire il turno palloni", async () => {
|
|
const res = await chiama("promemoria-palloni", allenatore.token, { eventoId: PALLONI_2 });
|
|
assert.ok(!res.ok, `stato ${res.status}`);
|
|
});
|
|
} finally {
|
|
server?.stop();
|
|
servizioPush.close();
|
|
await rest(
|
|
`push_subscriptions?endpoint=like.${encodeURIComponent(`http://127.0.0.1:${porta}/*`)}`,
|
|
SERVIZIO,
|
|
{
|
|
method: "DELETE",
|
|
},
|
|
);
|
|
await rest(`notifiche_utente?corpo=like.${encodeURIComponent(`${PREFISSO}*`)}`, SERVIZIO, {
|
|
method: "DELETE",
|
|
});
|
|
for (const id of [
|
|
PARTITA,
|
|
PRESENZE,
|
|
PRESENZE_CONVOCATI,
|
|
PALLONI_1,
|
|
PALLONI_2,
|
|
GENERICO,
|
|
PROMEMORIA_CONVOCATI,
|
|
PROMEMORIA_TUTTI,
|
|
]) {
|
|
await rest(`notifiche_utente?evento_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
|
await rest(`turni_palloni?evento_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
|
await rest(`eventi_app?id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
|
}
|
|
if (tokenAdmin) {
|
|
for (const [id, valori] of originali) {
|
|
await rest(`giocatori_squadra?id=eq.${id}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(valori),
|
|
});
|
|
}
|
|
}
|
|
for (const id of idUtenti) {
|
|
await rest(`user_roles?user_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
|
await fetch(`${URL_BASE}/auth/v1/admin/users/${id}`, {
|
|
method: "DELETE",
|
|
headers: { apikey: SERVIZIO, Authorization: `Bearer ${SERVIZIO}` },
|
|
});
|
|
}
|
|
riepilogo("destinatari-notifiche");
|
|
}
|
|
}
|