From 70688df94820ba55d2ba87ea08b254d2cc4bf0f5 Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 8 Sep 2025 12:32:15 +0200 Subject: [PATCH] =?UTF-8?q?test:=20a=C3=B1ade=20tests=20para=20Migrator=20?= =?UTF-8?q?y=20initializeDatabase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- tests/unit/migrator.test.ts | 108 ++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/unit/migrator.test.ts diff --git a/tests/unit/migrator.test.ts b/tests/unit/migrator.test.ts new file mode 100644 index 0000000..92e192d --- /dev/null +++ b/tests/unit/migrator.test.ts @@ -0,0 +1,108 @@ +import { describe, test, expect } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { Migrator } from '../../src/db/migrator'; +import { initializeDatabase } from '../../src/db'; +import { readFileSync, existsSync } from 'fs'; + +describe('Migrator', () => { + test('aplica todas las migraciones en una DB vacía', () => { + const mem = new Database(':memory:'); + // No baseline; sin backup en tests + Migrator.migrateToLatest(mem, { withBackup: false, allowBaseline: false }); + + const tables = mem.query("SELECT name FROM sqlite_master WHERE type='table'").all().map((r: any) => String(r.name)); + // Ignorar tablas internas de sqlite_ + const userTables = tables.filter(n => !n.startsWith('sqlite_')).sort(); + + // Deben existir todas las tablas clave + const expected = ['schema_migrations', 'users', 'groups', 'tasks', 'task_assignments', 'response_queue', 'user_preferences', 'group_members'].sort(); + + expected.forEach(t => expect(userTables).toContain(t)); + }); + + test('con esquema parcial (p.ej. users/response_queue), crea lo que falta (p.ej. groups)', () => { + const mem = new Database(':memory:'); + + // Crear solo parte del esquema (parcial). Evitamos FKs para no depender de otras tablas. + mem.exec(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + first_seen TEXT, + last_seen TEXT + ); + `); + mem.exec(` + CREATE TABLE IF NOT EXISTS response_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + recipient TEXT NOT NULL, + message TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'queued', + attempts INTEGER NOT NULL DEFAULT 0, + last_error TEXT NULL, + metadata TEXT NULL, + created_at TEXT, + updated_at TEXT + ); + `); + + // Migrar sin baseline para asegurar que se ejecuta v1 y siguientes + Migrator.migrateToLatest(mem, { withBackup: false, allowBaseline: false }); + + // Verificar que ahora existe 'groups' y el resto + const hasGroups = !!mem.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='groups'`).get(); + expect(hasGroups).toBe(true); + + const tables = mem.query("SELECT name FROM sqlite_master WHERE type='table'").all().map((r: any) => String(r.name)); + ['tasks', 'task_assignments', 'user_preferences', 'group_members'].forEach(t => { + expect(tables).toContain(t); + }); + }); + + test('checksum estricto: si una migración aplicada se altera, el migrador aborta', () => { + const mem = new Database(':memory:'); + Migrator.migrateToLatest(mem, { withBackup: false, allowBaseline: false }); + + // Corromper checksum de v1 + mem.exec(`UPDATE schema_migrations SET checksum = 'tampered' WHERE version = 1`); + + // Llamar de nuevo debe lanzar por mismatch (MIGRATOR_CHECKSUM_STRICT por defecto es true) + expect(() => { + Migrator.migrateToLatest(mem, { withBackup: false, allowBaseline: false }); + }).toThrow(); + }); + + test('escribe log persistente: migrations.log contiene eventos de arranque y no_pending tras segunda pasada', () => { + const mem = new Database(':memory:'); + + // Primera ejecución: aplica migraciones + Migrator.migrateToLatest(mem, { withBackup: false, allowBaseline: false }); + // Segunda ejecución: no hay pendientes + Migrator.migrateToLatest(mem, { withBackup: false, allowBaseline: false }); + + // El fichero debe existir y contener eventos conocidos + const logPath = 'data/migrations.log'; + expect(existsSync(logPath)).toBe(true); + const content = readFileSync(logPath, 'utf-8'); + expect(content).toContain('"event":"startup_summary"'); + expect(content).toContain('"event":"no_pending"'); + }); +}); + +describe('initializeDatabase', () => { + test('activa PRAGMA foreign_keys=ON y deja la DB migrada', () => { + const mem = new Database(':memory:'); + + // No habilitamos FK manualmente aquí; initializeDatabase debe hacerlo + initializeDatabase(mem); + + // Verificar FK ON + const row = mem.query(`PRAGMA foreign_keys`).get() as any; + // Dependiendo de la versión, el campo puede llamarse 'foreign_keys' o 'value' + const fkOn = Number((row && (row.foreign_keys ?? row.value ?? row['foreign_keys'])) || 0); + expect(fkOn).toBe(1); + + // Y que exista una tabla clave creada por migraciones + const hasUsers = !!mem.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='users'`).get(); + expect(hasUsers).toBe(true); + }); +});