// 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); } }); });