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:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Schema e permessi del Profilo Giocatore: `bun test/integration/schema-profili.test.ts`.
|
||||
*
|
||||
* Verifica contro un database vero (locale con `npx supabase start`, oppure quello
|
||||
* configurato in `.env`) le tre cose che il codice dà per scontate: le colonne della
|
||||
* tabella, la chiusura verso l'utente anonimo e il bucket privato.
|
||||
*
|
||||
* Salta con un motivo esplicito quando mancano le credenziali o quando le migration
|
||||
* M2/M3 non sono ancora applicate a quel database: sono stati dell'ambiente, non difetti.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { COLONNE_PROFILO } from "@/lib/profili-core";
|
||||
import { envDaFile } from "../helpers/server";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const env = { ...envDaFile(), ...process.env };
|
||||
const URL_BASE = env["SUPABASE_URL"];
|
||||
const CHIAVE_SERVIZIO = env["SUPABASE_SERVICE_ROLE_KEY"];
|
||||
const CHIAVE_PUBBLICA = env["SUPABASE_PUBLISHABLE_KEY"];
|
||||
|
||||
/** Le chiavi nuove sono opache e viaggiano solo in `apikey`; quelle legacy sono JWT. */
|
||||
function intestazioni(chiave: string): Record<string, string> {
|
||||
const base: Record<string, string> = { apikey: chiave, "content-type": "application/json" };
|
||||
if (chiave.startsWith("eyJ")) base["Authorization"] = `Bearer ${chiave}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
const rest = (percorso: string, chiave: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: { ...intestazioni(chiave), ...(init?.headers ?? {}) },
|
||||
});
|
||||
|
||||
const bucketProfili = (chiave: string) =>
|
||||
fetch(`${URL_BASE}/storage/v1/bucket/profili-giocatore`, { headers: intestazioni(chiave) });
|
||||
|
||||
console.log(`schema profili su ${URL_BASE ?? "(non configurato)"}`);
|
||||
|
||||
if (!URL_BASE || !CHIAVE_SERVIZIO || !CHIAVE_PUBBLICA) {
|
||||
salta("schema e permessi dei profili", "credenziali Supabase non configurate");
|
||||
riepilogo("schema profili");
|
||||
} else {
|
||||
try {
|
||||
// --- M1: anagrafica della squadra ------------------------------------------
|
||||
await prova("la rosa di M1 è popolata e collegabile agli account", async () => {
|
||||
const res = await rest(
|
||||
"giocatori_squadra?select=id,nome,cognome,auth_user_id,attivo",
|
||||
CHIAVE_SERVIZIO,
|
||||
);
|
||||
assert.equal(res.status, 200);
|
||||
const righe = (await res.json()) as Array<{ id: string }>;
|
||||
assert.ok(righe.length >= 17, `attesi almeno 17 giocatori, trovati ${righe.length}`);
|
||||
assert.ok(
|
||||
righe.every((r) => /^g[0-9]+$/.test(r.id)),
|
||||
"gli ID restano nel formato g1..gN (DD-012)",
|
||||
);
|
||||
});
|
||||
|
||||
// Attenzione al falso verde: su un UPDATE che non tocca nessuna riga PostgREST
|
||||
// risponde comunque 2xx. Quello che conta è che il dato non cambi.
|
||||
await prova("un anonimo non si collega a uno slot della rosa", async () => {
|
||||
const res = await rest("giocatori_squadra?id=eq.g1", CHIAVE_PUBBLICA, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ auth_user_id: "00000000-0000-0000-0000-000000000000" }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const aggiornate = (await res.json()) as unknown[];
|
||||
assert.equal(aggiornate.length, 0, "nessuna riga aggiornata senza sessione");
|
||||
}
|
||||
const dopo = await rest("giocatori_squadra?id=eq.g1&select=auth_user_id", CHIAVE_SERVIZIO);
|
||||
const righe = (await dopo.json()) as Array<{ auth_user_id: string | null }>;
|
||||
assert.equal(righe[0]?.auth_user_id ?? null, null, "lo slot g1 è rimasto libero");
|
||||
});
|
||||
|
||||
// --- M2: tabella dei profili -----------------------------------------------
|
||||
const m2 = await rest("profili_giocatore?select=giocatore_id&limit=1", CHIAVE_SERVIZIO);
|
||||
if (!m2.ok) {
|
||||
salta("schema e permessi di profili_giocatore", "M2 non applicata (npx supabase db push)");
|
||||
} else {
|
||||
await prova("profili_giocatore espone tutte le colonne che il codice legge", async () => {
|
||||
const colonne = COLONNE_PROFILO.replace(/\s/g, "");
|
||||
const res = await rest(`profili_giocatore?select=${colonne}&limit=1`, CHIAVE_SERVIZIO);
|
||||
const corpo = await res.text();
|
||||
assert.equal(res.status, 200, corpo);
|
||||
assert.ok(Array.isArray(JSON.parse(corpo)));
|
||||
});
|
||||
|
||||
await prova("il documento ha due facciate separate", async () => {
|
||||
const res = await rest(
|
||||
"profili_giocatore?select=documento_fronte_path,documento_retro_path&limit=1",
|
||||
CHIAVE_SERVIZIO,
|
||||
);
|
||||
assert.equal(res.status, 200, "fronte e retro sono colonne distinte");
|
||||
});
|
||||
|
||||
// Il punto della RLS: senza sessione non si legge e non si scrive. È la garanzia su
|
||||
// cui si regge tutto il modulo, e a nessuno serve fidarsi della prosa.
|
||||
await prova("un anonimo non legge i profili", async () => {
|
||||
const res = await rest("profili_giocatore?select=giocatore_id", CHIAVE_PUBBLICA);
|
||||
if (res.ok) {
|
||||
const righe = (await res.json()) as unknown[];
|
||||
assert.equal(righe.length, 0, "nessuna riga visibile senza sessione");
|
||||
} else {
|
||||
assert.ok(res.status >= 400, `accesso negato (${res.status})`);
|
||||
}
|
||||
});
|
||||
|
||||
await prova("un anonimo non scrive i profili", async () => {
|
||||
const res = await rest("profili_giocatore", CHIAVE_PUBBLICA, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ giocatore_id: "g1", telefono: "000" }),
|
||||
});
|
||||
assert.ok(!res.ok, `la scrittura anonima deve fallire, invece ha risposto ${res.status}`);
|
||||
});
|
||||
}
|
||||
|
||||
// --- M3: bucket privato ------------------------------------------------------
|
||||
const m3 = await bucketProfili(CHIAVE_SERVIZIO);
|
||||
if (!m3.ok) {
|
||||
salta("bucket dei documenti", "M3 non applicata (npx supabase db push)");
|
||||
} else {
|
||||
await prova("il bucket dei documenti è privato", async () => {
|
||||
const bucket = (await m3.json()) as { public?: boolean };
|
||||
assert.equal(bucket.public, false, "documenti e certificati non sono mai pubblici");
|
||||
});
|
||||
|
||||
await prova("un anonimo non scarica i file dei profili", async () => {
|
||||
const res = await fetch(
|
||||
`${URL_BASE}/storage/v1/object/profili-giocatore/g1/certificato.pdf`,
|
||||
{ headers: intestazioni(CHIAVE_PUBBLICA) },
|
||||
);
|
||||
assert.ok(!res.ok, `nessun accesso anonimo allo storage (${res.status})`);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
riepilogo("schema profili");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user