diff --git a/docs/DESIGN_DECISIONS.md b/docs/DESIGN_DECISIONS.md index 139bfc5..91dd055 100644 --- a/docs/DESIGN_DECISIONS.md +++ b/docs/DESIGN_DECISIONS.md @@ -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 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. + +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. diff --git a/test/README.md b/test/README.md index 85ef903..7a730ac 100644 --- a/test/README.md +++ b/test/README.md @@ -26,7 +26,7 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push | 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 | -| `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ì | | `helpers/` | Avvio del server di test e mini-harness condiviso | — | diff --git a/test/integration/backfill-nascita-esistenti.test.ts b/test/integration/backfill-nascita-esistenti.test.ts new file mode 100644 index 0000000..e26178b --- /dev/null +++ b/test/integration/backfill-nascita-esistenti.test.ts @@ -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 { + 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"); + } + } +}