import { Database } from 'bun:sqlite'; import { initializeDatabase, ensureUserExists } from '../../src/db'; // Import ensureUserExists import { describe, test, expect, beforeEach, afterAll, beforeAll } from 'bun:test'; // Helper function to introduce slight delay for timestamp checks const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); describe('Database', () => { let testDb: Database; // Create an in-memory database before any tests run beforeAll(() => { testDb = new Database(':memory:'); // Enable foreign keys for the test database instance testDb.exec('PRAGMA foreign_keys = ON;'); }); // Close the database connection after all tests have run afterAll(() => { testDb.close(); }); beforeEach(() => { // Reset del esquema entre tests. // Desactivar FKs para poder dropear en cualquier orden, incluyendo tablas nuevas con FKs (p.ej., calendar_tokens). testDb.exec('PRAGMA foreign_keys = OFF;'); // Tablas añadidas en migraciones posteriores (limpieza preventiva) testDb.exec('DROP TABLE IF EXISTS calendar_tokens'); testDb.exec('DROP TABLE IF EXISTS web_sessions'); testDb.exec('DROP TABLE IF EXISTS web_tokens'); testDb.exec('DROP TABLE IF EXISTS allowed_groups'); testDb.exec('DROP TABLE IF EXISTS user_aliases'); testDb.exec('DROP TABLE IF EXISTS user_preferences'); // Tablas base (dependientes primero) testDb.exec('DROP TABLE IF EXISTS task_assignments'); // Drop dependent tables first testDb.exec('DROP TABLE IF EXISTS tasks'); testDb.exec('DROP TABLE IF EXISTS response_queue'); testDb.exec('DROP TABLE IF EXISTS group_members'); testDb.exec('DROP TABLE IF EXISTS groups'); testDb.exec('DROP TABLE IF EXISTS users'); // Reiniciar histórico de migraciones para forzar recreación íntegra testDb.exec('DROP TABLE IF EXISTS schema_migrations'); // Re-activar FKs y re-inicializar el esquema testDb.exec('PRAGMA foreign_keys = ON;'); initializeDatabase(testDb); }); describe('Table Creation', () => { test('should create all required tables in correct order', () => { const tables = testDb .query("SELECT name FROM sqlite_master WHERE type='table'") .all() .map((t: any) => t.name); // Order matters if foreign keys are involved during creation, though SQLite is flexible const expectedTables = ['users', 'groups', 'group_members', 'tasks', 'task_assignments', 'response_queue']; // Filtrar tablas internas y de control de migraciones const userTables = tables.filter(t => !t.startsWith('sqlite_') && t !== 'schema_migrations'); // Check if all expected tables exist (pueden existir tablas adicionales válidas como user_preferences) expectedTables.forEach(table => expect(userTables).toContain(table)); }); }); describe('Table Schemas', () => { test('users table should have correct columns', () => { const columns = testDb .query("PRAGMA table_info(users)") .all() .map((c: any) => c.name); expect(columns).toEqual(['id', 'first_seen', 'last_seen', 'last_command_at']); }); test('tasks table should have required columns', () => { const columns = testDb .query("PRAGMA table_info(tasks)") .all() .map((c: any) => c.name); expect(columns).toContain('description'); expect(columns).toContain('due_date'); expect(columns).toContain('group_id'); expect(columns).toContain('created_by'); }); test('task_assignments table should have correct columns', () => { const columns = testDb .query("PRAGMA table_info(task_assignments)") .all() .map((c: any) => c.name); expect(columns).toEqual(['task_id', 'user_id', 'assigned_by', 'assigned_at']); }); test('groups table should have active flag default to true', () => { testDb.exec(` INSERT INTO groups (id, community_id, name) VALUES ('test-group', 'test-community', 'Test Group') `); const group = testDb.query("SELECT active FROM groups WHERE id = 'test-group'").get(); expect(group.active).toBe(1); // SQLite uses 1 for TRUE }); test('group_members table should have required columns', () => { const columns = testDb .query("PRAGMA table_info(group_members)") .all() .map((c: any) => c.name); expect(columns).toEqual([ 'group_id', 'user_id', 'is_admin', 'is_active', 'first_seen_at', 'last_seen_at', 'last_role_change_at' ]); }); test('response_queue table should have required columns (at least base set)', () => { const columns = testDb .query("PRAGMA table_info(response_queue)") .all() .map((c: any) => c.name); const expectedBase = ['id', 'recipient', 'message', 'status', 'attempts', 'last_error', 'metadata', 'created_at', 'updated_at']; expectedBase.forEach(col => expect(columns).toContain(col)); }); }); describe('Foreign Keys', () => { test('tasks should reference users via created_by', () => { const fkInfo = testDb.query("PRAGMA foreign_key_list(tasks)").all(); const createdByFk = fkInfo.find(fk => fk.from === 'created_by'); expect(createdByFk).toBeDefined(); expect(createdByFk.table).toBe('users'); expect(createdByFk.to).toBe('id'); }); test('tasks should reference groups via group_id', () => { const fkInfo = testDb.query("PRAGMA foreign_key_list(tasks)").all(); const groupIdFk = fkInfo.find(fk => fk.from === 'group_id'); expect(groupIdFk).toBeDefined(); expect(groupIdFk.table).toBe('groups'); expect(groupIdFk.to).toBe('id'); expect(groupIdFk.on_delete).toBe('SET NULL'); }); test('task_assignments should reference tasks via task_id', () => { const fkInfo = testDb.query("PRAGMA foreign_key_list(task_assignments)").all(); const taskIdFk = fkInfo.find(fk => fk.from === 'task_id'); expect(taskIdFk).toBeDefined(); expect(taskIdFk.table).toBe('tasks'); expect(taskIdFk.to).toBe('id'); expect(taskIdFk.on_delete).toBe('CASCADE'); }); test('task_assignments should reference users via user_id', () => { const fkInfo = testDb.query("PRAGMA foreign_key_list(task_assignments)").all(); const userIdFk = fkInfo.find(fk => fk.from === 'user_id'); expect(userIdFk).toBeDefined(); expect(userIdFk.table).toBe('users'); expect(userIdFk.to).toBe('id'); expect(userIdFk.on_delete).toBe('CASCADE'); }); test('task_assignments should reference users via assigned_by', () => { const fkInfo = testDb.query("PRAGMA foreign_key_list(task_assignments)").all(); const assignedByFk = fkInfo.find(fk => fk.from === 'assigned_by'); expect(assignedByFk).toBeDefined(); expect(assignedByFk.table).toBe('users'); expect(assignedByFk.to).toBe('id'); expect(assignedByFk.on_delete).toBe('CASCADE'); }); test('group_members should reference groups and users via FKs', () => { const fkInfo = testDb.query("PRAGMA foreign_key_list(group_members)").all(); const groupFk = fkInfo.find((fk: any) => fk.from === 'group_id'); const userFk = fkInfo.find((fk: any) => fk.from === 'user_id'); expect(groupFk).toBeDefined(); expect(groupFk.table).toBe('groups'); expect(groupFk.to).toBe('id'); expect(groupFk.on_delete).toBe('CASCADE'); expect(userFk).toBeDefined(); expect(userFk.table).toBe('users'); expect(userFk.to).toBe('id'); expect(userFk.on_delete).toBe('CASCADE'); }); test('should prevent inserting group_members with non-existent FKs', () => { expect(() => { testDb.prepare(` INSERT INTO group_members (group_id, user_id, is_admin) VALUES ('nonexistent-group', 'nonexistent-user', 0) `).run(); }).toThrow(); }); test('deleting a group cascades to group_members', () => { // Arrange: create user and group and membership testDb.exec(`INSERT INTO users (id) VALUES ('user-x')`); testDb.exec(`INSERT INTO groups (id, community_id, name) VALUES ('group-x', 'comm', 'Group X')`); testDb.exec(`INSERT INTO group_members (group_id, user_id, is_admin) VALUES ('group-x', 'user-x', 0)`); // Ensure membership exists let count = testDb.query(`SELECT COUNT(*) as c FROM group_members WHERE group_id='group-x' AND user_id='user-x'`).get() as any; expect(Number(count.c)).toBe(1); // Act: delete group testDb.exec(`DELETE FROM groups WHERE id='group-x'`); // Assert: membership is gone due to ON DELETE CASCADE count = testDb.query(`SELECT COUNT(*) as c FROM group_members WHERE group_id='group-x' AND user_id='user-x'`).get() as any; expect(Number(count.c)).toBe(0); }); test('should prevent inserting task with non-existent user', () => { expect(() => { testDb.prepare(` INSERT INTO tasks (description, created_by) VALUES ('Task for non-existent user', 'nonexistentuser') `).run(); }).toThrow(); // Foreign key constraint failure }); test('should prevent inserting assignment with non-existent user', () => { // Need a valid user and task first testDb.exec(`INSERT INTO users (id) VALUES ('user1')`); const taskResult = testDb.prepare(`INSERT INTO tasks (description, created_by) VALUES ('Test Task', 'user1') RETURNING id`).get(); const taskId = taskResult.id; expect(() => { testDb.prepare(` INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, 'nonexistentuser', 'user1') `).run(taskId); }).toThrow(); // Foreign key constraint failure on user_id expect(() => { testDb.prepare(` INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, 'user1', 'nonexistentassigner') `).run(taskId); }).toThrow(); // Foreign key constraint failure on assigned_by }); }); describe('ensureUserExists', () => { const rawUserId = '1234567890@s.whatsapp.net'; const normalizedId = '1234567890'; test('should create a new user if they do not exist', () => { const resultId = ensureUserExists(rawUserId, testDb); expect(resultId).toBe(normalizedId); const user = testDb.query("SELECT * FROM users WHERE id = ?").get(normalizedId); expect(user).toBeDefined(); expect(user.id).toBe(normalizedId); expect(user.first_seen).toBeDefined(); expect(user.last_seen).toBe(user.first_seen); // Initially they are the same }); test('should update last_seen for an existing user', async () => { // First call creates the user ensureUserExists(rawUserId, testDb); const initialUser = testDb.query("SELECT * FROM users WHERE id = ?").get(normalizedId); const initialLastSeenStr = initialUser.last_seen; const initialLastSeenMs = Date.parse(initialLastSeenStr); // Convert to milliseconds // Wait a bit to ensure timestamp potentially changes // Increase sleep slightly to make change more likely even if DB is fast await sleep(100); // Second call should update last_seen const resultId = ensureUserExists(rawUserId, testDb); expect(resultId).toBe(normalizedId); const updatedUser = testDb.query("SELECT * FROM users WHERE id = ?").get(normalizedId); const updatedLastSeenStr = updatedUser.last_seen; const updatedLastSeenMs = Date.parse(updatedLastSeenStr); // Convert to milliseconds expect(updatedLastSeenStr).toBeDefined(); expect(updatedUser.first_seen).toBe(initialUser.first_seen); // first_seen should NOT be updated // Compare timestamps numerically (milliseconds since epoch) // It should be greater than or equal to the initial one. // Using '>' is safer if the sleep guarantees a change. expect(updatedLastSeenMs).toBeGreaterThan(initialLastSeenMs); // Keep the string check just in case, but the numerical one is more reliable here // This might still fail occasionally if the calls land in the exact same second. // expect(updatedLastSeenStr).not.toBe(initialLastSeenStr); }); test('should return the normalized ID on success', () => { const resultId = ensureUserExists('9876543210@s.whatsapp.net', testDb); expect(resultId).toBe('9876543210'); }); test('should return null for invalid raw user ID', () => { // Suppress console.error during this specific test if needed, or just check output const resultId = ensureUserExists('invalid-id!', testDb); expect(resultId).toBeNull(); const userCount = testDb.query("SELECT COUNT(*) as count FROM users").get(); expect(userCount.count).toBe(0); // No user should be created }); test('should return null for null input', () => { const resultId = ensureUserExists(null, testDb); expect(resultId).toBeNull(); }); test('should return null for undefined input', () => { const resultId = ensureUserExists(undefined, testDb); expect(resultId).toBeNull(); }); test('should handle user ID with participant correctly', () => { const resultId = ensureUserExists('1122334455:12@s.whatsapp.net', testDb); expect(resultId).toBe('1122334455'); const user = testDb.query("SELECT * FROM users WHERE id = ?").get('1122334455'); expect(user).toBeDefined(); }); }); // Keep existing Data Operations tests, but ensure users/groups are created first describe('Data Operations (with user checks)', () => { const user1 = '34650112233'; const group1 = 'test-group-123'; beforeEach(() => { // Ensure users and groups exist before task operations ensureUserExists(`${user1}@s.whatsapp.net`, testDb); testDb.exec(` INSERT OR IGNORE INTO groups (id, community_id, name) VALUES (?, 'test-community', 'Test Group') `, [group1]); }); test('should allow inserting group tasks with existing user and group', () => { const result = testDb.prepare(` INSERT INTO tasks (description, created_by, group_id) VALUES ('Test task', ?, ?) `).run(user1, group1); expect(result.changes).toBe(1); }); test('should allow inserting private tasks with existing user', () => { const result = testDb.prepare(` INSERT INTO tasks (description, created_by) VALUES ('Private task', ?) `).run(user1); expect(result.changes).toBe(1); }); }); });