longevity: crea l'accesso al sito per un cliente che ha già il fascicolo
accessoDi/collegaAccesso in anagrafica.ts (e le varianti sul singleton accessoDelCliente/collegaAccessoCliente) leggono e scrivono il legame clienti.user_id restando l'unico punto che apre identity. accesso.ts aggiunge creaAccessoCliente: crea l'utente cliente, lo collega, e se il collegamento fallisce cancella l'utente appena creato (compensazione, due database, stesso schema di creaCliente). Nuova pagina del gestionale pages/longevity/gestionale/cliente/[code]/accesso.astro: genera username e password una volta sola, niente redirect, no-store. Refs #8 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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<typeof createIdentityDb>, utenti: ReturnType<typeof createDb>): 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(');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user