Separazione: le aree riservate diventano un'app propria (apps/platforms)
Campus, Stress Index e Longevity escono dal sito e diventano apps.insanitylab.it, con utenti, sessioni, login e database propri. Deciso da Adriano il 03/09/2026: una repo sola, due applicazioni autosufficienti, cosi' che ognuna resti estraibile. Misura che ha guidato il taglio: le tre aree condividevano col sito SOLO l'identita' - ~170 righe fra auth.ts, middleware.ts e safe-next.ts. Layout, librerie e dati erano gia' disgiunti. Lato piattaforme: - ruoli piattaforme/cliente/trainer/admin, senza quelli redazionali - canAccessPath con default CHIUSO (nel sito era aperto: li' le rotte non elencate erano di chiunque fosse loggato, qui ogni sezione e' di qualcuno) - tutta l'app protetta, si elencano le eccezioni invece dei prefissi protetti - /api risponde 401/403, non un redirect verso una pagina di login - tre database distinti: utenti, dati clinici senza nomi, mappatura coi nomi Lato sito: - ROLES tornano i tre redazionali; via le regole e il login pubblico - il login del pannello RESPINGE un account emigrato invece di aprirgli una sessione: senza, landingFor lo mandava su /admin, il middleware glielo negava, e si formava un ciclo di redirect che sembra un guasto del sito - l'header non mostra piu' un menu delle piattaforme: un link che esce Verificato: sito 20 file / 146 test, piattaforme 26 file / 197 test, entrambi i build passano, e l'app girata davvero in locale - il ruolo piattaforme entra in Campus e viene rimbalzato da Longevity, il cliente il contrario. Non ancora in produzione: manca il record DNS di apps.insanitylab.it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createLongevityDb } from '../../src/lib/longevity/db';
|
||||
import { seedRegistro } from '../../src/lib/longevity/registro';
|
||||
import { salvaCompilazione } from '../../src/lib/longevity/questionario';
|
||||
import { esportaMisure, esportaCsv } from '../../src/lib/longevity/export';
|
||||
|
||||
function dbConDati() {
|
||||
const db = createLongevityDb(':memory:');
|
||||
seedRegistro(db);
|
||||
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001', 'F')`).run();
|
||||
salvaCompilazione(db, {
|
||||
client_code: 'ISL-0001', data: '2026-08-21', eta: 35,
|
||||
risposte: { q_ore_sonno: 7, q_riposato: 8 },
|
||||
liberi: { farmaci: 'nessuno' },
|
||||
});
|
||||
return db;
|
||||
}
|
||||
|
||||
describe('export per le statistiche', () => {
|
||||
it('esporta una riga per misura con codice, non con nome', () => {
|
||||
const db = dbConDati();
|
||||
const out = esportaMisure(db);
|
||||
expect(out.intestazioni).toEqual(
|
||||
['client_code', 'sesso', 'eta', 'data', 'tipo_sessione', 'quest_version', 'test_id', 'valore', 'unita', 'fonte', 'fuori_range']
|
||||
);
|
||||
expect(out.righe.length).toBe(2);
|
||||
expect(out.righe[0][0]).toBe('ISL-0001');
|
||||
});
|
||||
|
||||
it('l export non contiene campi identificativi', () => {
|
||||
const db = dbConDati();
|
||||
const testo = JSON.stringify(esportaMisure(db));
|
||||
for (const vietato of ['nome', 'cognome', 'email', 'telefono', 'data_nascita']) {
|
||||
expect(testo).not.toContain(vietato);
|
||||
}
|
||||
});
|
||||
|
||||
it('i campi liberi restano fuori dall export statistico', () => {
|
||||
const db = dbConDati();
|
||||
expect(JSON.stringify(esportaMisure(db))).not.toContain('nessuno');
|
||||
});
|
||||
|
||||
it('produce un CSV con intestazione e una riga per misura', () => {
|
||||
const db = dbConDati();
|
||||
const righe = esportaCsv(db).trim().split('\n');
|
||||
expect(righe.length).toBe(3); // intestazione + 2 misure
|
||||
expect(righe[0]).toBe('client_code,sesso,eta,data,tipo_sessione,quest_version,test_id,valore,unita,fonte,fuori_range');
|
||||
});
|
||||
|
||||
it('ogni valore sta sotto l intestazione giusta, colonna per colonna (asserzione posizionale)', () => {
|
||||
// Non un indice scritto a mano: l'indice si cerca per NOME nell'intestazione prodotta.
|
||||
// Se un domani la SELECT in export.ts viene riordinata senza riallineare INTESTAZIONI,
|
||||
// il valore letto a quell'indice smette di corrispondere e l'asserzione fallisce -
|
||||
// invece di restare verde con l'età etichettata come sesso.
|
||||
const db = dbConDati();
|
||||
const { intestazioni, righe } = esportaMisure(db);
|
||||
const idx = (col: string) => {
|
||||
const i = intestazioni.indexOf(col);
|
||||
if (i < 0) throw new Error(`intestazione mancante: ${col}`);
|
||||
return i;
|
||||
};
|
||||
|
||||
const rigaSonno = righe.find((r) => r[idx('test_id')] === 'q_ore_sonno')!;
|
||||
expect(rigaSonno).toBeDefined();
|
||||
expect(rigaSonno[idx('client_code')]).toBe('ISL-0001');
|
||||
expect(rigaSonno[idx('sesso')]).toBe('F');
|
||||
expect(rigaSonno[idx('eta')]).toBe(35);
|
||||
expect(rigaSonno[idx('data')]).toBe('2026-08-21');
|
||||
expect(rigaSonno[idx('tipo_sessione')]).toBe('questionario');
|
||||
expect(rigaSonno[idx('quest_version')]).toBe('v1.0');
|
||||
expect(rigaSonno[idx('valore')]).toBe(7);
|
||||
// salvaCompilazione non passa 'unita' per le risposte del questionario: resta null.
|
||||
expect(rigaSonno[idx('unita')]).toBeNull();
|
||||
expect(rigaSonno[idx('fonte')]).toBe('questionario');
|
||||
expect(rigaSonno[idx('fuori_range')]).toBe(0);
|
||||
|
||||
const rigaRiposato = righe.find((r) => r[idx('test_id')] === 'q_riposato')!;
|
||||
expect(rigaRiposato).toBeDefined();
|
||||
expect(rigaRiposato[idx('client_code')]).toBe('ISL-0001');
|
||||
expect(rigaRiposato[idx('sesso')]).toBe('F');
|
||||
expect(rigaRiposato[idx('eta')]).toBe(35);
|
||||
expect(rigaRiposato[idx('valore')]).toBe(8);
|
||||
expect(rigaRiposato[idx('unita')]).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user