Files
InsanityLab-web/apps/platforms/tests/longevity/giornale.test.ts
T
Adriano 37b1a316d8 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 <noreply@anthropic.com>
2026-09-27 22:22:40 +02:00

151 lines
6.8 KiB
TypeScript

// 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<typeof dbPronto>) =>
(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);
}
});
});