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>
This commit is contained in:
@@ -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<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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user