From 37b1a316d82ec5824ba1a3726fa86643113f29c4 Mon Sep 17 00:00:00 2001 From: "Adriano (daitarn)" Date: Sun, 27 Sep 2026 22:22:40 +0200 Subject: [PATCH] longevity: test unitari giornale di bordo per cliente Aggiunta/lettura, rifiuti di aggiungiVoce, cancellaVoce (autore/admin/altro trainer/id inesistente), schema idempotente, nessuna pagina cliente importa giornale.ts. Refs #16 Co-Authored-By: Claude Sonnet 5 --- .../tests/longevity/giornale.test.ts | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 apps/platforms/tests/longevity/giornale.test.ts diff --git a/apps/platforms/tests/longevity/giornale.test.ts b/apps/platforms/tests/longevity/giornale.test.ts new file mode 100644 index 0000000..58b2977 --- /dev/null +++ b/apps/platforms/tests/longevity/giornale.test.ts @@ -0,0 +1,150 @@ +// Issue #16: giornale di bordo per cliente (solo staff, nessuna voce automatica, si +// aggiunge e si cancella, non si modifica). +import { describe, it, expect } from 'vitest'; +import { createLongevityDb } from '../../src/lib/longevity/db'; +import { vociDelGiornale, aggiungiVoce, cancellaVoce } from '../../src/lib/longevity/giornale'; + +function dbPronto() { + const db = createLongevityDb(':memory:'); + db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001', 'F')`).run(); + return db; +} + +describe('aggiungiVoce e vociDelGiornale', () => { + it('aggiunge e legge le voci, più recente prima (data DESC, a parità id DESC), a capo conservati', () => { + const db = dbPronto(); + + const a = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: 'Prima nota' }); + const b = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-10', autore: 'donata', testo: 'Riga uno\nRiga due' }); + const c = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-10', autore: 'nicola', testo: 'Stessa data, dopo' }); + + expect(a.ok).toBe(true); + expect(b.ok).toBe(true); + expect(c.ok).toBe(true); + + const voci = vociDelGiornale(db, 'ISL-0001'); + expect(voci.map((v) => v.testo)).toEqual(['Stessa data, dopo', 'Riga uno\nRiga due', 'Prima nota']); + expect(voci[1].testo).toBe('Riga uno\nRiga due'); // a capo preservati + expect(voci[0].autore).toBe('nicola'); + }); +}); + +describe('aggiungiVoce: rifiuti, nessuna riga scritta', () => { + const contaRighe = (db: ReturnType) => + (db.prepare(`SELECT COUNT(*) n FROM giornale`).get() as { n: number }).n; + + it('rifiuta testo vuoto', () => { + const db = dbPronto(); + const esito = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: '' }); + expect(esito.ok).toBe(false); + expect(contaRighe(db)).toBe(0); + }); + + it('rifiuta testo di soli spazi', () => { + const db = dbPronto(); + const esito = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: ' \n ' }); + expect(esito.ok).toBe(false); + expect(contaRighe(db)).toBe(0); + }); + + it('rifiuta testo troppo lungo', () => { + const db = dbPronto(); + const esito = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: 'x'.repeat(5001) }); + expect(esito.ok).toBe(false); + expect(contaRighe(db)).toBe(0); + }); + + it('rifiuta una data nel futuro', () => { + const db = dbPronto(); + const domani = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().slice(0, 10); + const esito = aggiungiVoce(db, { clientCode: 'ISL-0001', data: domani, autore: 'nicola', testo: 'Nota' }); + expect(esito.ok).toBe(false); + expect(contaRighe(db)).toBe(0); + }); + + it('rifiuta una data malformata', () => { + const db = dbPronto(); + const esito = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '01-09-2026', autore: 'nicola', testo: 'Nota' }); + expect(esito.ok).toBe(false); + expect(contaRighe(db)).toBe(0); + }); + + it('rifiuta un cliente inesistente', () => { + const db = dbPronto(); + const esito = aggiungiVoce(db, { clientCode: 'ISL-9999', data: '2026-09-01', autore: 'nicola', testo: 'Nota' }); + expect(esito.ok).toBe(false); + expect(contaRighe(db)).toBe(0); + }); +}); + +describe('cancellaVoce', () => { + it('l\'autore può cancellare la propria voce', () => { + const db = dbPronto(); + const a = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: 'Nota' }); + if (!a.ok) throw new Error('setup fallito'); + + const esito = cancellaVoce(db, { id: a.id, utente: 'nicola', ruolo: 'trainer' }); + expect(esito.ok).toBe(true); + if (esito.ok) expect(esito.clientCode).toBe('ISL-0001'); + expect(vociDelGiornale(db, 'ISL-0001')).toEqual([]); + }); + + it('un admin può cancellare la voce di un altro', () => { + const db = dbPronto(); + const a = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: 'Nota' }); + if (!a.ok) throw new Error('setup fallito'); + + const esito = cancellaVoce(db, { id: a.id, utente: 'adriano', ruolo: 'admin' }); + expect(esito.ok).toBe(true); + expect(vociDelGiornale(db, 'ISL-0001')).toEqual([]); + }); + + it('un altro trainer non può cancellare la voce di un collega', () => { + const db = dbPronto(); + const a = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: 'Nota' }); + if (!a.ok) throw new Error('setup fallito'); + + const esito = cancellaVoce(db, { id: a.id, utente: 'donata', ruolo: 'trainer' }); + expect(esito.ok).toBe(false); + expect(vociDelGiornale(db, 'ISL-0001').length).toBe(1); // nessuna scrittura + }); + + it('rifiuta un id inesistente', () => { + const db = dbPronto(); + const esito = cancellaVoce(db, { id: 999999, utente: 'nicola', ruolo: 'trainer' }); + expect(esito.ok).toBe(false); + }); +}); + +describe('schema: la tabella giornale si crea su un longevity.db preesistente senza di essa', () => { + it('CREATE TABLE IF NOT EXISTS basta: nessuna migrazione a mano serve', () => { + // db.ts crea la tabella con CREATE TABLE IF NOT EXISTS nello stesso blocco SCHEMA_LONGEVITY + // delle altre tabelle: su un file che le ha già (come questo, aperto due volte) la seconda + // apertura è un'operazione a vuoto, esattamente come per soggetti o sessioni — non serve + // una migra() dedicata perché non c'è una tabella preesistente da alterare, solo una nuova + // da creare la prima volta che manca. + const db = createLongevityDb(':memory:'); + db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001', 'F')`).run(); + const esito = aggiungiVoce(db, { clientCode: 'ISL-0001', data: '2026-09-01', autore: 'nicola', testo: 'Nota' }); + expect(esito.ok).toBe(true); + + // Riaprire lo stesso schema (qui, un nuovo :memory:, ma stesso SCHEMA_LONGEVITY) non deve + // fallire: la tabella è già presente, IF NOT EXISTS salta la creazione. + expect(() => createLongevityDb(':memory:')).not.toThrow(); + }); +}); + +describe('nessuna pagina cliente importa il giornale', () => { + it('io.astro, progressione.astro e questionario.astro non fanno riferimento a giornale.ts', () => { + // Stile di anagrafica.ts: controllo testuale sui file, non sull'albero delle import — + // basta a catturare un'importazione diretta, che è l'unico modo in cui il cliente + // potrebbe finire per vedere il giornale di bordo (solo staff, #16). + const fs = require('node:fs') as typeof import('node:fs'); + const path = require('node:path') as typeof import('node:path'); + const pagine = ['io.astro', 'progressione.astro', 'questionario.astro']; + for (const p of pagine) { + const src = fs.readFileSync(path.join(process.cwd(), 'src/pages/longevity', p), 'utf8'); + expect(src, `${p} non deve importare giornale.ts`).not.toMatch(/giornale/i); + } + }); +});