Files
CRAPP/test/integration/permessi-allenatore.test.ts
davideandClaude Sonnet 5.5 5fed105850 Test del voto MVP: regole a database, segretezza, nome dalla rosa, chiusura
Integrazione (permessi, permessi-allenatore, mvp-badge, s-tiebreak-badge), unit (mvp-voti) e helper
delle date di Roma. Con le parità l'MVP vale per tutti i pari merito.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 09:57:35 +01:00

384 lines
15 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 { romaTra } from "../helpers/roma";
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",
// M36-M37: una partita recente, perché il voto sia aperto e il rifiuto dipenda dal ruolo.
body: JSON.stringify({ convocati: [], ...romaTra(-3) }),
});
// M36: perché il rifiuto dipenda solo dal ruolo, tutti sono presenti (lo slot dell'allenatore
// lo segniamo presente a mano: da sé non potrebbe rispondere) e la partita è passata.
for (const g of [GIOCATORE, SLOT, "g5"]) {
await rest("risposte_presenze", SERVIZIO, {
method: "POST",
headers: { Prefer: "resolution=merge-duplicates" },
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: g, stato: "presente" }),
});
}
// Controllo positivo: due giocatori presenti, il voto passa.
const valido = await rest("mvp_voti", tokenGiocatore, {
method: "POST",
headers: { Prefer: "return=representation" },
body: JSON.stringify({
match_id: EVENTO,
votante_id: GIOCATORE,
votato_id: "g5",
votato_nome: "Cinque",
}),
});
assert.equal(
await righeToccate(valido),
1,
`un giocatore vota un giocatore (${valido.status})`,
);
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");
}
}