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>
This commit is contained in:
1 parent
127d5cb5b8
commit
5fed105850
6 files changed
+509
-36
No files matched your search
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Data e ora (fuso Europe/Rome) di un istante a `ore` ore da adesso, nel formato di `eventi_app`
|
||||
* (`data` AAAA-MM-GG, `ora` HH:MM). Serve ai test del voto MVP, che dipende dall'orario di inizio.
|
||||
*/
|
||||
export function romaTra(ore: number): { data: string; ora: string } {
|
||||
const p: Record<string, string> = Object.fromEntries(
|
||||
new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Europe/Rome",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
})
|
||||
.formatToParts(new Date(Date.now() + ore * 3_600_000))
|
||||
.map((x) => [x.type, x.value]),
|
||||
);
|
||||
return { data: `${p["year"]}-${p["month"]}-${p["day"]}`, ora: `${p["hour"]}:${p["minute"]}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Una data con ora vuota vale mezzanotte. Ritorna il giorno la cui mezzanotte cade tra 2 e 26 ore
|
||||
* fa (il voto è aperto): oggi se sono passate almeno 2 ore dalla mezzanotte, altrimenti ieri.
|
||||
*/
|
||||
export function giornoSenzaOraAperto(): string {
|
||||
const adesso = romaTra(0);
|
||||
return Number(adesso.ora.slice(0, 2)) >= 2 ? adesso.data : romaTra(-24).data;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeDefs, statoBadge } from "@/lib/badges";
|
||||
import { mvpVintiPerGiocatore, type VotoMvp } from "@/lib/mvp-voti";
|
||||
import { mvpVintiPerGiocatore, type VotoMvp, type VotoMvpPubblico } from "@/lib/mvp-voti";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
@@ -52,11 +52,11 @@ if (!locale) {
|
||||
}
|
||||
|
||||
/** Rilegge esattamente come `useVotiMvp()`. */
|
||||
async function leggiVoti(): Promise<VotoMvp[]> {
|
||||
async function leggiVoti(): Promise<VotoMvpPubblico[]> {
|
||||
const res = await rest(
|
||||
`mvp_voti?match_id=like.${PREFISSO}-*&select=match_id,votante_id,votato_id,votato_nome`,
|
||||
`mvp_voti_anonimi?match_id=like.${PREFISSO}-*&select=match_id,votato_id,votato_nome`,
|
||||
);
|
||||
return (await res.json()) as VotoMvp[];
|
||||
return (await res.json()) as VotoMvpPubblico[];
|
||||
}
|
||||
|
||||
/** Un giocatore azzerato, come in `test/unit/badges.test.ts`. */
|
||||
@@ -68,8 +68,8 @@ if (!locale) {
|
||||
await prova(
|
||||
"il badge MVP passa da bronzo ad argento con vittorie vere lette dal database",
|
||||
async () => {
|
||||
// Il votato "vg1" vince nettamente le partite m1 e m2 (2 voti contro 1), pareggia in
|
||||
// m3 (nessun vincitore) e perde in m4 (0 voti): dopo m1+m2 ha 2 vittorie -> bronzo,
|
||||
// Il votato "vg1" vince nettamente le partite m1 e m2 (2 voti contro 1), in m3 ha un solo
|
||||
// voto (sotto quorum: nessun vincitore) e in m4 nessuno: dopo m1+m2 ha 2 vittorie -> bronzo,
|
||||
// non ancora argento (soglia 3). Il votato "vg2" non vince mai: resta senza badge.
|
||||
await upsert({
|
||||
match_id: `${PREFISSO}-m1`,
|
||||
@@ -107,17 +107,15 @@ if (!locale) {
|
||||
votato_id: "vg1",
|
||||
votato_nome: "Uno",
|
||||
});
|
||||
await upsert({
|
||||
match_id: `${PREFISSO}-m3`,
|
||||
votante_id: "vb",
|
||||
votato_id: "vg2",
|
||||
votato_nome: "Due",
|
||||
});
|
||||
|
||||
const voti = await leggiVoti();
|
||||
const vinti = mvpVintiPerGiocatore(voti);
|
||||
assert.equal(vinti["vg1"], 2, "vg1 vince m1 e m2, pareggia in m3 (non conta)");
|
||||
assert.equal(vinti["vg2"] ?? 0, 0, "vg2 non vince mai nettamente");
|
||||
assert.equal(
|
||||
vinti["vg1"],
|
||||
2,
|
||||
"vg1 vince m1 e m2, m3 ha un solo voto (sotto quorum, non conta)",
|
||||
);
|
||||
assert.equal(vinti["vg2"] ?? 0, 0, "vg2 non vince mai");
|
||||
|
||||
const badgeVg1 = statoBadge(mvpDef, giocatoreAzzerato(vinti["vg1"] ?? 0));
|
||||
assert.equal(badgeVg1.grado, "bronzo", "2 vittorie: soglia bronzo (1) raggiunta");
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* 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";
|
||||
@@ -259,8 +260,34 @@ if (!locale) {
|
||||
// Evento con `convocati` vuoto: tutta la rosa, ma l'allenatore resta fuori comunque.
|
||||
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ convocati: [] }),
|
||||
// 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({
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { giornoSenzaOraAperto, romaTra } from "../helpers/roma";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
@@ -310,12 +311,47 @@ if (!locale) {
|
||||
}),
|
||||
});
|
||||
assert.ok(!falso.ok, `non si vota a nome di un altro (${falso.status})`);
|
||||
});
|
||||
|
||||
// --- Voto MVP: definitivo (M35), solo tra presenti e dopo 2 ore dall'inizio (M36) ----------
|
||||
let contatoreMvp = 0;
|
||||
/** Crea una partita (o altro tipo) con le presenze indicate; ritorna l'id. */
|
||||
async function eventoMvp(
|
||||
quando: { data: string; ora: string },
|
||||
presenze: Record<string, string>,
|
||||
tipo = "partita",
|
||||
): Promise<string> {
|
||||
const id = `${PREFISSO}-mvp-${++contatoreMvp}`;
|
||||
const creato = await rest("eventi_app", tokenAdmin, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ id, tipo, titolo: "Voto MVP", luogo: "Palestra", ...quando }),
|
||||
});
|
||||
assert.equal(await righeToccate(creato), 1, "l'evento di prova si crea");
|
||||
for (const [giocatore_id, stato] of Object.entries(presenze)) {
|
||||
const r = await rest("risposte_presenze", SERVIZIO, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ evento_id: id, giocatore_id, stato }),
|
||||
});
|
||||
assert.ok(r.ok, `presenza di prova ${giocatore_id}: ${r.status}`);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
const votaMvp = (token: string, match_id: string, votante_id: string, votato_id: string) =>
|
||||
rest("mvp_voti", token, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ match_id, votante_id, votato_id, votato_nome: votato_id }),
|
||||
});
|
||||
const passata = romaTra(-3);
|
||||
|
||||
await prova("il voto MVP è uno solo, definitivo e firmato (M35)", async () => {
|
||||
const EVENTO_MVP = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
||||
const mioMvp = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO,
|
||||
match_id: EVENTO_MVP,
|
||||
votante_id: "g1",
|
||||
votato_id: "g5",
|
||||
votato_nome: "Cinque",
|
||||
@@ -323,11 +359,55 @@ if (!locale) {
|
||||
});
|
||||
assert.equal(await righeToccate(mioMvp), 1, "il proprio voto MVP si registra");
|
||||
|
||||
// Il voto MVP è definitivo (M35): né un secondo voto, né una modifica, né la cancellazione.
|
||||
const secondo = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_MVP,
|
||||
votante_id: "g1",
|
||||
votato_id: "g6",
|
||||
votato_nome: "Sei",
|
||||
}),
|
||||
});
|
||||
assert.ok(
|
||||
!secondo.ok,
|
||||
`un secondo voto MVP sulla stessa partita è rifiutato (${secondo.status})`,
|
||||
);
|
||||
const modifica = await rest(
|
||||
`mvp_voti?match_id=eq.${EVENTO_MVP}&votante_id=eq.g1`,
|
||||
tokenGiocatore,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ votato_id: "g6", votato_nome: "Sei" }),
|
||||
},
|
||||
);
|
||||
assert.equal(await righeToccate(modifica), 0, "il proprio voto MVP non si modifica");
|
||||
const cancella = await rest(
|
||||
`mvp_voti?match_id=eq.${EVENTO_MVP}&votante_id=eq.g1`,
|
||||
tokenGiocatore,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { Prefer: "return=representation" },
|
||||
},
|
||||
);
|
||||
assert.equal(await righeToccate(cancella), 0, "il proprio voto MVP non si cancella");
|
||||
const dopo = await rest(
|
||||
`mvp_voti?match_id=eq.${EVENTO_MVP}&votante_id=eq.g1&select=votato_id`,
|
||||
SERVIZIO,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await dopo.json(),
|
||||
[{ votato_id: "g5" }],
|
||||
"il voto è rimasto quello iniziale",
|
||||
);
|
||||
|
||||
const mvp = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO,
|
||||
match_id: EVENTO_MVP,
|
||||
votante_id: "g5",
|
||||
votato_id: "g1",
|
||||
votato_nome: "Uno",
|
||||
@@ -336,6 +416,265 @@ if (!locale) {
|
||||
assert.ok(!mvp.ok, `vale anche per l'MVP (${mvp.status})`);
|
||||
});
|
||||
|
||||
await prova("il voto MVP vale solo tra presenti (M36)", async () => {
|
||||
const q = passata;
|
||||
// Controllo positivo: presente e in ritardo possono votare e farsi votare.
|
||||
const ok = await eventoMvp(q, { g1: "presente", g5: "ritardo" });
|
||||
assert.equal(await righeToccate(await votaMvp(tokenGiocatore, ok, "g1", "g5")), 1);
|
||||
const okRitardo = await eventoMvp(q, { g1: "ritardo", g5: "presente" });
|
||||
assert.equal(await righeToccate(await votaMvp(tokenGiocatore, okRitardo, "g1", "g5")), 1);
|
||||
|
||||
for (const [stato, nome] of [
|
||||
["forse", "forse"],
|
||||
["assente", "assente"],
|
||||
["infortunato", "infortunato"],
|
||||
[null, "senza risposta"],
|
||||
] as const) {
|
||||
const votatoNo = await eventoMvp(q, {
|
||||
g1: "presente",
|
||||
...(stato ? { g5: stato } : {}),
|
||||
});
|
||||
const r = await votaMvp(tokenGiocatore, votatoNo, "g1", "g5");
|
||||
assert.ok(!r.ok, `un votato ${nome} non si può votare (${r.status})`);
|
||||
|
||||
const votanteNo = await eventoMvp(q, {
|
||||
g5: "presente",
|
||||
...(stato ? { g1: stato } : {}),
|
||||
});
|
||||
const r2 = await votaMvp(tokenGiocatore, votanteNo, "g1", "g5");
|
||||
assert.ok(!r2.ok, `un votante ${nome} non può votare (${r2.status})`);
|
||||
}
|
||||
});
|
||||
|
||||
await prova(
|
||||
"il voto MVP apre 2 ore dopo l'inizio e chiude 24 ore dopo (M36, M37)",
|
||||
async () => {
|
||||
const presenti = { g1: "presente", g5: "presente" };
|
||||
const tre = await eventoMvp(romaTra(-3), presenti);
|
||||
assert.equal(
|
||||
await righeToccate(await votaMvp(tokenGiocatore, tre, "g1", "g5")),
|
||||
1,
|
||||
"3 ore dopo",
|
||||
);
|
||||
|
||||
const una = await eventoMvp(romaTra(-1), presenti);
|
||||
const rUna = await votaMvp(tokenGiocatore, una, "g1", "g5");
|
||||
assert.ok(!rUna.ok, `1 ora dopo l'inizio: ancora chiuso (${rUna.status})`);
|
||||
|
||||
const venticinque = await eventoMvp(romaTra(-25), presenti);
|
||||
assert.equal(
|
||||
await righeToccate(await votaMvp(tokenGiocatore, venticinque, "g1", "g5")),
|
||||
1,
|
||||
"25 ore dopo l'inizio: ancora aperto (chiude a 26)",
|
||||
);
|
||||
const ventisette = await eventoMvp(romaTra(-27), presenti);
|
||||
const rChiuso = await votaMvp(tokenGiocatore, ventisette, "g1", "g5");
|
||||
assert.ok(!rChiuso.ok, `27 ore dopo l'inizio: chiuso (${rChiuso.status})`);
|
||||
|
||||
const futura = await eventoMvp(romaTra(48), presenti);
|
||||
const rFutura = await votaMvp(tokenGiocatore, futura, "g1", "g5");
|
||||
assert.ok(!rFutura.ok, `partita non ancora giocata (${rFutura.status})`);
|
||||
|
||||
// Un'ora vuota vale mezzanotte, come nell'app.
|
||||
const senzaOra = await eventoMvp({ data: giornoSenzaOraAperto(), ora: "" }, presenti);
|
||||
assert.equal(
|
||||
await righeToccate(await votaMvp(tokenGiocatore, senzaOra, "g1", "g5")),
|
||||
1,
|
||||
"senza ora vale mezzanotte",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await prova("il voto MVP esiste solo per le partite (M36)", async () => {
|
||||
const presenti = { g1: "presente", g5: "presente" };
|
||||
for (const tipo of ["allenamento", "evento"]) {
|
||||
const e = await eventoMvp(passata, presenti, tipo);
|
||||
const r = await votaMvp(tokenGiocatore, e, "g1", "g5");
|
||||
assert.ok(!r.ok, `nessun MVP per ${tipo} (${r.status})`);
|
||||
}
|
||||
});
|
||||
|
||||
await prova("l'admin può registrare un voto MVP anche fuori da queste regole", async () => {
|
||||
const futura = await eventoMvp(romaTra(48), { g1: "assente" });
|
||||
const r = await votaMvp(tokenAdmin, futura, "g1", "g5");
|
||||
assert.equal(await righeToccate(r), 1, "l'admin corregge/inserisce senza vincoli di M36");
|
||||
const chiusa = await eventoMvp(romaTra(-48), { g1: "assente" });
|
||||
const rc = await votaMvp(tokenAdmin, chiusa, "g1", "g5");
|
||||
assert.equal(await righeToccate(rc), 1, "l'admin può votare anche a votazione chiusa");
|
||||
});
|
||||
|
||||
await prova("il voto MVP è segreto: nessuno legge chi ha votato chi (M37)", async () => {
|
||||
const e = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
||||
assert.equal(await righeToccate(await votaMvp(tokenGiocatore, e, "g1", "g5")), 1);
|
||||
// Un secondo voto di un altro votante (inserito dall'admin) per avere due righe.
|
||||
assert.equal(await righeToccate(await votaMvp(tokenAdmin, e, "g5", "g1")), 1);
|
||||
|
||||
const tabella = await rest(
|
||||
`mvp_voti?match_id=eq.${e}&select=votante_id,votato_id`,
|
||||
tokenGiocatore,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await tabella.json(),
|
||||
[{ votante_id: "g1", votato_id: "g5" }],
|
||||
"dalla tabella un giocatore legge solo il proprio voto",
|
||||
);
|
||||
|
||||
const vista = await rest(`mvp_voti_anonimi?match_id=eq.${e}&select=*`, tokenGiocatore);
|
||||
assert.ok(vista.ok, `la vista dei conteggi si legge (${vista.status})`);
|
||||
const righe = (await vista.json()) as Record<string, string>[];
|
||||
assert.deepEqual(
|
||||
righe.map((r) => r["votato_id"]).sort(),
|
||||
["g1", "g5"],
|
||||
"ci sono tutti i voti",
|
||||
);
|
||||
assert.ok(
|
||||
righe.every((r) => !("votante_id" in r)),
|
||||
"ma la vista non dice chi li ha dati",
|
||||
);
|
||||
|
||||
const anonimo = await rest(`mvp_voti_anonimi?match_id=eq.${e}`, ANON);
|
||||
assert.ok(!anonimo.ok, `senza account non si legge nemmeno la vista (${anonimo.status})`);
|
||||
const anonimoTabella = await rest(`mvp_voti?match_id=eq.${e}`, ANON);
|
||||
const corpo = anonimoTabella.ok ? ((await anonimoTabella.json()) as unknown[]) : [];
|
||||
assert.equal(corpo.length, 0, "né la tabella");
|
||||
|
||||
const admin = await rest(`mvp_voti?match_id=eq.${e}&select=votante_id`, tokenAdmin);
|
||||
assert.equal(
|
||||
((await admin.json()) as unknown[]).length,
|
||||
2,
|
||||
"l'admin vede i voti dalla tabella: gli servono per correggerli",
|
||||
);
|
||||
});
|
||||
|
||||
/** Nome in rosa di uno slot, come lo compone il trigger (nome + cognome, senza spazi in testa). */
|
||||
async function nomeInRosa(id: string): Promise<string> {
|
||||
const r = await rest(`giocatori_squadra?id=eq.${id}&select=nome,cognome`, SERVIZIO);
|
||||
const [g] = (await r.json()) as { nome: string; cognome: string }[];
|
||||
return `${g!.nome} ${g!.cognome}`.trim();
|
||||
}
|
||||
const votoNome = (token: string, match_id: string, votato_id: string, resto: object) =>
|
||||
rest("mvp_voti", token, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ match_id, votante_id: "g1", votato_id, ...resto }),
|
||||
});
|
||||
const nomeRiga = async (res: Response) =>
|
||||
((await res.json()) as { votato_nome: string }[])[0]?.votato_nome;
|
||||
|
||||
await prova("MVP: il nome del votato lo mette il database, non il client (M37)", async () => {
|
||||
const g5 = await nomeInRosa("g5");
|
||||
|
||||
// Nome inventato: ignorato.
|
||||
const falso = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
||||
assert.equal(
|
||||
await nomeRiga(
|
||||
await votoNome(tokenGiocatore, falso, "g5", { votato_nome: "Nome Inventato" }),
|
||||
),
|
||||
g5,
|
||||
);
|
||||
const visto = await rest(
|
||||
`mvp_voti_anonimi?match_id=eq.${falso}&select=votato_nome`,
|
||||
tokenGiocatore,
|
||||
);
|
||||
assert.deepEqual(await visto.json(), [{ votato_nome: g5 }], "e così lo vede la squadra");
|
||||
|
||||
// Nome omesso (la colonna è NOT NULL): lo riempie il trigger, il voto passa.
|
||||
const omesso = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
||||
assert.equal(
|
||||
await nomeRiga(await votoNome(tokenGiocatore, omesso, "g5", {})),
|
||||
g5,
|
||||
"nome omesso",
|
||||
);
|
||||
|
||||
// Nome vuoto, con spazi, con un altro giocatore: sempre quello della rosa.
|
||||
for (const [i, strano] of ["", " ", await nomeInRosa("g1"), "<b>x</b>"].entries()) {
|
||||
const e = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
||||
assert.equal(
|
||||
await nomeRiga(await votoNome(tokenGiocatore, e, "g5", { votato_nome: strano })),
|
||||
g5,
|
||||
`caso ${i}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Nome già corretto: resta lo stesso.
|
||||
const giusto = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
||||
assert.equal(
|
||||
await nomeRiga(await votoNome(tokenGiocatore, giusto, "g5", { votato_nome: g5 })),
|
||||
g5,
|
||||
);
|
||||
});
|
||||
|
||||
await prova(
|
||||
"MVP: l'admin che corregge un voto non può far divergere il nome (M37)",
|
||||
async () => {
|
||||
const [g5, g6] = [await nomeInRosa("g5"), await nomeInRosa("g6")];
|
||||
const e = await eventoMvp(passata, { g1: "presente", g5: "presente", g6: "presente" });
|
||||
await votoNome(tokenGiocatore, e, "g5", {});
|
||||
const filtro = `mvp_voti?match_id=eq.${e}&votante_id=eq.g1`;
|
||||
|
||||
// Cambia solo il nome: torna quello della rosa.
|
||||
const soloNome = await rest(filtro, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ votato_nome: "Falso" }),
|
||||
});
|
||||
assert.equal(await nomeRiga(soloNome), g5, "nome riscritto da solo");
|
||||
|
||||
// Cambia il votato: il nome lo segue.
|
||||
const altro = await rest(filtro, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ votato_id: "g6", votato_nome: "Falso" }),
|
||||
});
|
||||
assert.equal(await nomeRiga(altro), g6, "nome che segue il nuovo votato");
|
||||
},
|
||||
);
|
||||
|
||||
await prova("MVP: un id fuori rosa conserva il nome ricevuto (M37)", async () => {
|
||||
// Solo la service role (come i test sui badge) può scrivere id che non sono in rosa.
|
||||
const e = `${PREFISSO}-mvp-fuori-rosa`;
|
||||
const r = await rest("mvp_voti", SERVIZIO, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: e,
|
||||
votante_id: "xx1",
|
||||
votato_id: "xx2",
|
||||
votato_nome: "Ospite",
|
||||
}),
|
||||
});
|
||||
assert.equal(await nomeRiga(r), "Ospite", "niente da sostituire: resta il nome ricevuto");
|
||||
});
|
||||
|
||||
await prova("badge social: il nome del votato lo mette il database (M38)", async () => {
|
||||
const g5 = await nomeInRosa("g5");
|
||||
const e = await eventoMvp(passata, { g1: "presente", g5: "presente" });
|
||||
const base = { match_id: e, categoria: "sorriso", votante_id: "g1", votato_id: "g5" };
|
||||
const post = (resto: object) =>
|
||||
rest("badge_social_voti?on_conflict=match_id,categoria,votante_id", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({ ...base, ...resto }),
|
||||
});
|
||||
assert.equal(
|
||||
await nomeRiga(await post({ votato_nome: "Nome Inventato" })),
|
||||
g5,
|
||||
"nome inventato",
|
||||
);
|
||||
// Stesso voto rinviato con un altro nome (upsert): il nome resta quello della rosa.
|
||||
assert.equal(
|
||||
await nomeRiga(await post({ votato_nome: "Un Altro" })),
|
||||
g5,
|
||||
"upsert con altro nome",
|
||||
);
|
||||
const omesso = await rest("badge_social_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ ...base, categoria: "meme" }),
|
||||
});
|
||||
assert.equal(await nomeRiga(omesso), g5, "nome omesso");
|
||||
});
|
||||
|
||||
await prova("le cacche le dichiara il diretto interessato", async () => {
|
||||
const mie = await rest("cacche_partita", tokenGiocatore, {
|
||||
method: "POST",
|
||||
@@ -455,8 +794,8 @@ if (!locale) {
|
||||
id: EVENTO_CON_G1,
|
||||
tipo: "partita",
|
||||
titolo: "Partita con g1 convocato, g2 no",
|
||||
data: "2026-01-02",
|
||||
ora: "20:00",
|
||||
data: romaTra(-3).data,
|
||||
ora: romaTra(-3).ora,
|
||||
luogo: "Palestra",
|
||||
convocati: ["g1", "g5"],
|
||||
}),
|
||||
@@ -482,9 +821,17 @@ if (!locale) {
|
||||
// Controllo positivo sullo stesso evento: g1 è convocato e vota g5, anche lui
|
||||
// convocato — senza questo, il test sopra potrebbe fallire per un altro motivo
|
||||
// (es. un evento inesistente) e sembrare comunque corretto.
|
||||
// M36: il voto MVP richiede che votante e votato siano presenti.
|
||||
for (const g of ["g1", "g5"]) {
|
||||
await rest("risposte_presenze", SERVIZIO, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates" },
|
||||
body: JSON.stringify({ evento_id: EVENTO_CON_G1, giocatore_id: g, stato: "presente" }),
|
||||
});
|
||||
}
|
||||
const votoValido = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_CON_G1,
|
||||
votante_id: "g1",
|
||||
@@ -503,8 +850,8 @@ if (!locale) {
|
||||
id: EVENTO_CHIUSO,
|
||||
tipo: "partita",
|
||||
titolo: "Partita con pagelle chiuse",
|
||||
data: "2026-01-03",
|
||||
ora: "20:00",
|
||||
data: romaTra(-3).data,
|
||||
ora: romaTra(-3).ora,
|
||||
luogo: "Palestra",
|
||||
pagelle_chiuse: true,
|
||||
}),
|
||||
@@ -529,9 +876,17 @@ if (!locale) {
|
||||
// Il flag riguarda solo le pagelle: MVP e badge social non hanno un concetto di
|
||||
// "chiusura" (mvp.md lo segnala esplicitamente come limite noto), quindi restano
|
||||
// votabili sullo stesso evento.
|
||||
// M36: il voto MVP richiede che votante e votato siano presenti.
|
||||
for (const g of ["g1", "g5"]) {
|
||||
await rest("risposte_presenze", SERVIZIO, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates" },
|
||||
body: JSON.stringify({ evento_id: EVENTO_CHIUSO, giocatore_id: g, stato: "presente" }),
|
||||
});
|
||||
}
|
||||
const mvpAncoraAperto = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_CHIUSO,
|
||||
votante_id: "g1",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeSegreti, statoBadge, VOTI_MINIMI_PAGELLA } from "@/lib/badges";
|
||||
import { mvpVintiPerGiocatore, type VotoMvp } from "@/lib/mvp-voti";
|
||||
import { mvpVintiPerGiocatore, type VotoMvp, type VotoMvpPubblico } from "@/lib/mvp-voti";
|
||||
import { mediePagelle, type VotoPagella } from "@/lib/pagelle";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
@@ -67,11 +67,11 @@ if (!locale) {
|
||||
if (!res.ok) throw new Error(`upsert su pagelle_voti: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function leggiMvp(): Promise<VotoMvp[]> {
|
||||
async function leggiMvp(): Promise<VotoMvpPubblico[]> {
|
||||
const res = await rest(
|
||||
`mvp_voti?match_id=like.${PREFISSO}-*&select=match_id,votante_id,votato_id,votato_nome`,
|
||||
`mvp_voti_anonimi?match_id=like.${PREFISSO}-*&select=match_id,votato_id,votato_nome`,
|
||||
);
|
||||
return (await res.json()) as VotoMvp[];
|
||||
return (await res.json()) as VotoMvpPubblico[];
|
||||
}
|
||||
|
||||
async function leggiPagelle(): Promise<VotoPagella[]> {
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
/** Check dei voti MVP: `bun test/unit/mvp-voti.test.ts`. */
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
conteggioPartita,
|
||||
elencoNomi,
|
||||
mioVoto,
|
||||
mvpPartita,
|
||||
mvpVintiPerGiocatore,
|
||||
DURATA_VOTO_MVP_ORE,
|
||||
ORE_ATTESA_MVP,
|
||||
vincitoriMvp,
|
||||
votoMvpAperto,
|
||||
votoMvpChiuso,
|
||||
votoMvpInCorso,
|
||||
VOTI_MINIMI_MVP,
|
||||
type VotoMvp,
|
||||
} from "@/lib/mvp-voti";
|
||||
@@ -44,9 +51,13 @@ assert.deepEqual(
|
||||
["Anna", "Zeno"],
|
||||
);
|
||||
|
||||
// --- vincitoriMvp: la parità non assegna nessun MVP --------------------------
|
||||
// --- vincitoriMvp: la parità in testa assegna l'MVP a tutti i pari merito --------------------------
|
||||
assert.deepEqual(vincitoriMvp(partita), { m1: "Bruno", m2: "Anna" });
|
||||
assert.deepEqual(vincitoriMvp(pari), {}, "due voti pari: MVP non assegnato");
|
||||
assert.deepEqual(
|
||||
vincitoriMvp(pari),
|
||||
{ m3: "Anna e Zeno" },
|
||||
"due voti pari in testa: sono MVP entrambi, a pari merito",
|
||||
);
|
||||
assert.deepEqual(vincitoriMvp([]), {});
|
||||
|
||||
// --- vincitoriMvp: quorum minimo di voti -------------------------------------
|
||||
@@ -69,13 +80,38 @@ const triplaPari = [
|
||||
v("m6", "g2", "g8", "Anna"),
|
||||
v("m6", "g3", "g7", "Bea"),
|
||||
];
|
||||
assert.deepEqual(vincitoriMvp(triplaPari), {}, "primo e secondo pari: nessun MVP anche a 3 vie");
|
||||
assert.deepEqual(
|
||||
vincitoriMvp(triplaPari),
|
||||
{ m6: "Anna, Bea e Zeno" },
|
||||
"tre pari in testa: tre MVP, in ordine alfabetico",
|
||||
);
|
||||
assert.deepEqual(
|
||||
mvpVintiPerGiocatore(triplaPari),
|
||||
{},
|
||||
"stessa parità: nessuna vittoria netta da contare",
|
||||
{ g9: 1, g8: 1, g7: 1 },
|
||||
"ognuno dei pari merito conta un titolo",
|
||||
);
|
||||
|
||||
// Primo pari col secondo, terzo staccato: il terzo resta fuori.
|
||||
const duePariUnoSotto = [
|
||||
v("m7", "g1", "g9", "Zeno"),
|
||||
v("m7", "g2", "g9", "Zeno"),
|
||||
v("m7", "g3", "g8", "Anna"),
|
||||
v("m7", "g4", "g8", "Anna"),
|
||||
v("m7", "g5", "g7", "Bea"),
|
||||
];
|
||||
assert.deepEqual(
|
||||
mvpPartita(duePariUnoSotto, "m7").map((c) => c.nome),
|
||||
["Anna", "Zeno"],
|
||||
);
|
||||
assert.deepEqual(mvpPartita([v("m8", "g1", "g2", "Solo")], "m8"), [], "sotto quorum: nessuno");
|
||||
assert.deepEqual(mvpPartita([], "m8"), []);
|
||||
|
||||
// --- elencoNomi ---
|
||||
assert.equal(elencoNomi([]), "");
|
||||
assert.equal(elencoNomi(["Anna"]), "Anna");
|
||||
assert.equal(elencoNomi(["Anna", "Bruno"]), "Anna e Bruno");
|
||||
assert.equal(elencoNomi(["Anna", "Bruno", "Carla"]), "Anna, Bruno e Carla");
|
||||
|
||||
// --- mioVoto -----------------------------------------------------------------
|
||||
assert.equal(mioVoto(partita, "m1", "g1")?.votato_nome, "Bruno");
|
||||
assert.equal(mioVoto(partita, "m1", "g9"), null, "chi non ha votato non ha voto");
|
||||
@@ -89,8 +125,8 @@ assert.deepEqual(
|
||||
);
|
||||
assert.deepEqual(
|
||||
mvpVintiPerGiocatore([...partita, ...pari]),
|
||||
{ g2: 1, g5: 1 },
|
||||
"la parità non assegna",
|
||||
{ g2: 1, g5: 1, g9: 1, g8: 1 },
|
||||
"m3 è pari: i due pari merito prendono un titolo ciascuno",
|
||||
);
|
||||
assert.deepEqual(
|
||||
mvpVintiPerGiocatore([...partita, v("m5", "g1", "g2", "Bruno"), v("m5", "g3", "g2", "Bruno")]),
|
||||
@@ -114,4 +150,32 @@ assert.equal(votoMvpAperto("2026-09-04", "20:30", alle(0, 1)), true, "partita pa
|
||||
assert.equal(votoMvpAperto("2026-09-05", "", alle(1, 59)), false, "senza ora vale mezzanotte");
|
||||
assert.equal(votoMvpAperto("2026-09-05", "", alle(2)), true);
|
||||
|
||||
// --- votoMvpChiuso: 24 ore dopo l'apertura (2 ore dopo l'inizio) -----------------
|
||||
assert.equal(DURATA_VOTO_MVP_ORE, 24);
|
||||
const giorno = (g: number, o: number, m = 0) => new Date(2026, 8, g, o, m);
|
||||
// partita del 5/9 alle 20:30: apre il 5 alle 22:30, chiude il 6 alle 22:30
|
||||
assert.equal(votoMvpChiuso("2026-09-05", "20:30", giorno(6, 22, 29)), false, "un minuto prima");
|
||||
assert.equal(votoMvpChiuso("2026-09-05", "20:30", giorno(6, 22, 30)), true, "24 ore in punto");
|
||||
assert.equal(votoMvpChiuso("2026-09-05", "20:30", giorno(20, 12)), true, "partita vecchia");
|
||||
assert.equal(votoMvpChiuso("2026-09-05", "20:30", giorno(5, 12)), false, "prima della partita");
|
||||
assert.equal(votoMvpInCorso("2026-09-05", "20:30", giorno(5, 21)), false, "non ancora aperto");
|
||||
assert.equal(votoMvpInCorso("2026-09-05", "20:30", giorno(5, 23)), true, "aperto");
|
||||
assert.equal(votoMvpInCorso("2026-09-05", "20:30", giorno(7, 8)), false, "chiuso");
|
||||
assert.equal(votoMvpChiuso("2026-09-05", "", giorno(6, 1, 59)), false, "senza ora vale mezzanotte");
|
||||
assert.equal(votoMvpChiuso("2026-09-05", "", giorno(6, 2)), true);
|
||||
|
||||
// --- l'app e il database (M36, M37) hanno la stessa finestra -----------------------
|
||||
const m37 = readFileSync(
|
||||
"supabase/migrations/20261026170000_m37_voto_mvp_segreto_e_chiusura.sql",
|
||||
"utf8",
|
||||
);
|
||||
assert.ok(
|
||||
m37.includes(`interval '${ORE_ATTESA_MVP} hours'`),
|
||||
"ORE_ATTESA_MVP e l'attesa di voto_mvp_consentito() devono coincidere",
|
||||
);
|
||||
assert.ok(
|
||||
m37.includes(`interval '${DURATA_VOTO_MVP_ORE} hours'`),
|
||||
"DURATA_VOTO_MVP_ORE e la chiusura di voto_mvp_consentito() devono coincidere",
|
||||
);
|
||||
|
||||
console.log("mvp-voti: ok");
|
||||
Reference in new issue
Block a user