Copre con un test il backfill di M19 (nascita dei profili preesistenti

M19 era stato applicato in produzione senza copertura automatica, solo
verificato a mano. Il test simula un profilo compilato prima di M18 ed
esegue lo stesso SQL del backfill via psql, perché PostgREST non espone
un update multi-tabella come RPC.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-15 13:22:19 +02:00
1 parent 3f2ca30775
commit cdd433cb15
3 files changed
+142 -1

No files matched your search

+7
View File
@@ -1309,3 +1309,10 @@ allinea una tantum `giocatori_squadra.nascita` a `profili_giocatore.data_nascita
profili già esistenti, con la stessa disattivazione temporanea del trigger di sicurezza usata profili già esistenti, con la stessa disattivazione temporanea del trigger di sicurezza usata
in M18. Da qui in poi il gap non si può ripresentare: ogni scrittura successiva passa dal in M18. Da qui in poi il gap non si può ripresentare: ogni scrittura successiva passa dal
trigger di sync, questo backfill serve solo per lo storico antecedente a M18. trigger di sync, questo backfill serve solo per lo storico antecedente a M18.
A differenza della bonifica eventi orfani (DD-029, M15→M16), qui il backfill non è stato
reso una funzione richiamabile: resta SQL diretto nella migration. La copertura di test
(`test/integration/backfill-nascita-esistenti.test.ts`) verifica comunque lo stesso
comportamento eseguendo lo stesso pattern SQL via `psql` nel container Postgres locale —
scelta necessaria perché senza una funzione RPC, PostgREST non espone un update
multi-tabella come questo.
+1 -1
View File
@@ -26,7 +26,7 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
| 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. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route) | No | | `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route) | No |
| `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; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) e la funzione di bonifica delle righe orfane (`bonifica-evento`, M16) sul database locale | 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; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | 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 | — |
@@ -0,0 +1,134 @@
/**
* Backfill di M19 (`m19_backfill_nascita_da_profili_esistenti`):
* `bun test/integration/backfill-nascita-esistenti.test.ts`.
*
* M18 ha aggiunto `giocatori_squadra.nascita`, sincronizzata da un trigger su
* `profili_giocatore` — ma solo per le scritture *future*. In produzione questo ha lasciato
* scoperti i profili già compilati **prima** di M18: la nascita restava vuota finché
* `m19_backfill_nascita_da_profili_esistenti` non ha rieseguito una tantum lo stesso
* allineamento (vedi DD-031, aggiornamento del 15/09/2026).
*
* A differenza della bonifica eventi orfani (M15→M16, `bonifica_dati_evento_orfani()`),
* questo backfill non è stato reso una funzione richiamabile: qui si verifica lo stesso
* pattern SQL (`UPDATE giocatori_squadra ... FROM profili_giocatore`, con
* `enforce_giocatori_squadra_update` temporaneamente disattivato) eseguendolo via `psql`
* dentro il container Postgres locale — PostgREST non espone un update multi-tabella come
* RPC, quindi qui serve accesso diretto al database, non solo alla REST.
*
* Simula un profilo "storico" (inserito con il trigger di sync disattivato, come se
* esistesse da prima di M18) e verifica che il backfill lo recuperi. Gira solo sullo stack
* locale con Docker attivo: usa lo slot `g9` della rosa seed, ripristinato alla fine.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
const locale = statoLocale();
if (!locale) {
salta("backfill nascita profili esistenti", "stack locale non attivo (npx supabase start)");
riepilogo("backfill-nascita-esistenti");
} else {
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
const projectId = /project_id\s*=\s*"([^"]+)"/.exec(
readFileSync(new URL("../../supabase/config.toml", import.meta.url), "utf8"),
)?.[1];
const container = projectId ? `supabase_db_${projectId}` : null;
const dockerDisponibile =
!!container && spawnSync("docker", ["inspect", container], { stdio: "ignore" }).status === 0;
if (!dockerDisponibile) {
salta("backfill nascita profili esistenti", "container Postgres locale non trovato");
riepilogo("backfill-nascita-esistenti");
} else {
console.log(`backfill nascita profili esistenti su ${URL_BASE} (container ${container})`);
function eseguiSql(sql: string): string {
const esito = spawnSync(
"docker",
["exec", "-i", container!, "psql", "-U", "postgres", "-d", "postgres"],
{
input: sql,
encoding: "utf8",
},
);
if (esito.status !== 0) throw new Error(`psql fallito: ${esito.stderr || esito.stdout}`);
return esito.stdout;
}
const rest = (percorso: string, token: string, init?: RequestInit) =>
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
...init,
headers: {
apikey: token === SERVIZIO ? SERVIZIO : ANON,
Authorization: `Bearer ${token}`,
"content-type": "application/json",
...(init?.headers ?? {}),
},
});
async function leggiNascita(): Promise<string | null> {
const res = await rest("giocatori_squadra?id=eq.g9&select=nascita", SERVIZIO);
const righe = (await res.json()) as Array<{ nascita: string | null }>;
return righe[0]?.nascita ?? null;
}
try {
await prova(
"il backfill recupera la nascita di un profilo compilato prima di M18",
async () => {
// g9 è uno dei 17 storici: il backfill hardcoded di M18 le ha già dato una nascita
// pubblica. Per simulare lo stato "prima di M19" — un profilo compilato ma non
// ancora sincronizzato — va prima azzerata, poi inserito il profilo con il trigger
// di sync disattivato (come se esistesse da prima che M18 lo introducesse).
eseguiSql(`
ALTER TABLE public.giocatori_squadra DISABLE TRIGGER enforce_giocatori_squadra_update;
UPDATE public.giocatori_squadra SET nascita = NULL WHERE id = 'g9';
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
ALTER TABLE public.profili_giocatore DISABLE TRIGGER sincronizza_nascita_pubblica;
INSERT INTO public.profili_giocatore (giocatore_id, data_nascita)
VALUES ('g9', '1988-07-21');
ALTER TABLE public.profili_giocatore ENABLE TRIGGER sincronizza_nascita_pubblica;
`);
assert.equal(
await leggiNascita(),
null,
"prima del backfill la nascita pubblica è ancora vuota",
);
// Stesso corpo esatto di m19_backfill_nascita_da_profili_esistenti.sql.
eseguiSql(`
ALTER TABLE public.giocatori_squadra DISABLE TRIGGER enforce_giocatori_squadra_update;
UPDATE public.giocatori_squadra gs
SET nascita = pg.data_nascita
FROM public.profili_giocatore pg
WHERE pg.giocatore_id = gs.id;
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
`);
assert.equal(
await leggiNascita(),
"1988-07-21",
"dopo il backfill la nascita del profilo preesistente è recuperata",
);
},
);
} finally {
// g9 (Camilla Esposito) è uno dei 17 storici: il backfill di M18 le aveva già dato
// '2003-04-04' (src/lib/crapp-data.ts) prima che questo test la sovrascrivesse — va
// ripristinato quel valore, non svuotato, per non alterare lo stato del seed.
eseguiSql(`
ALTER TABLE public.giocatori_squadra DISABLE TRIGGER enforce_giocatori_squadra_update;
DELETE FROM public.profili_giocatore WHERE giocatore_id = 'g9';
UPDATE public.giocatori_squadra SET nascita = '2003-04-04' WHERE id = 'g9';
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
`);
riepilogo("backfill-nascita-esistenti");
}
}
}