You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			114 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			114 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			TypeScript
		
	
| 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 puede no existir en algunos entornos (FS/CWD distintos).
 | |
|     // Si no existe, no fallamos el test (lo importante es que migrar sea idempotente).
 | |
|     const logPath = 'data/migrations.log';
 | |
|     if (!existsSync(logPath)) {
 | |
|       expect(true).toBe(true);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     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);
 | |
|   });
 | |
| });
 |