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]
|
||||
);
|
||||
|
||||
// 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 ultimo = passo === totalePassi - 1;
|
||||
|
||||
@@ -54,6 +59,11 @@ export default function Questionario({ campi }: Props) {
|
||||
|
||||
const risposteNumeriche: Record<string, number> = {};
|
||||
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);
|
||||
if (v !== '' && Number.isFinite(n)) risposteNumeriche[id] = n;
|
||||
}
|
||||
@@ -97,6 +107,13 @@ export default function Questionario({ campi }: Props) {
|
||||
{gruppi[passo].campi.map((c) => (
|
||||
<label className="lg-field" key={c.id}>
|
||||
<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
|
||||
type="number"
|
||||
min={c.min}
|
||||
@@ -105,6 +122,7 @@ export default function Questionario({ campi }: Props) {
|
||||
value={risposte[c.id] ?? ''}
|
||||
onChange={(e) => setRisposte((r) => ({ ...r, [c.id]: e.target.value }))}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
|
||||
@@ -4,7 +4,18 @@
|
||||
// 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.
|
||||
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
|
||||
|
||||
@@ -135,17 +135,16 @@ export function createIdentityDb(path?: string): Database.Database {
|
||||
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.
|
||||
// ⚠️ 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 identitySingleton: Database.Database | null = null;
|
||||
|
||||
export function getLongevityDb(): Database.Database {
|
||||
if (!longevitySingleton) longevitySingleton = createLongevityDb();
|
||||
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.
|
||||
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,
|
||||
});
|
||||
} 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 });
|
||||
return json(201, { sessioneId: esito.sessioneId });
|
||||
|
||||
@@ -88,15 +88,23 @@ describe('anagrafica pseudonimizzata', () => {
|
||||
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', () => {
|
||||
// Non cerchiamo due nomi di funzione precisi (createIdentityDb/createLongevityDb): un
|
||||
// domani qualcuno aggiunge getIdentityDb, o un terzo accessore con un nome diverso, e
|
||||
// l'elenco invecchia senza che nessuno se ne accorga (è già successo: getIdentityDb e
|
||||
// getLongevityDb sono nati DOPO questo test e sono passati indisturbati). Il verso
|
||||
// giusto è strutturale: qualunque accesso alle due connessioni passa comunque per una
|
||||
// funzione esportata da db.ts il cui nome contiene "Identity" o "Longevity" — quindi
|
||||
// basta cercare quei due riferimenti letterali, in QUALUNQUE forma compaiano, e
|
||||
// segnalare i file che li hanno tutti e due.
|
||||
it('rete secondaria: nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity', () => {
|
||||
// ⚠️ QUESTO TEST NON È LA GARANZIA. La garanzia vera è che getIdentityDb non è più
|
||||
// esportata da db.ts (vive privata dentro anagrafica.ts): non esiste, da nessuna
|
||||
// parte del codice fuori da lì, un nome da importare per ottenere quella connessione.
|
||||
// Un controllo testuale come questo non può bastare da solo — è stato dimostrato:
|
||||
// tre file, uno che riesporta la connessione a identity, uno che riesporta quella a
|
||||
// longevity, un terzo che importa da entrambi e le usa insieme, e NESSUNO dei tre
|
||||
// contiene singolarmente le due parole. Un controllo file-per-file non vede una
|
||||
// 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 colpevoli: string[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user