Test per profili, ruoli e permessi sul database

Copre le parti nuove e una trappola che sarebbe passata inosservata.

- unit: completamento del profilo (30/30/30/10), stato di scadenza dei
  documenti, export CSV, conversione riga <-> modello, anagrafica di squadra.
- unit: risoluzione dei permessi admin, incluso il fatto che con una sessione
  attiva decide il database e la lista di nomi non conta più.
- integration (schema-profili): verifica su un database vero le colonne che il
  codice legge, il bucket privato e la chiusura verso l'utente anonimo.
- e2e: /admin entra nell'elenco delle schermate verificate.

schema-profili tenta scritture da anonimo per dimostrare che la RLS le respinge,
e poi rilegge la riga: su un UPDATE che tocca zero righe PostgREST risponde 2xx,
quindi fidarsi del codice di risposta darebbe un falso verde. Si salta da solo
dove M2/M3 non sono ancora applicate, indicandolo nel motivo.

Verificato: 8/8 contro lo stack locale (che ha M2/M3), 21/21 file con
npm run test:all contro il progetto cloud.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 17:58:12 +02:00
co-authored by Claude Opus 5
parent da51517ffc
commit 255afde48e
5 changed files with 338 additions and 1 deletions
+165
View File
@@ -0,0 +1,165 @@
/** Check dei profili giocatore: `bun test/unit/profili-core.test.ts`. */
import assert from "node:assert/strict";
import {
aRigaProfilo,
completamento,
csvTesseramento,
daRigaProfilo,
sezioniComplete,
statoScadenza,
type Profilo,
} from "@/lib/profili-core";
import {
dividiNome,
rosaFallback,
slotDi,
slotLiberi,
type GiocatoreSquadra,
} from "@/lib/giocatori-squadra";
const vuoto: Profilo = {
giocatoreId: "g1",
dataNascita: null,
luogoNascita: null,
indirizzo: null,
telefono: null,
email: null,
documentoTipo: null,
documentoNumero: null,
documentoRilasciatoDa: null,
documentoEmissione: null,
documentoScadenza: null,
documentoFrontePath: null,
documentoRetroPath: null,
certificatoScadenza: null,
certificatoPath: null,
fotoPath: null,
};
const completo: Profilo = {
...vuoto,
dataNascita: "1995-05-01",
luogoNascita: "Bologna",
indirizzo: "Via Roma 1",
telefono: "3331234567",
email: "ivan@example.com",
documentoTipo: "Carta d'identità",
documentoNumero: "CA12345",
documentoRilasciatoDa: "Comune di Bologna",
documentoEmissione: "2020-01-01",
documentoScadenza: "2030-01-01",
documentoFrontePath: "g1/documento-fronte.jpg",
documentoRetroPath: "g1/documento-retro.jpg",
certificatoScadenza: "2027-06-30",
certificatoPath: "g1/certificato.pdf",
fotoPath: "g1/foto.jpg",
};
// --- completamento -----------------------------------------------------------
assert.equal(completamento(null), 0, "profilo inesistente = 0%");
assert.equal(completamento(vuoto), 0);
assert.equal(completamento(completo), 100, "tutte le sezioni piene = 100%");
assert.equal(completamento({ ...completo, fotoPath: null }), 90, "la foto pesa 10");
assert.equal(completamento({ ...completo, certificatoPath: null }), 70, "il certificato pesa 30");
assert.equal(
completamento({ ...completo, email: null }),
70,
"i dati personali sono completi solo tutti insieme",
);
// I metadati senza file (o viceversa) non contano come sezione completa.
assert.equal(sezioniComplete({ ...completo, certificatoScadenza: null }).certificato, false);
assert.equal(
sezioniComplete({ ...completo, documentoRetroPath: null }).documento,
false,
"il documento vale solo con fronte e retro",
);
assert.equal(sezioniComplete({ ...completo, documentoFrontePath: null }).documento, false);
// --- aRigaProfilo ------------------------------------------------------------
const riga = aRigaProfilo({ ...completo, luogoNascita: " ", telefono: " 333 " });
assert.equal(riga.luogo_nascita, null, "i campi solo-spazi tornano NULL, non stringa vuota");
assert.equal(riga.telefono, "333", "il resto viene ripulito ai bordi");
assert.equal(riga.documento_fronte_path, "g1/documento-fronte.jpg");
assert.deepEqual(
daRigaProfilo(aRigaProfilo(completo)),
completo,
"modello -> riga -> modello non perde niente",
);
// --- statoScadenza -----------------------------------------------------------
const oggi = "2026-08-30";
assert.equal(statoScadenza(null, null, oggi), "mancante");
assert.equal(statoScadenza("2027-01-01", null, oggi), "mancante", "senza file non vale");
assert.equal(statoScadenza(null, "g1/cert.pdf", oggi), "mancante", "senza data non vale");
assert.equal(statoScadenza("2026-08-29", "g1/cert.pdf", oggi), "scaduto");
assert.equal(
statoScadenza("2026-08-30", "g1/cert.pdf", oggi),
"valido",
"scade oggi = ancora valido",
);
assert.equal(statoScadenza("2026-12-31", "g1/cert.pdf", oggi), "valido");
// --- csvTesseramento ---------------------------------------------------------
const squadra: GiocatoreSquadra[] = [
{
id: "g1",
nome: "Ivan",
cognome: "Cacciari",
numero: 23,
ruolo: "Banda",
authUserId: null,
attivo: true,
},
{
id: "g2",
nome: "Anna",
cognome: 'De "Rossi"',
numero: 7,
ruolo: "Libero",
authUserId: "u2",
attivo: true,
},
];
const csv = csvTesseramento(squadra, { g1: completo });
const righe = csv.split("\n");
assert.equal(righe.length, 3, "intestazione + un giocatore per riga");
assert.equal(righe[0]?.split(";").length, 12, "i 12 campi richiesti dal CSI");
assert.match(righe[1] ?? "", /^Ivan;Cacciari;1995-05-01;Bologna/);
assert.match(
righe[2] ?? "",
/^Anna;"De ""Rossi""";;;/,
"chi non ha profilo esce con i campi vuoti",
);
// --- anagrafica squadra ------------------------------------------------------
assert.deepEqual(dividiNome("Ivan Cacciari"), { nome: "Ivan", cognome: "Cacciari" });
assert.deepEqual(
dividiNome("Carlo Di Castelnuovo"),
{ nome: "Carlo", cognome: "Di Castelnuovo" },
"il cognome composto resta intero",
);
assert.deepEqual(dividiNome("Ivan"), { nome: "Ivan", cognome: "" });
assert.equal(slotDi(squadra, null), null, "senza sessione nessuno slot");
assert.equal(slotDi(squadra, "u2")?.id, "g2");
assert.equal(slotDi(squadra, "sconosciuto"), null);
assert.deepEqual(
slotLiberi(squadra).map((g) => g.id),
["g1"],
"uno slot già collegato non è più libero",
);
assert.deepEqual(
slotLiberi([...squadra, { ...squadra[0]!, id: "g3", attivo: false }]).map((g) => g.id),
["g1"],
"i giocatori non attivi restano fuori",
);
const fallback = rosaFallback();
assert.ok(fallback.length > 0, "il fallback da crapp-data non è mai vuoto");
assert.ok(
fallback.every((g) => g.authUserId === null && g.attivo),
"il fallback non può collegare account",
);
console.log("profili-core: ok");
+24
View File
@@ -0,0 +1,24 @@
/** Check dei permessi di amministrazione: `bun test/unit/ruoli.test.ts`. */
import assert from "node:assert/strict";
import { risolviAdmin } from "@/lib/ruoli";
import { adminNomi, giocatori } from "@/lib/crapp-data";
const referente = giocatori.find((g) => adminNomi.includes(g.nome))!;
const chiunque = giocatori.find((g) => !adminNomi.includes(g.nome))!;
// Con una sessione attiva decide il database, e basta: la lista di nomi non conta più.
assert.equal(risolviAdmin(true, chiunque.id), true, "il ruolo nel database concede");
assert.equal(
risolviAdmin(false, referente.id),
false,
"il ruolo nel database nega anche a chi è nella lista dei nomi",
);
assert.equal(risolviAdmin(true, null), true);
// Senza sessione (null) resta il ponte temporaneo sulla lista di nomi.
assert.equal(risolviAdmin(null, referente.id), true);
assert.equal(risolviAdmin(null, chiunque.id), false);
assert.equal(risolviAdmin(null, null), false, "nessuna identità, nessun permesso");
assert.equal(risolviAdmin(null, "gXX"), false, "un id sconosciuto non concede nulla");
console.log("ruoli: ok");