From 95ffa08197c8d7406756978694a0d968d5f157ab Mon Sep 17 00:00:00 2001 From: "Adriano (goldrake)" Date: Sat, 26 Sep 2026 12:53:46 +0000 Subject: [PATCH] =?UTF-8?q?longevity:=20API=20del=20gestionale=20(clienti,?= =?UTF-8?q?=20check-up)=20e=20test=20dell'unit=C3=A0=202a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pages/api/longevity/gestionale/clienti.ts: POST del nuovo fascicolo (nome, cognome, sesso obbligatori; data di nascita/email/telefono facoltativi) → apriFascicolo → 303 alla scheda cliente, o 303 al modulo con ?errore=. pages/api/longevity/gestionale/checkup.ts: POST del check-up manuale, con misureDalForm (funzione pura, esportata e testata) per il parsing del form — virgola decimale accettata, campo vuoto = non misurato, valore non numerico ferma tutto, chiavi che non sono campi f_* attivi ignorate. operatore sempre da locals.user (mai dal corpo, stesso principio del client_code in questionario.ts); eta da etaDelCliente. Entrambi riservati a admin+trainer da RULES in auth.ts (già in vigore, verificato dal test su canAccessPath). tests/longevity/gestionale.test.ts: 17 test — salvaCheckup e il congelamento dei punteggi, il referto che combina check-up e questionario di sessioni diverse (#1), i rifiuti prima della scrittura, le misure fuori range, le versioni parametrizzate dell'anagrafica, apriFascicolo sul singleton (stesso trucco env-var di questionario-pagina.test.ts), misureDalForm, canAccessPath. vitest: 229 passed (212 di partenza + 17). tsc --noEmit: 0 errori. Refs #2 Co-Authored-By: Claude Sonnet 5 --- .../pages/api/longevity/gestionale/checkup.ts | 90 ++++++ .../pages/api/longevity/gestionale/clienti.ts | 56 ++++ .../tests/longevity/gestionale.test.ts | 281 ++++++++++++++++++ 3 files changed, 427 insertions(+) create mode 100644 apps/platforms/src/pages/api/longevity/gestionale/checkup.ts create mode 100644 apps/platforms/src/pages/api/longevity/gestionale/clienti.ts create mode 100644 apps/platforms/tests/longevity/gestionale.test.ts diff --git a/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts b/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts new file mode 100644 index 0000000..8ca5c92 --- /dev/null +++ b/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts @@ -0,0 +1,90 @@ +import type { APIRoute } from 'astro'; +import { getLongevityDb } from '../../../../lib/longevity/db'; +import { etaDelCliente } from '../../../../lib/longevity/anagrafica'; +import { testAttivi, type VoceRegistro } from '../../../../lib/longevity/registro'; +import { salvaCheckup } from '../../../../lib/longevity/gestionale'; +import type { MisuraIn } from '../../../../lib/longevity/misure'; + +export const prerender = false; + +// Riservato a admin+trainer da src/lib/auth.ts (RULES, /^\/api\/longevity\/gestionale(\/|$)/). + +const REGEX_DATA = /^\d{4}-\d{2}-\d{2}$/; +const REGEX_CODE = /^ISL-\d{4}$/; + +const redirect = (location: string) => new Response(null, { status: 303, headers: { Location: location } }); + +/** + * Il parsing del form del check-up, isolato dall'I/O (nessun DB, nessuna Request) apposta + * per essere testato senza fixture: riceve i campi ATTIVI (da `testAttivi`, filtrati su + * `f_*` da chi chiama) e il `FormData` grezzo, e produce le misure da passare a + * `salvaCheckup` — o l'errore che ferma tutto PRIMA che una sessione si apra. + * + * Regole, dalla spec (#2): un campo vuoto = non misurato (non un errore, non uno zero); la + * virgola decimale si accetta ("32,5" come "32.5", scrittura comune su un modulo cartaceo); + * un valore non numerico è un errore che blocca l'intero check-up, non solo quel campo — + * meglio fermarsi che salvare a metà con un tacito zero; le chiavi che non sono fra i campi + * attivi (client_code, data, un q_* del questionario, un refuso) si ignorano in silenzio, + * mai un errore per la loro sola presenza — un `` nascosto o un campo dismesso non + * deve rompere il salvataggio del resto. + */ +export function misureDalForm( + form: FormData, + campiAttivi: Pick[] +): { ok: true; misure: MisuraIn[] } | { ok: false; errore: string } { + const perTestId = new Map(campiAttivi.map((c) => [c.test_id, c])); + const misure: MisuraIn[] = []; + + for (const [chiave, valoreGrezzo] of form.entries()) { + const campo = perTestId.get(chiave); + if (!campo) continue; // chiave estranea: ignorata, non è un errore del form + + const testo = String(valoreGrezzo).trim(); + if (testo === '') continue; // vuoto = non misurato + + const numero = Number(testo.replace(',', '.')); + if (!Number.isFinite(numero)) { + return { ok: false, errore: `Valore non numerico per ${chiave}: "${testo}"` }; + } + misure.push({ test_id: chiave, valore_num: numero, unita: campo.unita }); + } + + return { ok: true, misure }; +} + +export const POST: APIRoute = async ({ request, locals }) => { + const form = await request.formData(); + const clientCode = String(form.get('client_code') ?? '').trim(); + const data = String(form.get('data') ?? '').trim(); + + // Senza un codice valido non c'è una scheda a cui tornare: l'unico ritorno sensato è + // l'indice del gestionale, non /cliente/undefined/checkup. + if (!REGEX_CODE.test(clientCode)) { + return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Codice cliente non valido.')}`); + } + const erroreAllaScheda = (testo: string) => + redirect(`/longevity/gestionale/cliente/${clientCode}/checkup?errore=${encodeURIComponent(testo)}`); + + if (!REGEX_DATA.test(data)) return erroreAllaScheda('Data non valida (formato AAAA-MM-GG).'); + const oggi = new Date().toISOString().slice(0, 10); + if (data > oggi) return erroreAllaScheda('La data del check-up non può essere nel futuro.'); + + const db = getLongevityDb(); + const campiAttivi = testAttivi(db, data).filter((v) => v.test_id.startsWith('f_')); + + const parse = misureDalForm(form, campiAttivi); + if (!parse.ok) return erroreAllaScheda(parse.errore); + + // L'operatore è SEMPRE quello della sessione loggata (garantita da RULES + middleware: + // solo admin/trainer arrivano qui), mai un campo del form — lo stesso principio del + // client_code in pages/api/longevity/questionario.ts, applicato al campo che qui conta. + const operatore = locals.user!.username; + const eta = etaDelCliente(clientCode, data); + + const esito = salvaCheckup(db, { client_code: clientCode, data, eta, operatore, misure: parse.misure }); + if (!esito.ok) return erroreAllaScheda(esito.errore); + + const params = new URLSearchParams({ salvato: String(esito.sessioneId) }); + if (esito.fuoriRange.length > 0) params.set('fuori', String(esito.fuoriRange.length)); + return redirect(`/longevity/gestionale/cliente/${clientCode}?${params.toString()}`); +}; diff --git a/apps/platforms/src/pages/api/longevity/gestionale/clienti.ts b/apps/platforms/src/pages/api/longevity/gestionale/clienti.ts new file mode 100644 index 0000000..e10d011 --- /dev/null +++ b/apps/platforms/src/pages/api/longevity/gestionale/clienti.ts @@ -0,0 +1,56 @@ +import type { APIRoute } from 'astro'; +import { apriFascicolo } from '../../../../lib/longevity/anagrafica'; + +export const prerender = false; + +// Riservato a admin+trainer da src/lib/auth.ts (RULES, /^\/api\/longevity\/gestionale(\/|$)/): +// il middleware nega prima che questo file veda la richiesta. + +const REGEX_DATA = /^\d{4}-\d{2}-\d{2}$/; + +const redirect = (location: string) => new Response(null, { status: 303, headers: { Location: location } }); + +/** Sempre la stessa pagina di partenza: questo endpoint ha un solo modulo che lo chiama + * (pages/longevity/gestionale/nuovo.astro, unità 2b), quindi il ritorno in caso di errore + * non serve dedurlo da un Referer — si scrive qui, una volta. */ +const erroreAlModulo = (testo: string) => + redirect(`/longevity/gestionale/nuovo?errore=${encodeURIComponent(testo)}`); + +function isSesso(v: string): v is 'M' | 'F' { + return v === 'M' || v === 'F'; +} + +export const POST: APIRoute = async ({ request }) => { + const form = await request.formData(); + const leggi = (chiave: string) => String(form.get(chiave) ?? '').trim(); + + const nome = leggi('nome'); + const cognome = leggi('cognome'); + const sesso = leggi('sesso'); + const dataNascita = leggi('data_nascita'); + const email = leggi('email'); + const telefono = leggi('telefono'); + + if (!nome) return erroreAlModulo('Il nome è obbligatorio.'); + if (!cognome) return erroreAlModulo('Il cognome è obbligatorio.'); + if (!isSesso(sesso)) return erroreAlModulo('Il sesso deve essere M o F.'); + + if (dataNascita) { + if (!REGEX_DATA.test(dataNascita)) { + return erroreAlModulo('Data di nascita non valida (formato AAAA-MM-GG).'); + } + // La data del server, non quella (manomettibile) del form: stesso criterio di + // pages/api/longevity/questionario.ts per "oggi". + const oggi = new Date().toISOString().slice(0, 10); + if (dataNascita > oggi) return erroreAlModulo('La data di nascita non può essere nel futuro.'); + } + + const code = apriFascicolo({ + nome, cognome, sesso, + data_nascita: dataNascita || undefined, + email: email || undefined, + telefono: telefono || undefined, + }); + + return redirect(`/longevity/gestionale/cliente/${code}`); +}; diff --git a/apps/platforms/tests/longevity/gestionale.test.ts b/apps/platforms/tests/longevity/gestionale.test.ts new file mode 100644 index 0000000..5e160f7 --- /dev/null +++ b/apps/platforms/tests/longevity/gestionale.test.ts @@ -0,0 +1,281 @@ +// Unità 2a (#2): il modulo del check-up manuale del trainer e le letture dell'anagrafica che +// gli servono. Stesso principio di salvataggio-e2e.test.ts — si parte dal modulo vero +// (salvaCheckup), mai da calcolaSessione/salvaScore chiamati a mano — più la prova che +// questo file esiste apposta a garantire: un check-up e un questionario sono sessioni +// separate (#1, misureValideAlla), quindi il referto del secondo deve vedere il primo. +import { describe, it, expect } from 'vitest'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { createLongevityDb, createIdentityDb } from '../../src/lib/longevity/db'; +import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro'; +import { salvaCheckup, sessioniDi, misureFuoriRange } from '../../src/lib/longevity/gestionale'; +import { salvaDalForm, refertoDi } from '../../src/lib/longevity/vista'; +import { + creaCliente, elencoClientiParam, clienteDaCodiceParam, etaDelClienteParam, apriFascicolo, +} from '../../src/lib/longevity/anagrafica'; +import { canAccessPath } from '../../src/lib/auth'; +import { misureDalForm } from '../../src/pages/api/longevity/gestionale/checkup'; + +function dbPronto(clientCode = 'ISL-0001', sesso: 'M' | 'F' = 'M') { + const db = createLongevityDb(':memory:'); + seedRegistro(db); + seedPesi(db, MODEL_VERSION); + db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES (?, ?)`).run(clientCode, sesso); + return db; +} + +describe('salvaCheckup scrive la sessione e congela i punteggi da solo', () => { + it('una sessione tipo checkup, misure di fonte manuale, almeno una riga in score', () => { + const db = dbPronto(); + const esito = salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'nicola', + misure: [ + { test_id: 'f_peso_kg', valore_num: 80 }, + { test_id: 'f_handgrip_kg', valore_num: 45 }, + ], + }); + expect(esito.ok).toBe(true); + if (!esito.ok) throw new Error(esito.errore); + + const sessione = db.prepare(`SELECT tipo, operatore FROM sessioni WHERE id = ?`).get(esito.sessioneId) as + { tipo: string; operatore: string }; + expect(sessione.tipo).toBe('checkup'); + expect(sessione.operatore).toBe('nicola'); + + const fonti = db.prepare(`SELECT DISTINCT fonte FROM misure WHERE sessione_id = ?`).all(esito.sessioneId) as + { fonte: string }[]; + expect(fonti).toEqual([{ fonte: 'manuale' }]); + + const righeScore = db.prepare(`SELECT COUNT(*) n FROM score WHERE sessione_id = ?`).get(esito.sessioneId) as + { n: number }; + expect(righeScore.n).toBeGreaterThan(0); + }); + + it('dopo un questionario, il referto del check-up vede sia gli assi del questionario sia quelli fisici (sessioni combinate, #1)', () => { + const db = dbPronto(); + const q = salvaDalForm(db, { + client_code: 'ISL-0001', data: '2026-09-01', eta: 38, + risposte: { q_ore_sonno: 7.5, q_min_addorm: 15, q_risvegli: 0, q_riposato: 8, q_caffeina: 1, q_sonnolenza_diurna: 1 }, + consensoSanitario: 'Si', liberi: {}, + }); + expect(q.ok).toBe(true); + + const c = salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'nicola', + misure: [ + { test_id: 'f_handgrip_kg', valore_num: 48 }, + { test_id: 'f_pushup_rip', valore_num: 24 }, + ], + }); + expect(c.ok).toBe(true); + if (!c.ok) throw new Error(c.errore); + + const referto = refertoDi(db, c.sessioneId); + // 'Recupero & Sistema Nervoso' pesa questionario_sonno (dal questionario di prima, NON + // di questa sessione): la copertura sale sopra zero solo se misureValideAlla porta + // avanti la risposta del 2026-09-01 fino alla sessione del 2026-09-26. + const recupero = referto.assi.find((a) => a.nome === 'Recupero & Sistema Nervoso')!; + expect(recupero.copertura).toBeGreaterThan(0); + // 'Forza & Struttura' vede handgrip e spinta, appena registrati in QUESTA sessione. + const forza = referto.assi.find((a) => a.nome === 'Forza & Struttura')!; + expect(forza.copertura).toBeGreaterThan(0); + }); +}); + +describe('salvaCheckup rifiuta prima di scrivere: nessuna traccia parziale', () => { + it('un test_id sconosciuto al registro: ok:false, nessuna sessione', () => { + const db = dbPronto(); + const esito = salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n', + misure: [{ test_id: 'f_non_esiste', valore_num: 1 }], + }); + expect(esito.ok).toBe(false); + expect((db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }).n).toBe(0); + }); + + it('un q_* del questionario nel check-up: ok:false, nessuna sessione', () => { + const db = dbPronto(); + const esito = salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n', + misure: [{ test_id: 'q_ore_sonno', valore_num: 7 }], + }); + expect(esito.ok).toBe(false); + expect((db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }).n).toBe(0); + }); + + it('nessuna misura: ok:false, nessuna sessione', () => { + const db = dbPronto(); + const esito = salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n', misure: [], + }); + expect(esito.ok).toBe(false); + expect((db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }).n).toBe(0); + }); + + it('un soggetto che non esiste in longevity: ok:false, nessuna sessione', () => { + const db = dbPronto(); + const esito = salvaCheckup(db, { + client_code: 'ISL-9999', data: '2026-09-26', eta: null, operatore: 'n', + misure: [{ test_id: 'f_peso_kg', valore_num: 80 }], + }); + expect(esito.ok).toBe(false); + expect((db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }).n).toBe(0); + }); +}); + +describe('le misure fuori range restano nel fascicolo, marcate e leggibili', () => { + it('una misura oltre il range di plausibilità (f_peso_kg, max 250) si salva, si marca, ed esce da misureFuoriRange', () => { + const db = dbPronto(); + const esito = salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n', + misure: [{ test_id: 'f_peso_kg', valore_num: 500 }], + }); + expect(esito.ok).toBe(true); + if (!esito.ok) throw new Error(esito.errore); + expect(esito.fuoriRange).toEqual(['f_peso_kg']); + + const riga = db.prepare( + `SELECT fuori_range FROM misure WHERE sessione_id = ? AND test_id = 'f_peso_kg'` + ).get(esito.sessioneId) as { fuori_range: number }; + expect(riga.fuori_range).toBe(1); + + const elenco = misureFuoriRange(db, 'ISL-0001'); + expect(elenco.length).toBe(1); + expect(elenco[0]).toMatchObject({ test_id: 'f_peso_kg', valore: 500, data: '2026-09-26' }); + }); +}); + +describe('sessioniDi', () => { + it('elenca le sessioni del cliente, più recente prima, con numero misure e fitness age congelata', () => { + const db = dbPronto(); + salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-01', eta: 38, operatore: 'n', + misure: [{ test_id: 'f_peso_kg', valore_num: 80 }], + }); + const seconda = salvaCheckup(db, { + client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n', + misure: [{ test_id: 'f_handgrip_kg', valore_num: 45 }], + }); + expect(seconda.ok).toBe(true); + + const elenco = sessioniDi(db, 'ISL-0001'); + expect(elenco.length).toBe(2); + expect(elenco[0].data).toBe('2026-09-26'); // la più recente prima + expect(elenco[0].numeroMisure).toBe(1); + // Con una sola misura la copertura di ogni asse resta troppo bassa per una Fitness Age: + // resta null, non un numero congetturato. + expect(elenco[0].fitnessAge).toBeNull(); + }); +}); + +describe('anagrafica per il gestionale, versioni parametrizzate (testabili su :memory:, come codicePerUtente accanto a codicePerUtenteLoggato)', () => { + it('elencoClientiParam ordina per cognome poi nome, e dice chi ha già un accesso', () => { + const id = createIdentityDb(':memory:'); + const lg = createLongevityDb(':memory:'); + const anna = creaCliente(id, lg, { nome: 'Anna', cognome: 'Verdi', sesso: 'F' }); + const bruno = creaCliente(id, lg, { nome: 'Bruno', cognome: 'Rossi', sesso: 'M', user_id: 7 }); + + const elenco = elencoClientiParam(id); + expect(elenco.map((c) => c.client_code)).toEqual([bruno, anna]); // Rossi prima di Verdi + expect(elenco.find((c) => c.client_code === bruno)!.ha_accesso).toBe(true); + expect(elenco.find((c) => c.client_code === anna)!.ha_accesso).toBe(false); + }); + + it('clienteDaCodiceParam restituisce il dettaglio, null se il codice non esiste', () => { + const id = createIdentityDb(':memory:'); + const lg = createLongevityDb(':memory:'); + const code = creaCliente(id, lg, { nome: 'Anna', cognome: 'Verdi', sesso: 'F', email: 'a@b.it' }); + + const dettaglio = clienteDaCodiceParam(id, code); + expect(dettaglio?.email).toBe('a@b.it'); + expect(dettaglio?.ha_accesso).toBe(false); + expect(clienteDaCodiceParam(id, 'ISL-9999')).toBeNull(); + }); + + it('etaDelClienteParam calcola l età alla data, null senza data di nascita in anagrafica', () => { + const id = createIdentityDb(':memory:'); + const lg = createLongevityDb(':memory:'); + const conData = creaCliente(id, lg, { nome: 'A', cognome: 'A', sesso: 'F', data_nascita: '1988-03-04' }); + const senzaData = creaCliente(id, lg, { nome: 'B', cognome: 'B', sesso: 'M' }); + + expect(etaDelClienteParam(id, conData, '2026-08-21')).toBe(38); + expect(etaDelClienteParam(id, senzaData, '2026-08-21')).toBeNull(); + }); +}); + +describe('apriFascicolo, la versione operativa sul singleton', () => { + it('apre un fascicolo nuovo senza mai collegare uno user_id (l accesso è #8, non questa unità)', () => { + const dir = mkdtempSync(join(tmpdir(), 'lg-gestionale-test-')); + const idPath = join(dir, 'identity.db'); + const lgPath = join(dir, 'longevity.db'); + const primaId = process.env.IDENTITY_DB_PATH; + const primaLg = process.env.LONGEVITY_DB_PATH; + try { + // Stesso trucco di tests/longevity/questionario-pagina.test.ts: il singleton di + // getIdentityDb/getLongevityDb si crea alla prima chiamata, quindi le variabili + // d'ambiente vanno impostate PRIMA — e questo deve restare l'unico test del file + // che chiama una funzione dell'anagrafica sul singleton, non su un DB :memory: proprio. + process.env.IDENTITY_DB_PATH = idPath; + process.env.LONGEVITY_DB_PATH = lgPath; + + const code = apriFascicolo({ nome: 'Nuovo', cognome: 'Cliente', sesso: 'F' }); + expect(code).toMatch(/^ISL-\d{4}$/); + + const id = createIdentityDb(idPath); + const riga = id.prepare(`SELECT user_id FROM clienti WHERE client_code = ?`).get(code) as + { user_id: number | null }; + expect(riga.user_id).toBeNull(); + } finally { + if (primaId === undefined) delete process.env.IDENTITY_DB_PATH; else process.env.IDENTITY_DB_PATH = primaId; + if (primaLg === undefined) delete process.env.LONGEVITY_DB_PATH; else process.env.LONGEVITY_DB_PATH = primaLg; + rmSync(dir, { recursive: true, force: true }); + } + }); +}); + +describe('misureDalForm, il parsing puro del modulo di check-up', () => { + const campiAttivi = [ + { test_id: 'f_peso_kg', unita: 'kg' }, + { test_id: 'f_handgrip_kg', unita: 'kg' }, + ]; + + function formCon(dati: Record): FormData { + const f = new FormData(); + for (const [k, v] of Object.entries(dati)) f.set(k, v); + return f; + } + + it('accetta la virgola decimale', () => { + const r = misureDalForm(formCon({ f_peso_kg: '80,5' }), campiAttivi); + expect(r.ok).toBe(true); + if (r.ok) expect(r.misure).toEqual([{ test_id: 'f_peso_kg', valore_num: 80.5, unita: 'kg' }]); + }); + + it('un campo vuoto = non misurato, nessuna misura per quella chiave', () => { + const r = misureDalForm(formCon({ f_peso_kg: '', f_handgrip_kg: '45' }), campiAttivi); + expect(r.ok).toBe(true); + if (r.ok) expect(r.misure.map((m) => m.test_id)).toEqual(['f_handgrip_kg']); + }); + + it('un valore non numerico ferma tutto: ok:false', () => { + const r = misureDalForm(formCon({ f_peso_kg: 'ottanta' }), campiAttivi); + expect(r.ok).toBe(false); + }); + + it('una chiave che non è fra i campi attivi (client_code, un q_*, un refuso) viene ignorata in silenzio', () => { + const r = misureDalForm( + formCon({ client_code: 'ISL-0001', q_ore_sonno: '7', f_peso_kg: '80' }), + campiAttivi + ); + expect(r.ok).toBe(true); + if (r.ok) expect(r.misure.map((m) => m.test_id)).toEqual(['f_peso_kg']); + }); +}); + +describe('canAccessPath: il check-up è del trainer, non del cliente', () => { + it('/api/longevity/gestionale/checkup nega il cliente, ammette il trainer', () => { + expect(canAccessPath('cliente', '/api/longevity/gestionale/checkup')).toBe(false); + expect(canAccessPath('trainer', '/api/longevity/gestionale/checkup')).toBe(true); + }); +});