8c980750c0
Il revisore ha dimostrato che il test anti-giunzione si aggira senza malizia:
tre file (uno riesporta la connessione a identity, uno quella a longevity, un
terzo le importa da entrambi e le usa insieme) e nessuno dei tre contiene
singolarmente le due parole cercate. Un controllo testuale file-per-file non
puo' vedere una giunzione che attraversa piu' file - quindi la risposta non
e' un test piu' furbo, e' rendere la cosa impossibile invece di sorvegliarla.
- db.ts: rimossa l'esportazione di getIdentityDb. Resta solo getLongevityDb
(il database senza nomi, puo' usarlo chiunque) e createIdentityDb (serve ai
test per aprire database in memoria - li' la giunzione e' voluta ed
evidente, il problema era il singleton condiviso, non la fabbrica).
- anagrafica.ts: getIdentityDb torna dentro, privata (non esportata) e usata
solo da codicePerUtenteLoggato. Verificato che il bypass a tre file del
revisore ora fallisce gia' a tsc --noEmit ('has no exported member named
getIdentityDb'), non solo a un test che lo sorveglia.
- tests/longevity/anagrafica.test.ts: il test anti-giunzione resta come rete
secondaria, con un commento che dichiara onestamente cosa non vede (una
giunzione via re-export su piu' file) invece di promettere una garanzia che
non mantiene.
Due correzioni minori dalla stessa revisione:
- api/longevity/questionario.ts: salvaDalForm (via salvaCompilazione) lancia
se una risposta porta un test_id sconosciuto al registro; ora e' avvolta in
try/catch e risponde 400 pulito invece di un 500 generico, come gia' fa
src/pages/api/contact.ts per l'invio email.
- components/longevity/Questionario.tsx: un campo con tipo_valore='txt' nel
registro ora rende un input testuale (non piu' sempre "number"), e in
invio i campi testuali non vengono convertiti in NaN e scartati in
silenzio - restano fuori esplicitamente, con un commento che dice perche'.
132 lines
6.7 KiB
TypeScript
132 lines
6.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { createLongevityDb, createIdentityDb } from '../../src/lib/longevity/db';
|
|
import { creaCliente, codicePerUtente, etaAllaData } from '../../src/lib/longevity/anagrafica';
|
|
|
|
describe('anagrafica pseudonimizzata', () => {
|
|
it('il nome sta in identity, il sesso in longevity, e i due non si mescolano', () => {
|
|
const id = createIdentityDb(':memory:');
|
|
const lg = createLongevityDb(':memory:');
|
|
const code = creaCliente(id, lg, { nome: 'Mario', cognome: 'Rossi', sesso: 'M', data_nascita: '1988-03-04' });
|
|
|
|
const inIdentity = id.prepare(`SELECT nome, cognome FROM clienti WHERE client_code = ?`).get(code) as
|
|
{ nome: string; cognome: string };
|
|
expect(inIdentity.nome).toBe('Mario');
|
|
|
|
const inLongevity = lg.prepare(`SELECT sesso FROM soggetti WHERE client_code = ?`).get(code) as { sesso: string };
|
|
expect(inLongevity.sesso).toBe('M');
|
|
|
|
// in longevity non deve esistere nessuna colonna che contenga il nome
|
|
const dump = JSON.stringify(lg.prepare(`SELECT * FROM soggetti`).all());
|
|
expect(dump).not.toContain('Mario');
|
|
expect(dump).not.toContain('Rossi');
|
|
expect(dump).not.toContain('1988-03-04');
|
|
});
|
|
|
|
it('assegna codici progressivi e non riusa i vecchi', () => {
|
|
const id = createIdentityDb(':memory:');
|
|
const lg = createLongevityDb(':memory:');
|
|
const a = creaCliente(id, lg, { nome: 'A', cognome: 'A', sesso: 'F' });
|
|
const b = creaCliente(id, lg, { nome: 'B', cognome: 'B', sesso: 'M' });
|
|
expect(a).toMatch(/^ISL-\d{4}$/);
|
|
expect(b).not.toBe(a);
|
|
});
|
|
|
|
it('ritrova il codice a partire dall utente del sito', () => {
|
|
const id = createIdentityDb(':memory:');
|
|
const lg = createLongevityDb(':memory:');
|
|
const code = creaCliente(id, lg, { nome: 'C', cognome: 'C', sesso: 'F', user_id: 42 });
|
|
expect(codicePerUtente(id, 42)).toBe(code);
|
|
expect(codicePerUtente(id, 99)).toBeNull();
|
|
});
|
|
|
|
it('calcola l eta alla data della sessione', () => {
|
|
expect(etaAllaData('1988-03-04', '2026-08-21')).toBe(38);
|
|
expect(etaAllaData('1988-12-31', '2026-08-21')).toBe(37); // compleanno non ancora passato
|
|
});
|
|
|
|
it('un codice cancellato dall anagrafica non torna mai disponibile: le misure del vecchio non si attribuiscono al nuovo', () => {
|
|
const id = createIdentityDb(':memory:');
|
|
const lg = createLongevityDb(':memory:');
|
|
const a = creaCliente(id, lg, { nome: 'A', cognome: 'A', sesso: 'F' });
|
|
const b = creaCliente(id, lg, { nome: 'B', cognome: 'B', sesso: 'M' });
|
|
|
|
// Il secondo cliente viene cancellato dall'anagrafica (disdetta, richiesta di
|
|
// cancellazione, errore): le sue misure restano in longevity, pseudonimizzate.
|
|
id.prepare(`DELETE FROM clienti WHERE client_code = ?`).run(b);
|
|
lg.prepare(`INSERT INTO sessioni (client_code, data, tipo) VALUES (?, '2026-08-01', 'checkup')`).run(b);
|
|
|
|
const c = creaCliente(id, lg, { nome: 'C', cognome: 'C', sesso: 'F' });
|
|
|
|
// Il terzo cliente riceve un codice nuovo, mai il codice riciclato del secondo.
|
|
expect(c).not.toBe(b);
|
|
expect(c).not.toBe(a);
|
|
|
|
// Le sessioni del vecchio cliente (b) restano leggibili sotto il suo codice...
|
|
const sessioniDiB = lg.prepare(`SELECT * FROM sessioni WHERE client_code = ?`).all(b);
|
|
expect(sessioniDiB.length).toBe(1);
|
|
|
|
// ...e non risultano in nessun modo attribuite al nuovo cliente (c).
|
|
const sessioniDiC = lg.prepare(`SELECT * FROM sessioni WHERE client_code = ?`).all(c);
|
|
expect(sessioniDiC.length).toBe(0);
|
|
});
|
|
|
|
it('se la scrittura su longevity fallisce, quella gia fatta su identity viene annullata', () => {
|
|
const id = createIdentityDb(':memory:');
|
|
const lg = createLongevityDb(':memory:');
|
|
const primaDelTentativo = (id.prepare(`SELECT COUNT(*) AS n FROM clienti`).get() as { n: number }).n;
|
|
|
|
// 'Z' non è un sesso valido (CHECK IN ('M','F') su longevity.soggetti): la scrittura
|
|
// su longevity fallisce per costruzione, dopo che quella su identity è già avvenuta.
|
|
expect(() =>
|
|
creaCliente(id, lg, { nome: 'Errato', cognome: 'Errato', sesso: 'Z' as unknown as 'M' | 'F' })
|
|
).toThrow();
|
|
|
|
const dopoIlTentativo = (id.prepare(`SELECT COUNT(*) AS n FROM clienti`).get() as { n: number }).n;
|
|
expect(dopoIlTentativo).toBe(primaDelTentativo); // nessun cliente orfano rimasto in identity
|
|
expect(id.prepare(`SELECT * FROM clienti WHERE nome = 'Errato'`).get()).toBeUndefined();
|
|
});
|
|
|
|
it('rete secondaria: nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity', () => {
|
|
// ⚠️ QUESTO TEST NON È LA GARANZIA. La garanzia vera è che getIdentityDb non è più
|
|
// esportata da db.ts (vive privata dentro anagrafica.ts): non esiste, da nessuna
|
|
// parte del codice fuori da lì, un nome da importare per ottenere quella connessione.
|
|
// Un controllo testuale come questo non può bastare da solo — è stato dimostrato:
|
|
// tre file, uno che riesporta la connessione a identity, uno che riesporta quella a
|
|
// longevity, un terzo che importa da entrambi e le usa insieme, e NESSUNO dei tre
|
|
// contiene singolarmente le due parole. Un controllo file-per-file non vede una
|
|
// giunzione che attraversa più file, e non c'è formulazione di questo test che lo
|
|
// possa vedere: la risposta era rendere la cosa impossibile, non sorvegliarla meglio.
|
|
// Questo test resta come rete secondaria — cattura il caso comune (una giunzione
|
|
// dentro un solo file) e protegge contro un domani in cui getIdentityDb tornasse
|
|
// esportata per errore — non come prova che la giunzione non possa esistere.
|
|
// Cerca i due riferimenti letterali "Identity"/"Longevity" (con l'iniziale maiuscola,
|
|
// come compaiono in qualunque accessore camelCase) invece di un elenco chiuso di nomi:
|
|
// un elenco di nomi vietati invecchia (è già successo qui: getIdentityDb/getLongevityDb
|
|
// sono nati dopo la prima versione di questo test e sono passati indisturbati).
|
|
const srcDir = join(process.cwd(), 'src');
|
|
const colpevoli: string[] = [];
|
|
|
|
const files = readdirSync(srcDir, { recursive: true, withFileTypes: false }) as string[];
|
|
for (const f of files) {
|
|
const path = join(srcDir, f);
|
|
|
|
// Salta node_modules e dist
|
|
if (f.includes('node_modules') || f.includes('dist')) continue;
|
|
|
|
// Accetta solo .ts e .astro
|
|
if (!f.endsWith('.ts') && !f.endsWith('.astro')) continue;
|
|
|
|
// Esclude i moduli autorizzati
|
|
if (f === 'lib/longevity/anagrafica.ts' || f === 'lib/longevity/db.ts') continue;
|
|
|
|
const src = readFileSync(path, 'utf8');
|
|
if (src.includes('Identity') && src.includes('Longevity')) {
|
|
colpevoli.push(f);
|
|
}
|
|
}
|
|
expect(colpevoli).toEqual([]);
|
|
});
|
|
});
|