import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { createDb } from '../../src/lib/db'; import { login } from '../../src/lib/auth'; import { createIdentityDb, createLongevityDb } from '../../src/lib/longevity/db'; import { creaCliente, accessoDi, collegaAccesso } from '../../src/lib/longevity/anagrafica'; import { creaAccessoCliente, type DipendenzeAccesso } from '../../src/lib/longevity/accesso'; // Doppio delle dipendenze su DB in memoria: come in anagrafica.test.ts, connessioni proprie // passate a mano, mai il singleton reale — questo è l'unico modo in cui accesso.ts si // prova senza toccare data/identity.db o data/piattaforme.db. function dipendenzeDiTest(identity: ReturnType, utenti: ReturnType): DipendenzeAccesso { return { utenti, accessoDi: (code) => accessoDi(identity, code), collega: (code, userId) => collegaAccesso(identity, code, userId), }; } function fascicoloPronto() { const identity = createIdentityDb(':memory:'); const longevity = createLongevityDb(':memory:'); const utenti = createDb(':memory:'); const code = creaCliente(identity, longevity, { nome: 'Mario', cognome: 'Rossi', sesso: 'M' }); return { identity, longevity, utenti, code }; } describe('creaAccessoCliente', () => { it('crea l utente cliente, lo collega, e la password permette il login', () => { const { identity, utenti, code } = fascicoloPronto(); const deps = dipendenzeDiTest(identity, utenti); const esito = creaAccessoCliente(deps, { code, username: 'MarioRossi' }); expect(esito.ok).toBe(true); if (!esito.ok) return; expect(esito.username).toBe('mariorossi'); // normalizzato minuscolo const riga = utenti.prepare('SELECT id, password_hash, role FROM users WHERE username = ?').get('mariorossi') as { id: number; password_hash: string; role: string }; expect(riga.role).toBe('cliente'); // la password torna in chiaro nell'esito, ma non è quella salvata: quella è l'hash. expect(riga.password_hash).not.toBe(esito.password); // il collegamento è avvenuto davvero: accessoDi lo ritrova. const dopo = accessoDi(identity, code); expect(dopo?.userId).toBe(riga.id); // e la password restituita fa davvero login. const token = login(utenti, 'mariorossi', esito.password); expect(token).not.toBeNull(); }); it('fascicolo già con un accesso: errore, nessun utente creato', () => { const { identity, utenti, code } = fascicoloPronto(); const deps = dipendenzeDiTest(identity, utenti); const primo = creaAccessoCliente(deps, { code, username: 'uno' }); expect(primo.ok).toBe(true); const secondo = creaAccessoCliente(deps, { code, username: 'due' }); expect(secondo.ok).toBe(false); const utentiDopo = utenti.prepare('SELECT COUNT(*) n FROM users').get() as { n: number }; expect(utentiDopo.n).toBe(1); // solo il primo, "due" non è mai stato creato }); it('codice inesistente: errore', () => { const { identity, utenti } = fascicoloPronto(); const deps = dipendenzeDiTest(identity, utenti); const esito = creaAccessoCliente(deps, { code: 'ISL-9999', username: 'fantasma' }); expect(esito.ok).toBe(false); }); it('username già esistente: errore, niente collegato', () => { const { identity, utenti, code } = fascicoloPronto(); const deps = dipendenzeDiTest(identity, utenti); utenti.prepare("INSERT INTO users (username, password_hash, role) VALUES ('occupato', 'x', 'admin')").run(); const esito = creaAccessoCliente(deps, { code, username: 'occupato' }); expect(esito.ok).toBe(false); const fascicolo = accessoDi(identity, code); expect(fascicolo?.userId).toBeNull(); // resta senza accesso }); it('username non valido (maiuscole non ammesse dopo la normalizzazione, o troppo corto): errore', () => { const { identity, utenti, code } = fascicoloPronto(); const deps = dipendenzeDiTest(identity, utenti); const corto = creaAccessoCliente(deps, { code, username: 'ab' }); expect(corto.ok).toBe(false); const conSpazio = creaAccessoCliente(deps, { code, username: 'ha spazi' }); expect(conSpazio.ok).toBe(false); const utentiDopo = utenti.prepare('SELECT COUNT(*) n FROM users').get() as { n: number }; expect(utentiDopo.n).toBe(0); }); it('se il collegamento fallisce, l utente appena creato viene cancellato (compensazione)', () => { const { identity, utenti, code } = fascicoloPronto(); const deps: DipendenzeAccesso = { utenti, accessoDi: (c) => accessoDi(identity, c), collega: () => false, // simula un collegamento rifiutato (es. fascicolo cambiato nel frattempo) }; const esito = creaAccessoCliente(deps, { code, username: 'sfortunato' }); expect(esito.ok).toBe(false); const rimasto = utenti.prepare('SELECT * FROM users WHERE username = ?').get('sfortunato'); expect(rimasto).toBeUndefined(); // nessun utente orfano }); it('se il collegamento lancia, l utente appena creato viene comunque cancellato', () => { const { identity, utenti, code } = fascicoloPronto(); const deps: DipendenzeAccesso = { utenti, accessoDi: (c) => accessoDi(identity, c), collega: () => { throw new Error('identity irraggiungibile'); }, }; const esito = creaAccessoCliente(deps, { code, username: 'sfortunato2' }); expect(esito.ok).toBe(false); const rimasto = utenti.prepare('SELECT * FROM users WHERE username = ?').get('sfortunato2'); expect(rimasto).toBeUndefined(); }); it('rete secondaria: la pagina del gestionale non mette la password in cache e non nomina identity', () => { const path = join(process.cwd(), 'src/pages/longevity/gestionale/cliente/[code]/accesso.astro'); const src = readFileSync(path, 'utf8'); expect(src).toContain('no-store'); expect(src).not.toContain('Identity'); expect(src).not.toContain('.prepare('); }); });