import { describe, it, expect } from 'vitest'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createDb } from '../src/lib/db'; import { createUser } from '../src/lib/auth'; describe('schema content_blocks e ruoli', () => { it('crea la tabella content_blocks', () => { const db = createDb(':memory:'); const cols = db.prepare(`PRAGMA table_info(content_blocks)`).all() as { name: string }[]; expect(cols.map((c) => c.name)).toEqual( expect.arrayContaining(['tag', 'type', 'value', 'styles', 'updated_at', 'updated_by']) ); }); it('rifiuta type non valido', () => { const db = createDb(':memory:'); expect(() => db.prepare(`INSERT INTO content_blocks (tag, type, value) VALUES ('x.y', 'video', '')`).run() ).toThrow(); }); it('users ha colonna role con default admin', () => { const db = createDb(':memory:'); db.prepare(`INSERT INTO users (username, password_hash) VALUES ('a', 'h')`).run(); const row = db.prepare(`SELECT role FROM users WHERE username = 'a'`).get() as { role: string }; expect(row.role).toBe('admin'); }); it('migra i tag programmi → servizi preservando i valori modificati dal pannello', () => { const dir = mkdtempSync(join(tmpdir(), 'il-db-test-')); const path = join(dir, 'test.db'); try { // Simula un DB di produzione pre-rinomina: tag vecchi con valori modificati dal pannello. const db1 = createDb(path); const old = db1.transaction(() => { db1.prepare("DELETE FROM content_blocks WHERE tag IN ('home.services.title', 'service.rehab.title', 'services.hero.title', 'services.meta.title', 'footer.services.1.label', 'footer.col.services-title')").run(); const ins = db1.prepare("INSERT INTO content_blocks (tag, type, value, updated_by) VALUES (?, 'text', ?, 'editor')"); ins.run('home.programs.title', 'Titolo modificato'); ins.run('program.rehab.title', 'Rehab modificato'); ins.run('programs.hero.title', 'Hero modificato'); ins.run('footer.programs.1.label', 'Label modificata'); ins.run('footer.col.programs-title', 'Colonna modificata'); // Riga mai toccata dal pannello: valore identico al vecchio seed → deve prendere il nuovo default. db1.prepare("INSERT INTO content_blocks (tag, type, value) VALUES ('programs.meta.title', 'text', 'Programmi')").run(); db1.prepare("UPDATE content_blocks SET value = 'Programmi' WHERE tag = 'global.nav.2.label'").run(); }); old(); db1.close(); // Riapertura: la migrazione rinomina i tag preservando valore e updated_by. const db2 = createDb(path); const val = (tag: string) => db2.prepare('SELECT value, updated_by FROM content_blocks WHERE tag = ?').get(tag) as { value: string; updated_by: string | null } | undefined; expect(val('home.services.title')).toMatchObject({ value: 'Titolo modificato', updated_by: 'editor' }); expect(val('service.rehab.title')).toMatchObject({ value: 'Rehab modificato', updated_by: 'editor' }); expect(val('services.hero.title')).toMatchObject({ value: 'Hero modificato', updated_by: 'editor' }); expect(val('footer.services.1.label')).toMatchObject({ value: 'Label modificata', updated_by: 'editor' }); expect(val('footer.col.services-title')).toMatchObject({ value: 'Colonna modificata', updated_by: 'editor' }); // Le righe rimaste al vecchio valore di default prendono il nuovo ("Servizi"). // Dopo il riordino del menu (commit 5ff4771) la voce "Servizi" è la 2ª: la migrazione // aggiorna global.nav.2.label da 'Programmi' al seed corrente 'Servizi'. expect(val('services.meta.title')).toMatchObject({ value: 'Servizi' }); expect(val('global.nav.2.label')).toMatchObject({ value: 'Servizi' }); // La riga rinominata ma modificata dal pannello resta invariata (già verificata sopra): // home.services.title = 'Titolo modificato'. // Nessun tag vecchio residuo. const residui = db2.prepare( "SELECT count(*) AS c FROM content_blocks WHERE tag LIKE 'program%' OR tag LIKE 'home.programs%' OR tag LIKE 'footer.programs%' OR tag = 'footer.col.programs-title'" ).get() as { c: number }; expect(residui.c).toBe(0); // Idempotente: una terza apertura non altera i valori. db2.close(); const db3 = createDb(path); const again = db3.prepare('SELECT value FROM content_blocks WHERE tag = ?').get('home.services.title') as { value: string }; expect(again.value).toBe('Titolo modificato'); db3.close(); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('allinea i bottoni promo col vecchio testo-freccia al seed senza freccia', () => { const dir = mkdtempSync(join(tmpdir(), 'il-db-promo-')); const path = join(dir, 'test.db'); try { // DB pre-refactor: valori con freccia letterale e maiuscolo (poi spostati nel CSS). const db1 = createDb(path); db1.prepare("UPDATE content_blocks SET value = 'LE NOSTRE PROPOSTE →' WHERE tag = 'promo.hero.cta'").run(); db1.prepare("UPDATE content_blocks SET value = 'ACQUISTA →' WHERE tag = 'promo.buy-label'").run(); // Riga modificata dal pannello: NON deve essere toccata. db1.prepare("UPDATE content_blocks SET value = 'Compra ora', updated_by = 'editor' WHERE tag = 'promo.buy-label'").run(); db1.close(); // Riapertura: la migrazione allinea solo le righe rimaste al vecchio default con freccia. const db2 = createDb(path); const val = (tag: string) => (db2.prepare('SELECT value FROM content_blocks WHERE tag = ?').get(tag) as { value: string }).value; expect(val('promo.hero.cta')).toBe('Le nostre proposte'); // promo.buy-label era stato editato dal pannello → resta invariato. expect(val('promo.buy-label')).toBe('Compra ora'); db2.close(); // Idempotente: terza apertura non altera. const db3 = createDb(path); expect((db3.prepare("SELECT value FROM content_blocks WHERE tag = 'promo.hero.cta'").get() as { value: string }).value).toBe('Le nostre proposte'); db3.close(); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('riporta il titolo del servizio a "Class" (inverte Performance Class)', () => { const dir = mkdtempSync(join(tmpdir(), 'il-db-pc-')); const path = join(dir, 'test.db'); try { // DB con il vecchio valore normalizzato "Performance Class". const db1 = createDb(path); db1.prepare("UPDATE content_blocks SET value = 'Performance Class', updated_by = NULL WHERE tag = 'service.performance-class.title'").run(); db1.close(); const db2 = createDb(path); const val = (tag: string) => (db2.prepare('SELECT value FROM content_blocks WHERE tag = ?').get(tag) as { value: string }).value; expect(val('service.performance-class.title')).toBe('Class'); db2.close(); // Non deve toccare un valore modificato dal pannello. const db3 = createDb(path); db3.prepare("UPDATE content_blocks SET value = 'Titolo mio', updated_by = 'admin' WHERE tag = 'service.performance-class.title'").run(); db3.close(); const db4 = createDb(path); expect((db4.prepare("SELECT value FROM content_blocks WHERE tag = 'service.performance-class.title'").get() as { value: string }).value).toBe('Titolo mio'); db4.close(); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('rimuove i tag orfani training.performance-class.* (riquadro rimosso)', () => { const dir = mkdtempSync(join(tmpdir(), 'il-db-orf-')); const path = join(dir, 'test.db'); try { // DB con i tag orfani del vecchio riquadro performance-class. const db1 = createDb(path); const ins = db1.prepare("INSERT OR REPLACE INTO content_blocks (tag, type, value, guid) VALUES (?, 'text', ?, ?)"); ins.run('training.performance-class.title', 'Holistic Class', 'g1'); ins.run('training.performance-class.caption', 'Classi fino a 8 persone', 'g2'); db1.close(); const db2 = createDb(path); const count = (db2.prepare("SELECT count(*) c FROM content_blocks WHERE tag LIKE 'training.performance-class.%'").get() as { c: number }).c; expect(count).toBe(0); // I riquadri validi restano. expect((db2.prepare("SELECT count(*) c FROM content_blocks WHERE tag = 'training.holistic-class.title'").get() as { c: number }).c).toBe(1); db2.close(); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('migra un DB esistente senza colonna role', () => { // simula DB vecchio: createDb due volte sullo stesso path in-memory non è possibile, // quindi si verifica che la migrazione sia idempotente (doppia esecuzione non lancia) const db = createDb(':memory:'); expect(() => createDb(':memory:')).not.toThrow(); expect(db.prepare(`PRAGMA table_info(users)`).all().length).toBeGreaterThanOrEqual(4); }); }); describe('migrazioni ruoli/guid/autore', () => { it('ogni content_block ha un guid non vuoto e univoco', () => { const db = createDb(':memory:'); const rows = db.prepare('SELECT guid FROM content_blocks').all() as { guid: string | null }[]; expect(rows.length).toBeGreaterThan(100); expect(rows.every((r) => typeof r.guid === 'string' && r.guid!.length >= 10)).toBe(true); const guids = new Set(rows.map((r) => r.guid)); expect(guids.size).toBe(rows.length); }); it('lo statement di migrazione unifica editor in superuser', () => { // La migrazione gira dentro createDb, ma :memory: non persiste tra due boot: qui // verifichiamo l'invariante SQL applicata dalla migrazione (editor → superuser). // L'end-to-end su DB pre-esistente è coperto dallo smoke di produzione (Task 15). const db = createDb(':memory:'); db.prepare("INSERT INTO users (username, password_hash, role) VALUES ('vecchio', 'x', 'editor')").run(); db.prepare("UPDATE users SET role = 'superuser' WHERE role = 'editor'").run(); const row = db.prepare("SELECT role FROM users WHERE username = 'vecchio'").get() as { role: string }; expect(row.role).toBe('superuser'); }); it('posts ha la colonna author_id', () => { const db = createDb(':memory:'); const cols = db.prepare('PRAGMA table_info(posts)').all() as { name: string }[]; expect(cols.some((c) => c.name === 'author_id')).toBe(true); }); });