From e7d3596005402c4ab390f6c2977381786430a5f5 Mon Sep 17 00:00:00 2001 From: borja Date: Sun, 7 Sep 2025 17:25:52 +0200 Subject: [PATCH] =?UTF-8?q?test:=20actualiza=20pruebas=20unitarias=20de=20?= =?UTF-8?q?BD=20a=20nuevas=20tablas=20y=20a=C3=B1ade=20tests?= 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/db.test.ts | 67 ++++++++++- .../unit/services/group-sync.members.test.ts | 108 ++++++++++++++++++ 2 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 tests/unit/services/group-sync.members.test.ts diff --git a/tests/unit/db.test.ts b/tests/unit/db.test.ts index 9382981..4f0df5f 100644 --- a/tests/unit/db.test.ts +++ b/tests/unit/db.test.ts @@ -21,11 +21,13 @@ describe('Database', () => { }); beforeEach(() => { - // Reset database schema between tests by dropping tables and re-initializing + // Reset database schema between tests by dropping tables and re-initializing (respect FKs) 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 users'); + 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'); // Initialize schema on the test database instance initializeDatabase(testDb); }); @@ -38,7 +40,7 @@ describe('Database', () => { .map((t: any) => t.name); // Order matters if foreign keys are involved during creation, though SQLite is flexible - const expectedTables = ['users', 'groups', 'tasks', 'task_assignments', 'response_queue']; + const expectedTables = ['users', 'groups', 'group_members', 'tasks', 'task_assignments', 'response_queue']; const userTables = tables.filter(t => !t.startsWith('sqlite_')); // Check if all expected tables exist, order might vary slightly depending on execution expect(userTables).toHaveLength(expectedTables.length); @@ -83,6 +85,22 @@ describe('Database', () => { 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)") @@ -138,6 +156,49 @@ describe('Database', () => { 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(` diff --git a/tests/unit/services/group-sync.members.test.ts b/tests/unit/services/group-sync.members.test.ts new file mode 100644 index 0000000..aee35db --- /dev/null +++ b/tests/unit/services/group-sync.members.test.ts @@ -0,0 +1,108 @@ +import { Database } from 'bun:sqlite'; +import { beforeAll, beforeEach, afterAll, describe, expect, test } from 'bun:test'; +import { initializeDatabase } from '../../../src/db'; +import { GroupSyncService } from '../../../src/services/group-sync'; + +describe('GroupSyncService - reconcileGroupMembers', () => { + let memdb: Database; + + beforeAll(() => { + memdb = new Database(':memory:'); + memdb.exec('PRAGMA foreign_keys = ON;'); + initializeDatabase(memdb); + // Inyectar DB en el servicio + GroupSyncService.dbInstance = memdb as any; + }); + + afterAll(() => { + memdb.close(); + }); + + beforeEach(() => { + // Limpiar tablas relevantes entre tests + memdb.exec('DELETE FROM group_members'); + memdb.exec('DELETE FROM users'); + memdb.exec('DELETE FROM groups'); + + // Crear grupo base activo + memdb.prepare(`INSERT INTO groups (id, community_id, name) VALUES (?, ?, ?)`) + .run('123@g.us', 'community-1', 'Grupo 123'); + }); + + function getMember(userId: string) { + return memdb.prepare(` + SELECT group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at, last_role_change_at + FROM group_members + WHERE group_id = ? AND user_id = ? + `).get('123@g.us', userId) as any; + } + + test('inserta miembros y marca activos en la primera reconciliación', () => { + const res = GroupSyncService.reconcileGroupMembers('123@g.us', [ + { userId: '111', isAdmin: true }, + { userId: '222', isAdmin: false } + ], '2025-01-01 00:00:00.000'); + + expect(res).toEqual({ added: 2, updated: 0, deactivated: 0 }); + + const m1 = getMember('111'); + const m2 = getMember('222'); + + expect(m1).toBeDefined(); + expect(m1.is_active).toBe(1); + expect(m1.is_admin).toBe(1); + expect(m1.first_seen_at).toBe('2025-01-01 00:00:00.000'); + expect(m1.last_seen_at).toBe('2025-01-01 00:00:00.000'); + + expect(m2).toBeDefined(); + expect(m2.is_active).toBe(1); + expect(m2.is_admin).toBe(0); + }); + + test('actualiza roles, desactiva ausentes y añade nuevos en reconciliaciones posteriores', () => { + // Primera pasada + GroupSyncService.reconcileGroupMembers('123@g.us', [ + { userId: '111', isAdmin: true }, + { userId: '222', isAdmin: false } + ], '2025-01-01 00:00:00.000'); + + // Segunda pasada con cambios: 111 pierde admin, 222 desaparece, 333 aparece + const res2 = GroupSyncService.reconcileGroupMembers('123@g.us', [ + { userId: '111', isAdmin: false }, + { userId: '333', isAdmin: false } + ], '2025-01-02 00:00:00.000'); + + expect(res2).toEqual({ added: 1, updated: 1, deactivated: 1 }); + + const m111 = getMember('111'); + const m222 = getMember('222'); + const m333 = getMember('333'); + + expect(m111.is_active).toBe(1); + expect(m111.is_admin).toBe(0); + expect(m111.last_role_change_at).toBe('2025-01-02 00:00:00.000'); + expect(m222.is_active).toBe(0); + expect(m333.is_active).toBe(1); + expect(m333.is_admin).toBe(0); + }); + + test('idempotencia: aplicar mismo snapshot no altera contadores y actualiza last_seen_at', () => { + GroupSyncService.reconcileGroupMembers('123@g.us', [ + { userId: '111', isAdmin: false }, + { userId: '333', isAdmin: false } + ], '2025-01-02 00:00:00.000'); + + const res3 = GroupSyncService.reconcileGroupMembers('123@g.us', [ + { userId: '111', isAdmin: false }, + { userId: '333', isAdmin: false } + ], '2025-01-03 00:00:00.000'); + + expect(res3).toEqual({ added: 0, updated: 0, deactivated: 0 }); + + const m111 = getMember('111'); + const m333 = getMember('333'); + + expect(m111.last_seen_at).toBe('2025-01-03 00:00:00.000'); + expect(m333.last_seen_at).toBe('2025-01-03 00:00:00.000'); + }); +});