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:
+8
-1
@@ -20,7 +20,7 @@ non può inquinare gli altri.
|
|||||||
| Cartella | Cosa verifica | Serve rete? |
|
| Cartella | Cosa verifica | Serve rete? |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa | No |
|
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa | No |
|
||||||
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input | Sì |
|
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato | Sì |
|
||||||
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
||||||
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
||||||
|
|
||||||
@@ -28,6 +28,13 @@ non può inquinare gli altri.
|
|||||||
|
|
||||||
- **Nessun test scrive sul database.** Integration ed e2e fanno solo letture e
|
- **Nessun test scrive sul database.** Integration ed e2e fanno solo letture e
|
||||||
verifiche di validazione: si possono lanciare anche contro l'ambiente reale.
|
verifiche di validazione: si possono lanciare anche contro l'ambiente reale.
|
||||||
|
L'unica eccezione apparente è `schema-profili`, che *tenta* scritture da utente
|
||||||
|
anonimo proprio per dimostrare che la RLS le respinge, e poi rilegge la riga per
|
||||||
|
verificare che non sia cambiata: su un UPDATE a zero righe PostgREST risponde 2xx,
|
||||||
|
quindi lo stato conta più del codice di risposta.
|
||||||
|
- I test legati alle migration M2/M3 si **saltano da soli** dove quelle migration non
|
||||||
|
sono ancora applicate, indicandolo nel motivo. Per vederli tutti verdi serve un
|
||||||
|
database che le contenga: `npx supabase start` ne crea uno in locale.
|
||||||
- `integration` ed `e2e` avviano da soli il server di sviluppo. Per usarne uno già
|
- `integration` ed `e2e` avviano da soli il server di sviluppo. Per usarne uno già
|
||||||
attivo: `BASE_URL=http://localhost:8080 npm run test:e2e`.
|
attivo: `BASE_URL=http://localhost:8080 npm run test:e2e`.
|
||||||
- Le variabili d'ambiente vengono lette da `.env`; i nomi senza prefisso
|
- Le variabili d'ambiente vengono lette da `.env`; i nomi senza prefisso
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ try {
|
|||||||
["/profilo", /Profilo|CrAPP/],
|
["/profilo", /Profilo|CrAPP/],
|
||||||
["/eventi", /Eventi|CrAPP/],
|
["/eventi", /Eventi|CrAPP/],
|
||||||
["/scout", /Scout|CrAPP/],
|
["/scout", /Scout|CrAPP/],
|
||||||
|
["/admin", /Dashboard|CrAPP/],
|
||||||
];
|
];
|
||||||
for (const [percorso, atteso] of attesi) {
|
for (const [percorso, atteso] of attesi) {
|
||||||
assert.match(titolo(await pagina(percorso)), atteso, `${percorso}: titolo corretto`);
|
assert.match(titolo(await pagina(percorso)), atteso, `${percorso}: titolo corretto`);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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");
|
||||||
Reference in New Issue
Block a user