m25: giocatori_destinatari_evento() include solo i giocatori attivi, quindi l'allenatore non riceve più i promemoria a 24 e 3 ore, per nessun tipo di evento (fino alla m24 li riceveva sempre, DD-034). Riceve ancora i messaggi dello staff. Test: nuovo destinatari-notifiche.test.ts (14 controlli sulle route vere con un servizio push finto: promemoria, messaggio dello staff, sondaggio, sollecito, turno palloni, permessi di allenatore e giocatore) e aggiornato permessi-allenatore. Documentazione: DD-039, tabella delle notifiche dell'allenatore, catalogo e tabella dei test che lo verificano. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
357 lines
14 KiB
TypeScript
357 lines
14 KiB
TypeScript
/**
|
|
* Permessi del ruolo allenatore (DD-034, migration M21): `bun test/integration/permessi-allenatore.test.ts`.
|
|
*
|
|
* Stesso impianto di `permessi.test.ts`: utenti veri, database interrogato *come loro*,
|
|
* solo contro lo stack locale. L'allenatore è uno slot di `giocatori_squadra` con
|
|
* `tipo = 'allenatore'`, creato dall'admin e reclamato per email al primo accesso.
|
|
*
|
|
* Stato toccato e ripristinato alla fine: gli utenti creati, lo slot allenatore (cancellato),
|
|
* lo slot g3 reclamato dal giocatore di prova e le righe con il prefisso `test-allenatore`.
|
|
*/
|
|
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 allenatore", "stack locale non attivo (npx supabase start)");
|
|
riepilogo("permessi-allenatore");
|
|
} else {
|
|
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
|
console.log(`permessi allenatore 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 ?? {}),
|
|
},
|
|
});
|
|
|
|
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 };
|
|
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 ruoliDi = async (idUtente: string): Promise<string[]> => {
|
|
const res = await rest(`user_roles?user_id=eq.${idUtente}&select=role`, SERVIZIO);
|
|
return ((await res.json()) as Array<{ role: string }>).map((r) => r.role);
|
|
};
|
|
|
|
const PASSWORD = "prova-allenatore-123";
|
|
const PREFISSO = "test-allenatore";
|
|
const SLOT = "g9001";
|
|
const GIOCATORE = "g3";
|
|
const EVENTO = `${PREFISSO}-evento`;
|
|
const idUtenti: string[] = [];
|
|
let tokenAdmin = "";
|
|
|
|
try {
|
|
const oggi = Date.now();
|
|
const emailAllenatore = `test-allenatore-${oggi}@example.test`;
|
|
const emailAdmin = `test-admin-all-${oggi}@example.test`;
|
|
const emailGiocatore = `test-giocatore-all-${oggi}@example.test`;
|
|
const idAllenatore = await creaUtente(emailAllenatore, PASSWORD);
|
|
const idAdmin = await creaUtente(emailAdmin, PASSWORD);
|
|
const idGiocatore = await creaUtente(emailGiocatore, PASSWORD);
|
|
idUtenti.push(idAllenatore, idAdmin, idGiocatore);
|
|
|
|
await rest("user_roles", SERVIZIO, {
|
|
method: "POST",
|
|
body: JSON.stringify({ user_id: idAdmin, role: "admin" }),
|
|
});
|
|
tokenAdmin = await accedi(emailAdmin, PASSWORD);
|
|
const tokenAllenatore = await accedi(emailAllenatore, PASSWORD);
|
|
const tokenGiocatore = await accedi(emailGiocatore, PASSWORD);
|
|
|
|
const collegaGiocatore = await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ auth_user_id: idGiocatore }),
|
|
});
|
|
assert.equal(await righeToccate(collegaGiocatore), 1, `${GIOCATORE} collegato al test`);
|
|
|
|
await prova("l'admin registra un allenatore senza numero né ruolo", async () => {
|
|
const res = await rest("giocatori_squadra", tokenAdmin, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
id: SLOT,
|
|
nome: "Carla",
|
|
cognome: "Neri",
|
|
numero: null,
|
|
ruolo: "",
|
|
email: emailAllenatore,
|
|
tipo: "allenatore",
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(res), 1, `slot allenatore creato (${res.status})`);
|
|
});
|
|
|
|
await prova("un giocatore senza numero resta rifiutato", async () => {
|
|
const res = await rest("giocatori_squadra", tokenAdmin, {
|
|
method: "POST",
|
|
body: JSON.stringify({ id: "g9002", nome: "X", cognome: "Y", numero: null, ruolo: "" }),
|
|
});
|
|
assert.ok(!res.ok, `il numero è obbligatorio per chi gioca (${res.status})`);
|
|
});
|
|
|
|
await prova("il collegamento per email dà il ruolo allenatore", async () => {
|
|
assert.deepEqual(await ruoliDi(idAllenatore), [], "prima del collegamento nessun ruolo");
|
|
const res = await rest(
|
|
`giocatori_squadra?id=eq.${SLOT}&auth_user_id=is.null`,
|
|
tokenAllenatore,
|
|
{
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ auth_user_id: idAllenatore }),
|
|
},
|
|
);
|
|
assert.equal(await righeToccate(res), 1, `slot reclamato (${res.status})`);
|
|
assert.deepEqual(await ruoliDi(idAllenatore), ["allenatore"]);
|
|
});
|
|
|
|
await prova("l'allenatore crea, modifica ed elimina gli eventi", async () => {
|
|
const crea = await rest("eventi_app", tokenAllenatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
id: EVENTO,
|
|
tipo: "partita",
|
|
titolo: "Partita di prova",
|
|
data: "2026-01-01",
|
|
ora: "20:00",
|
|
luogo: "Palestra",
|
|
convocati: [GIOCATORE],
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(crea), 1, `evento creato (${crea.status})`);
|
|
|
|
const modifica = await rest(`eventi_app?id=eq.${EVENTO}`, tokenAllenatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ luogo: "Altra palestra" }),
|
|
});
|
|
assert.equal(await righeToccate(modifica), 1, "evento modificato");
|
|
|
|
const altro = await rest("eventi_app", tokenAllenatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({
|
|
id: `${PREFISSO}-da-cancellare`,
|
|
tipo: "allenamento",
|
|
titolo: "Da cancellare",
|
|
data: "2026-01-02",
|
|
ora: "20:00",
|
|
luogo: "",
|
|
}),
|
|
});
|
|
assert.equal(await righeToccate(altro), 1);
|
|
const cancella = await rest(`eventi_app?id=eq.${PREFISSO}-da-cancellare`, tokenAllenatore, {
|
|
method: "DELETE",
|
|
headers: { Prefer: "return=representation" },
|
|
});
|
|
assert.equal(await righeToccate(cancella), 1, "evento eliminato");
|
|
});
|
|
|
|
await prova("il giocatore continua a non gestire gli eventi", async () => {
|
|
const res = await rest(`eventi_app?id=eq.${EVENTO}`, tokenGiocatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ luogo: "Abusivo" }),
|
|
});
|
|
assert.equal(await righeToccate(res), 0);
|
|
});
|
|
|
|
await prova("l'allenatore cambia il proprio nome e cognome", async () => {
|
|
const res = await rest(`giocatori_squadra?id=eq.${SLOT}`, tokenAllenatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ nome: "Carlotta", cognome: "Neri Bianchi" }),
|
|
});
|
|
assert.equal(await righeToccate(res), 1, `nome aggiornato (${res.status})`);
|
|
});
|
|
|
|
await prova("l'allenatore non cambia il proprio tipo né gli altri campi admin", async () => {
|
|
for (const campi of [{ tipo: "giocatore" }, { numero: 5 }, { email: "altra@example.test" }]) {
|
|
const res = await rest(`giocatori_squadra?id=eq.${SLOT}`, tokenAllenatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify(campi),
|
|
});
|
|
assert.equal(await righeToccate(res), 0, `rifiutato: ${JSON.stringify(campi)}`);
|
|
}
|
|
assert.deepEqual(await ruoliDi(idAllenatore), ["allenatore"], "il ruolo resta");
|
|
});
|
|
|
|
await prova("l'allenatore non scrive lo slot di un giocatore", async () => {
|
|
const res = await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenAllenatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ nome: "Abusivo" }),
|
|
});
|
|
assert.equal(await righeToccate(res), 0);
|
|
});
|
|
|
|
await prova("il giocatore non cambia il proprio nome", async () => {
|
|
const res = await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenGiocatore, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ nome: "Abusivo" }),
|
|
});
|
|
assert.equal(await righeToccate(res), 0, "per chi gioca nome e cognome restano all'admin");
|
|
});
|
|
|
|
await prova("l'allenatore non risponde alle presenze né dichiara cacche", async () => {
|
|
const presenza = await rest("risposte_presenze", tokenAllenatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: SLOT, stato: "presente" }),
|
|
});
|
|
assert.ok(!presenza.ok, `presenza rifiutata (${presenza.status})`);
|
|
|
|
const cacche = await rest("cacche_partita", tokenAllenatore, {
|
|
method: "POST",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: SLOT, quantita: 1 }),
|
|
});
|
|
assert.ok(!cacche.ok, `cacche rifiutate (${cacche.status})`);
|
|
});
|
|
|
|
await prova("l'allenatore non vota e non viene votato", async () => {
|
|
// Evento con `convocati` vuoto: tutta la rosa, ma l'allenatore resta fuori comunque.
|
|
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ convocati: [] }),
|
|
});
|
|
const vota = await rest("mvp_voti", tokenAllenatore, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
match_id: EVENTO,
|
|
votante_id: SLOT,
|
|
votato_id: GIOCATORE,
|
|
votato_nome: "Tre",
|
|
}),
|
|
});
|
|
assert.ok(!vota.ok, `voto dell'allenatore rifiutato (${vota.status})`);
|
|
|
|
const votato = await rest("mvp_voti", tokenGiocatore, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
match_id: EVENTO,
|
|
votante_id: GIOCATORE,
|
|
votato_id: SLOT,
|
|
votato_nome: "Allenatore",
|
|
}),
|
|
});
|
|
assert.ok(!votato.ok, `l'allenatore non si vota (${votato.status})`);
|
|
});
|
|
|
|
await prova(
|
|
"i promemoria evento non arrivano all'allenatore, convocato o no (DD-039)",
|
|
async () => {
|
|
const destinatari = async () => {
|
|
const res = await rest("rpc/giocatori_destinatari_evento", SERVIZIO, {
|
|
method: "POST",
|
|
body: JSON.stringify({ p_evento_id: EVENTO }),
|
|
});
|
|
assert.equal(res.status, 200);
|
|
return ((await res.json()) as Array<string | Record<string, string>>).map((d) =>
|
|
typeof d === "string" ? d : Object.values(d)[0],
|
|
);
|
|
};
|
|
|
|
// Con i convocati: solo quelli, allenatore escluso anche se attivo.
|
|
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ convocati: [GIOCATORE] }),
|
|
});
|
|
assert.deepEqual(await destinatari(), [GIOCATORE]);
|
|
|
|
// Senza convocati (tutta la rosa): i giocatori attivi, mai l'allenatore.
|
|
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ convocati: [] }),
|
|
});
|
|
const tutti = await destinatari();
|
|
assert.ok(tutti.includes(GIOCATORE), "il giocatore attivo è tra i destinatari");
|
|
assert.ok(!tutti.includes(SLOT), "l'allenatore non è tra i destinatari");
|
|
},
|
|
);
|
|
|
|
await prova("scollegare l'account toglie il ruolo", async () => {
|
|
const res = await rest(`giocatori_squadra?id=eq.${SLOT}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
headers: { Prefer: "return=representation" },
|
|
body: JSON.stringify({ auth_user_id: null }),
|
|
});
|
|
assert.equal(await righeToccate(res), 1);
|
|
assert.deepEqual(await ruoliDi(idAllenatore), []);
|
|
const crea = await rest("eventi_app", tokenAllenatore, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
id: `${PREFISSO}-dopo`,
|
|
tipo: "allenamento",
|
|
titolo: "Non deve esistere",
|
|
data: "2026-01-03",
|
|
ora: "20:00",
|
|
luogo: "",
|
|
}),
|
|
});
|
|
assert.ok(!crea.ok, `senza ruolo niente eventi (${crea.status})`);
|
|
});
|
|
} finally {
|
|
await rest(`mvp_voti?match_id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
|
await rest(`eventi_app?id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
|
await rest(`giocatori_squadra?id=in.(${SLOT},g9002)`, SERVIZIO, { method: "DELETE" });
|
|
if (tokenAdmin) {
|
|
await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenAdmin, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ auth_user_id: null }),
|
|
});
|
|
}
|
|
for (const id of idUtenti) {
|
|
await rest(`user_roles?user_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
|
await eliminaUtente(id);
|
|
}
|
|
riepilogo("permessi-allenatore");
|
|
}
|
|
}
|