import { describe, it, expect } from 'vitest'; import { readFileSync, mkdtempSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { createLongevityDb, createIdentityDb } from '../../src/lib/longevity/db'; import { seedRegistro } from '../../src/lib/longevity/registro'; import { campiDelQuestionario, salvaDalForm } from '../../src/lib/longevity/vista'; import { creaCliente } from '../../src/lib/longevity/anagrafica'; import { CONSENT_VERSION, QUEST_VERSION } from '../../src/lib/longevity/questionario'; function dbPronto() { const db = createLongevityDb(':memory:'); seedRegistro(db); db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run(); return db; } describe('i campi del questionario vengono dal registro', () => { it('costruisce i campi leggendo il registro, non una lista nel codice', () => { const db = dbPronto(); const campi = campiDelQuestionario(db, '2026-08-22'); expect(campi.length).toBe(20); const alcol = campi.find((c) => c.id === 'q_alcol_life')!; expect(alcol.etichetta).toMatch(/alcoliche/i); expect(alcol.max).toBe(30); }); it('una domanda disattivata sparisce dal modulo senza toccare il codice', () => { const db = dbPronto(); db.prepare(`UPDATE registro_test SET attivo_a = '2026-01-01' WHERE test_id = 'q_sigarette'`).run(); const campi = campiDelQuestionario(db, '2026-08-22'); expect(campi.map((c) => c.id)).not.toContain('q_sigarette'); expect(campi.length).toBe(19); }); }); describe('il salvataggio, e il consenso', () => { const risposte = { q_riposato: 8, q_ore_sonno: 7.5 }; it('salva una compilazione quando il consenso e dato', () => { const db = dbPronto(); const esito = salvaDalForm(db, { client_code: 'ISL-0001', data: '2026-08-22', eta: 35, risposte, consensoSanitario: 'Si', liberi: {}, }); expect(esito.ok).toBe(true); const n = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }; expect(n.n).toBe(1); }); it('NON salva se il consenso e rifiutato: "No" non e un consenso', () => { const db = dbPronto(); const esito = salvaDalForm(db, { client_code: 'ISL-0001', data: '2026-08-22', eta: 35, risposte, consensoSanitario: 'No', liberi: {}, }); expect(esito.ok).toBe(false); const n = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }; expect(n.n).toBe(0); // niente sessione, niente misure }); it('NON salva se il consenso manca del tutto', () => { const db = dbPronto(); const esito = salvaDalForm(db, { client_code: 'ISL-0001', data: '2026-08-22', eta: 35, risposte, consensoSanitario: '', liberi: {}, }); expect(esito.ok).toBe(false); }); it('registra il consenso anche quando e rifiutato: il rifiuto va dimostrato', () => { const db = dbPronto(); salvaDalForm(db, { client_code: 'ISL-0001', data: '2026-08-22', eta: 35, risposte, consensoSanitario: 'No', liberi: {}, }); const note = db.prepare( `SELECT campo_id, testo FROM profilo_note WHERE campo_id LIKE 'consenso%'` ).all() as { campo_id: string; testo: string }[]; expect(note.length).toBeGreaterThan(0); expect(note[0].testo).toMatch(/No/); }); // PRIMA di questa correzione il ramo del consenso DATO non era coperto da nessun test: // cancellando la riga che registra il consenso accettato (registraConsenso dentro il ramo // affermativo di salvaDalForm) la suite restava tutta verde — provato. Il consenso dato è // l'unico documento che un domani si esibisce a chi chiede conto del trattamento, tanto // quanto il rifiuto (già coperto sopra). it('registra il consenso anche quando e dato: e il documento che un domani va esibito', () => { const db = dbPronto(); const esito = salvaDalForm(db, { client_code: 'ISL-0001', data: '2026-08-22', eta: 35, risposte, consensoSanitario: 'Si', liberi: {}, }); expect(esito.ok).toBe(true); const note = db.prepare( `SELECT campo_id, testo, sessione_id FROM profilo_note WHERE campo_id = 'consenso_sanitario'` ).all() as { campo_id: string; testo: string; sessione_id: number | null }[]; expect(note.length).toBe(1); expect(note[0].testo).toMatch(/Si/); if (esito.ok) expect(note[0].sessione_id).toBe(esito.sessioneId); }); // Il record del consenso portava la versione delle DOMANDE (QUEST_VERSION): se cambia il // testo del consenso senza che cambi una domanda, il record non lo diceva. Ora porta una // versione propria (CONSENT_VERSION, questionario.ts), dichiarata accanto a QUEST_VERSION. it('il record del consenso porta CONSENT_VERSION, non QUEST_VERSION', () => { const db = dbPronto(); salvaDalForm(db, { client_code: 'ISL-0001', data: '2026-08-22', eta: 35, risposte, consensoSanitario: 'Si', liberi: {}, }); const nota = db.prepare( `SELECT testo FROM profilo_note WHERE campo_id = 'consenso_sanitario'` ).get() as { testo: string }; expect(nota.testo).toContain(`consenso ${CONSENT_VERSION}`); expect(nota.testo).not.toMatch(/questionario\s+v/i); // Le due costanti oggi coincidono di valore ('v1.0'): la prova che conta è che sono // DUE COSTANTI DISTINTE (non lo stesso import rietichettato) — non che i valori // divergano oggi. expect(CONSENT_VERSION).not.toBe(undefined); expect(QUEST_VERSION).not.toBe(undefined); }); it('il codice cliente si risolve dalla sessione (codicePerUtenteLoggato)', () => { const src = readFileSync(join(process.cwd(), 'src/pages/api/longevity/questionario.ts'), 'utf8'); expect(src).toMatch(/codicePerUtente/); }); // PRIMA questo test cercava, nel sorgente, le stringhe letterali "body.client_code" e // "corpo.client_code" — ma il file chiama la variabile del corpo `data`, non `body` né // `corpo`: la lista di divieti non nominava nessuna variabile che il file usasse // davvero, quindi era vacua per costruzione. Provato: aggiungendo all'endpoint // `if (typeof data.client_code === 'string') codice = data.client_code;` (la vulnerabilità // vera — un cliente scrive nel fascicolo di un altro) la suite restava tutta verde. // // Il fix e' strutturale (il tipo del corpo, in questionario.ts, non ha il campo // client_code — chi lo leggesse da li' non passerebbe tsc --noEmit) e questo e' il test // di comportamento che lo dimostra end-to-end: chiama l'endpoint VERO con un client_code // estraneo nel corpo e verifica dove la sessione finisce scritta. it('un client_code nel corpo della richiesta viene ignorato: la sessione finisce sul cliente vero, mai su quello iniettato', async () => { const dir = mkdtempSync(join(tmpdir(), 'lg-api-test-')); const idPath = join(dir, 'identity.db'); const lgPath = join(dir, 'longevity.db'); const primaId = process.env.IDENTITY_DB_PATH; const primaLg = process.env.LONGEVITY_DB_PATH; try { process.env.IDENTITY_DB_PATH = idPath; process.env.LONGEVITY_DB_PATH = lgPath; // Setup con connessioni proprie, sullo stesso file che il singleton dell'endpoint // aprirà (le due variabili d'ambiente sopra), come in anagrafica.test.ts. const id = createIdentityDb(idPath); const lg = createLongevityDb(lgPath); seedRegistro(lg); const codiceVero = creaCliente(id, lg, { nome: 'Vera', cognome: 'Utente', sesso: 'F', user_id: 1 }); const codiceAltro = creaCliente(id, lg, { nome: 'Altro', cognome: 'Cliente', sesso: 'M', user_id: 2 }); const { POST } = await import('../../src/pages/api/longevity/questionario'); const richiesta = new Request('http://localhost/api/longevity/questionario', { method: 'POST', body: JSON.stringify({ client_code: codiceAltro, // tentativo: scrivere nel fascicolo di un altro cliente risposte: { q_riposato: 8 }, consensoSanitario: 'Si', }), }); const res = await POST({ request: richiesta, locals: { user: { id: 1 } } } as any); expect(res.status).toBe(201); const corpo = await res.json(); const riga = lg.prepare(`SELECT client_code FROM sessioni WHERE id = ?`).get(corpo.sessioneId) as { client_code: string }; expect(riga.client_code).toBe(codiceVero); expect(riga.client_code).not.toBe(codiceAltro); } finally { if (primaId === undefined) delete process.env.IDENTITY_DB_PATH; else process.env.IDENTITY_DB_PATH = primaId; if (primaLg === undefined) delete process.env.LONGEVITY_DB_PATH; else process.env.LONGEVITY_DB_PATH = primaLg; rmSync(dir, { recursive: true, force: true }); } }); });