longevity: la connessione a identity non e' piu' raggiungibile fuori da anagrafica.ts
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'.
This commit is contained in:
@@ -45,6 +45,11 @@ export default function Questionario({ campi }: Props) {
|
|||||||
[campi]
|
[campi]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Il tipo dichiarato nel registro per ciascun campo: serve sia per scegliere il tipo di
|
||||||
|
// input (numero o testo) sia, all'invio, per non trattare una risposta testuale come un
|
||||||
|
// numero da scartare in silenzio.
|
||||||
|
const tipoDiCampo = useMemo(() => new Map(campi.map((c) => [c.id, c.tipo])), [campi]);
|
||||||
|
|
||||||
const totalePassi = gruppi.length + 1; // + il blocco note/consenso
|
const totalePassi = gruppi.length + 1; // + il blocco note/consenso
|
||||||
const ultimo = passo === totalePassi - 1;
|
const ultimo = passo === totalePassi - 1;
|
||||||
|
|
||||||
@@ -54,6 +59,11 @@ export default function Questionario({ campi }: Props) {
|
|||||||
|
|
||||||
const risposteNumeriche: Record<string, number> = {};
|
const risposteNumeriche: Record<string, number> = {};
|
||||||
for (const [id, v] of Object.entries(risposte)) {
|
for (const [id, v] of Object.entries(risposte)) {
|
||||||
|
// Le domande testuali (tipo_valore='txt' nel registro) non sono ancora agganciate
|
||||||
|
// al salvataggio numerico: oggi non esistono nel questionario (i 20 campi attivi
|
||||||
|
// sono tutti 'num'), ma se un domani ne comparisse una non va convertita in NaN e
|
||||||
|
// scartata in silenzio — resta fuori esplicitamente.
|
||||||
|
if (tipoDiCampo.get(id) === 'txt') continue;
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
if (v !== '' && Number.isFinite(n)) risposteNumeriche[id] = n;
|
if (v !== '' && Number.isFinite(n)) risposteNumeriche[id] = n;
|
||||||
}
|
}
|
||||||
@@ -97,6 +107,13 @@ export default function Questionario({ campi }: Props) {
|
|||||||
{gruppi[passo].campi.map((c) => (
|
{gruppi[passo].campi.map((c) => (
|
||||||
<label className="lg-field" key={c.id}>
|
<label className="lg-field" key={c.id}>
|
||||||
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
|
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
|
||||||
|
{c.tipo === 'txt' ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={risposte[c.id] ?? ''}
|
||||||
|
onChange={(e) => setRisposte((r) => ({ ...r, [c.id]: e.target.value }))}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={c.min}
|
min={c.min}
|
||||||
@@ -105,6 +122,7 @@ export default function Questionario({ campi }: Props) {
|
|||||||
value={risposte[c.id] ?? ''}
|
value={risposte[c.id] ?? ''}
|
||||||
onChange={(e) => setRisposte((r) => ({ ...r, [c.id]: e.target.value }))}
|
onChange={(e) => setRisposte((r) => ({ ...r, [c.id]: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,7 +4,18 @@
|
|||||||
// risalga alla persona (richiesta del cliente, 21/08): un secondo punto di giunzione
|
// risalga alla persona (richiesta del cliente, 21/08): un secondo punto di giunzione
|
||||||
// la annullerebbe in silenzio. Un test in tests/longevity/anagrafica.test.ts lo verifica.
|
// la annullerebbe in silenzio. Un test in tests/longevity/anagrafica.test.ts lo verifica.
|
||||||
import type Database from 'better-sqlite3';
|
import type Database from 'better-sqlite3';
|
||||||
import { getIdentityDb } from './db';
|
import { createIdentityDb } from './db';
|
||||||
|
|
||||||
|
// Connessione singola a identity, privata di questo modulo: non è esportata, quindi non
|
||||||
|
// è raggiungibile da nessun'altra parte del codice, nemmeno con un re-export su più file
|
||||||
|
// (un controllo testuale file-per-file non lo vedrebbe, ma qui il problema non si pone:
|
||||||
|
// non esiste un nome da importare). QUESTO è il modo in cui il vincolo si rende vero,
|
||||||
|
// non solo sorvegliato.
|
||||||
|
let identitySingleton: Database.Database | null = null;
|
||||||
|
function getIdentityDb(): Database.Database {
|
||||||
|
if (!identitySingleton) identitySingleton = createIdentityDb();
|
||||||
|
return identitySingleton;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Il prossimo codice libero: il massimo fra quelli già usati in identity.clienti E in
|
* Il prossimo codice libero: il massimo fra quelli già usati in identity.clienti E in
|
||||||
|
|||||||
@@ -135,17 +135,16 @@ export function createIdentityDb(path?: string): Database.Database {
|
|||||||
return apri(path ?? process.env.IDENTITY_DB_PATH ?? 'data/identity.db', SCHEMA_IDENTITY);
|
return apri(path ?? process.env.IDENTITY_DB_PATH ?? 'data/identity.db', SCHEMA_IDENTITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connessioni singole per processo, come getDb() in src/lib/db.ts: gli endpoint chiedono
|
// Connessione singola per processo, come getDb() in src/lib/db.ts: gli endpoint chiedono
|
||||||
// una connessione già aperta invece di aprirne una nuova a ogni richiesta.
|
// una connessione già aperta invece di aprirne una nuova a ogni richiesta.
|
||||||
|
// ⚠️ Qui c'è SOLO longevity (il database senza nomi: può usarlo chiunque). Un singleton
|
||||||
|
// analogo per identity non si esporta da qui — vive privato dentro anagrafica.ts, l'unico
|
||||||
|
// modulo autorizzato a tenerlo: un controllo testuale file-per-file non vede una giunzione
|
||||||
|
// che passa da un re-export su piu' file, quindi la connessione condivisa a identity non
|
||||||
|
// deve essere raggiungibile da fuori quel modulo, punto.
|
||||||
let longevitySingleton: Database.Database | null = null;
|
let longevitySingleton: Database.Database | null = null;
|
||||||
let identitySingleton: Database.Database | null = null;
|
|
||||||
|
|
||||||
export function getLongevityDb(): Database.Database {
|
export function getLongevityDb(): Database.Database {
|
||||||
if (!longevitySingleton) longevitySingleton = createLongevityDb();
|
if (!longevitySingleton) longevitySingleton = createLongevityDb();
|
||||||
return longevitySingleton;
|
return longevitySingleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIdentityDb(): Database.Database {
|
|
||||||
if (!identitySingleton) identitySingleton = createIdentityDb();
|
|
||||||
return identitySingleton;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -46,9 +46,20 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
// La data della compilazione è quella del server, non quella (manomettibile) del client.
|
// La data della compilazione è quella del server, non quella (manomettibile) del client.
|
||||||
const oggi = new Date().toISOString().slice(0, 10);
|
const oggi = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
const esito = salvaDalForm(getLongevityDb(), {
|
// salvaDalForm delega a salvaCompilazione, che lancia se una risposta porta un test_id
|
||||||
|
// sconosciuto al registro (client disallineato, non serve malizia): un'eccezione qui
|
||||||
|
// non deve diventare un 500 generico, come in src/pages/api/contact.ts per l'invio email.
|
||||||
|
let esito;
|
||||||
|
try {
|
||||||
|
esito = salvaDalForm(getLongevityDb(), {
|
||||||
client_code: codice, data: oggi, eta, risposte, consensoSanitario, liberi,
|
client_code: codice, data: oggi, eta, risposte, consensoSanitario, liberi,
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Salvataggio questionario fallito:', err);
|
||||||
|
return json(400, {
|
||||||
|
error: 'Impossibile salvare: alcune risposte non sono riconosciute. Ricarica la pagina e riprova.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!esito.ok) return json(422, { error: esito.errore });
|
if (!esito.ok) return json(422, { error: esito.errore });
|
||||||
return json(201, { sessioneId: esito.sessioneId });
|
return json(201, { sessioneId: esito.sessioneId });
|
||||||
|
|||||||
@@ -88,15 +88,23 @@ describe('anagrafica pseudonimizzata', () => {
|
|||||||
expect(id.prepare(`SELECT * FROM clienti WHERE nome = 'Errato'`).get()).toBeUndefined();
|
expect(id.prepare(`SELECT * FROM clienti WHERE nome = 'Errato'`).get()).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity', () => {
|
it('rete secondaria: nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity', () => {
|
||||||
// Non cerchiamo due nomi di funzione precisi (createIdentityDb/createLongevityDb): un
|
// ⚠️ QUESTO TEST NON È LA GARANZIA. La garanzia vera è che getIdentityDb non è più
|
||||||
// domani qualcuno aggiunge getIdentityDb, o un terzo accessore con un nome diverso, e
|
// esportata da db.ts (vive privata dentro anagrafica.ts): non esiste, da nessuna
|
||||||
// l'elenco invecchia senza che nessuno se ne accorga (è già successo: getIdentityDb e
|
// parte del codice fuori da lì, un nome da importare per ottenere quella connessione.
|
||||||
// getLongevityDb sono nati DOPO questo test e sono passati indisturbati). Il verso
|
// Un controllo testuale come questo non può bastare da solo — è stato dimostrato:
|
||||||
// giusto è strutturale: qualunque accesso alle due connessioni passa comunque per una
|
// tre file, uno che riesporta la connessione a identity, uno che riesporta quella a
|
||||||
// funzione esportata da db.ts il cui nome contiene "Identity" o "Longevity" — quindi
|
// longevity, un terzo che importa da entrambi e le usa insieme, e NESSUNO dei tre
|
||||||
// basta cercare quei due riferimenti letterali, in QUALUNQUE forma compaiano, e
|
// contiene singolarmente le due parole. Un controllo file-per-file non vede una
|
||||||
// segnalare i file che li hanno tutti e due.
|
// 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 srcDir = join(process.cwd(), 'src');
|
||||||
const colpevoli: string[] = [];
|
const colpevoli: string[] = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user