Files
CRAPP/test/integration/obiettivi.test.ts
T
davideandClaude Sonnet 5 447016f03b Completa la copertura test dell'obiettivo "250 presenze complessive"
C'era solo un confronto circolare (la stessa formula ricalcolata sugli stessi
dati reali) più il caso rosa vuota. Aggiunto un unit test con roster piccolo e
valori noti (5 + 10 + 15 = 30), e un integration test end-to-end che scrive
eventi/risposte veri sul database locale, calcola contaPresenzeGiocatore() (la
stessa funzione pura usata da useRosa() in produzione) sui dati riletti, e
verifica che obiettiviSquadra() sommi correttamente il risultato.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:19:27 +02:00

348 lines
14 KiB
TypeScript

/**
* Obiettivi di squadra con scadenza/mese dinamici ("presenze del mese", "evento di squadra al
* mese" e "tutti rispondono alle convocazioni") end-to-end contro il database locale:
* `bun test/integration/obiettivi.test.ts`.
*
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
* pura, con un `ContestoObiettivi` costruito a mano. Qui invece si scrivono righe vere su
* `eventi_app` e `risposte_presenze`, si rileggono con `leggiEventi()` (la stessa funzione che
* usa l'app lato server) e una query REST equivalente a `fetchPresenze()`, e si verifica che il
* risultato che arriva a `obiettiviSquadra()` sia quello atteso: se una colonna cambia nome o
* la mappatura si rompe, qui il valore torna sbagliato (o NaN) anche se i test unitari restano
* verdi, perché quelli non toccano mai il database.
*
* Il mese di test è marzo 2099: non collide con gli eventi del seed (tutti in agosto 2026), così
* questi due obiettivi — che ora aggregano su TUTTI gli eventi del mese corrente, non solo
* quelli del test — restano prevedibili senza dover filtrare gli eventi letti dal database.
*
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
* usa id con il prefisso `test-obiettivi`, che nessun dato vero può avere.
*/
import assert from "node:assert/strict";
import { giocatori } from "@/lib/crapp-data";
import { obiettiviSquadra } from "@/lib/obiettivi";
import { contaPresenzeGiocatore, type MappaPresenze } from "@/lib/presenze";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
const locale = statoLocale();
if (!locale) {
salta("obiettivi (o1) sul database", "stack locale non attivo (npx supabase start)");
riepilogo("obiettivi");
} else {
const { url: URL_BASE, servizio: SERVIZIO } = locale;
console.log(`obiettivi su ${URL_BASE}`);
// Prima di qualsiasi import dei moduli server: è da qui che nasce `supabaseAdmin`.
process.env["SUPABASE_URL"] = URL_BASE;
process.env["SUPABASE_SERVICE_ROLE_KEY"] = SERVIZIO;
const { leggiEventi } = await import("@/lib/eventi.server");
const PREFISSO = "test-obiettivi";
const MESE_TEST = "2099-03";
const OGGI = new Date("2099-03-15T10:00:00Z");
const rest = (percorso: string, init?: RequestInit) =>
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
...init,
headers: {
apikey: SERVIZIO,
Authorization: `Bearer ${SERVIZIO}`,
"content-type": "application/json",
...(init?.headers ?? {}),
},
});
/** Stessa query di `fetchPresenze()` (`src/lib/presenze.ts`), raggruppata a mano. */
async function leggiPresenze(eventoId: string): Promise<MappaPresenze> {
const res = await rest(
`risposte_presenze?evento_id=eq.${eventoId}&select=evento_id,giocatore_id,stato`,
);
const righe = (await res.json()) as Array<{
evento_id: string;
giocatore_id: string;
stato: string;
}>;
const mappa: MappaPresenze = {};
for (const r of righe) {
(mappa[r.evento_id] ??= {})[r.giocatore_id] = r.stato as never;
}
return mappa;
}
try {
const eventoId = `${PREFISSO}-e1`;
await prova("prepara evento e presenze reali su Supabase locale", async () => {
const inserito = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: eventoId,
tipo: "allenamento",
titolo: "Test obiettivi o1",
data: `${MESE_TEST}-10`,
}),
});
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
// 9 presenti su 17 giocatori del roster reale (`crapp-data.ts`, id g1..g17).
const presenti = giocatori.slice(0, 9).map((g) => g.id);
const righe = giocatori.map((g) => ({
evento_id: eventoId,
giocatore_id: g.id,
stato: presenti.includes(g.id) ? "presente" : "assente",
}));
const inseritePresenze = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) });
if (!inseritePresenze.ok) {
throw new Error(`inserimento presenze fallito: ${await inseritePresenze.text()}`);
}
});
await prova(
"o1 legge l'evento e le presenze vere dal database e calcola la percentuale del mese",
async () => {
const eventiReali = await leggiEventi();
assert.ok(
eventiReali.some((e) => e.id === eventoId),
"leggiEventi() include l'evento appena inserito",
);
const presenzeReali = await leggiPresenze(eventoId);
const obiettivi = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
OGGI,
);
const o1 = obiettivi.find((o) => o.id === "o1")!;
assert.equal(
o1.valore,
Math.round((9 / giocatori.length) * 100),
"9 presenti su tutta la rosa, letti dal database",
);
assert.equal(o1.titolo, "90% di presenze a marzo", "titolo segue il mese iniettato");
assert.equal(o1.scadenza, "2099-03-31", "scadenza = ultimo giorno di marzo");
},
);
await prova("un evento fuori dal mese di test non sposta o1", async () => {
const eventoFuoriMese = `${PREFISSO}-e2`;
const fuori = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: eventoFuoriMese,
tipo: "allenamento",
titolo: "Test obiettivi o1 - fuori mese",
data: "2099-04-01",
}),
});
if (!fuori.ok) throw new Error(`inserimento evento fallito: ${await fuori.text()}`);
try {
const presenzeFuoriMese = giocatori.map((g) => ({
evento_id: eventoFuoriMese,
giocatore_id: g.id,
stato: "presente",
}));
const scritte = await rest("risposte_presenze", {
method: "POST",
body: JSON.stringify(presenzeFuoriMese),
});
if (!scritte.ok) throw new Error(`inserimento presenze fallito: ${await scritte.text()}`);
const eventiReali = await leggiEventi();
const presenzeReali = {
...(await leggiPresenze(eventoId)),
...(await leggiPresenze(eventoFuoriMese)),
};
const o1 = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
OGGI,
).find((o) => o.id === "o1")!;
assert.equal(
o1.valore,
Math.round((9 / giocatori.length) * 100),
"l'evento di aprile non entra nel calcolo di marzo",
);
} finally {
await rest(`eventi_app?id=eq.${eventoFuoriMese}`, { method: "DELETE" });
await rest(`risposte_presenze?evento_id=eq.${eventoFuoriMese}`, { method: "DELETE" });
}
});
await prova(
"o6 legge dal database l'evento sociale del mese, azzerandosi come o1",
async () => {
const pizzataId = `${PREFISSO}-pizzata`;
const inserita = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: pizzataId,
tipo: "evento",
titolo: "Test obiettivi o6",
data: `${MESE_TEST}-20`,
}),
});
if (!inserita.ok) throw new Error(`inserimento evento fallito: ${await inserita.text()}`);
const eventiReali = await leggiEventi();
const o6InMese = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: {}, pagelle: [] },
OGGI,
).find((o) => o.id === "o6")!;
assert.equal(o6InMese.valore, 1, "l'evento sociale di marzo conta letto dal database");
const OGGI_MESE_DOPO = new Date("2099-04-15T10:00:00Z");
const o6MeseDopo = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: {}, pagelle: [] },
OGGI_MESE_DOPO,
).find((o) => o.id === "o6")!;
assert.equal(
o6MeseDopo.valore,
0,
"lo stesso evento non conta più il mese successivo (si azzera)",
);
},
);
await prova(
"o2 legge dal database risposte reali ed esclude i compleanni, con scadenza dinamica",
async () => {
// o2 (percentualeRisposte) aggrega su TUTTI gli eventi non-compleanno, senza filtro di
// mese: per un'asserzione deterministica isoliamo dal risultato reale solo i due eventi
// di questo test, invece di dipendere dal numero di eventi già presenti nel database
// (seed incluso).
const partitaId = `${PREFISSO}-o2-partita`;
const compleannoId = `${PREFISSO}-o2-compleanno`;
const inseritaPartita = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: partitaId,
tipo: "partita",
titolo: "Test obiettivi o2",
data: `${MESE_TEST}-05`,
}),
});
if (!inseritaPartita.ok) {
throw new Error(`inserimento evento fallito: ${await inseritaPartita.text()}`);
}
const inseritoCompleanno = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: compleannoId,
tipo: "compleanno",
titolo: "Test obiettivi o2 - compleanno",
data: `${MESE_TEST}-06`,
}),
});
if (!inseritoCompleanno.ok) {
throw new Error(`inserimento evento fallito: ${await inseritoCompleanno.text()}`);
}
// Solo 12 giocatori su tutta la rosa rispondono alla partita; nessuno "risponde" al
// compleanno, perché non richiede risposta.
const rispondenti = giocatori.slice(0, 12);
const righe = rispondenti.map((g, i) => ({
evento_id: partitaId,
giocatore_id: g.id,
stato: i % 2 === 0 ? "presente" : "forse",
}));
const inseriteRisposte = await rest("risposte_presenze", {
method: "POST",
body: JSON.stringify(righe),
});
if (!inseriteRisposte.ok) {
throw new Error(`inserimento presenze fallito: ${await inseriteRisposte.text()}`);
}
const eventiReali = (await leggiEventi()).filter(
(e) => e.id === partitaId || e.id === compleannoId,
);
const presenzeReali = await leggiPresenze(partitaId);
const o2 = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
OGGI,
).find((o) => o.id === "o2")!;
assert.equal(
o2.valore,
Math.round((12 / giocatori.length) * 100),
"12 risposte reali su un solo evento che le richiede (il compleanno è escluso)",
);
assert.equal(o2.scadenza, "2099-03-31", "scadenza o2 = ultimo giorno del mese iniettato");
},
);
await prova(
"o7 somma presenze calcolate da eventi/risposte reali del database",
async () => {
// o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da
// `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita
// la stessa funzione pura sui dati appena scritti, per verificare l'intera catena
// DB -> contaPresenzeGiocatore -> o7, non solo la somma finale.
const allenamentoId = `${PREFISSO}-o7-allenamento`;
const partitaId = `${PREFISSO}-o7-partita`;
const OGGI_STR = "2099-01-20";
for (const [id, tipo, data] of [
[allenamentoId, "allenamento", "2099-01-05"],
[partitaId, "partita", "2099-01-08"],
] as const) {
const inserito = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }),
});
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
}
// g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze,
// il ritardo conta). g3: assente a entrambi (0 presenze).
const [g1, g2, g3] = giocatori;
const righe = [
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
{ evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
{ evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
{ evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
{ evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
];
const inserite = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) });
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
const eventiReali = (await leggiEventi()).filter(
(e) => e.id === allenamentoId || e.id === partitaId,
);
const presenzeReali = {
...(await leggiPresenze(allenamentoId)),
...(await leggiPresenze(partitaId)),
};
const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({
...g,
presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR),
}));
const o7 = obiettiviSquadra(rosaConPresenzeReali, { eventi: [], presenze: {}, pagelle: [] })
.find((o) => o.id === "o7")!;
assert.equal(
o7.valore,
4,
"g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database",
);
},
);
} finally {
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
riepilogo("obiettivi");
}
}