diff --git a/src/tasks/service.ts b/src/tasks/service.ts index 9b8f2cb..19d86f1 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -1,3 +1,4 @@ +import type { Database } from 'better-sqlite3'; import { db } from '../db'; type CreateTaskInput = { @@ -13,9 +14,11 @@ type CreateAssignmentInput = { }; export class TaskService { + static dbInstance: Database = db; + static createTask(task: CreateTaskInput, assignments: CreateAssignmentInput[] = []): number { - const runTx = db.transaction(() => { - const insertTask = db.prepare(` + const runTx = this.dbInstance.transaction(() => { + const insertTask = this.dbInstance.prepare(` INSERT INTO tasks (description, due_date, group_id, created_by) VALUES (?, ?, ?, ?) RETURNING id @@ -29,7 +32,7 @@ export class TaskService { ) as { id: number }; if (assignments.length > 0) { - const insertAssignment = db.prepare(` + const insertAssignment = this.dbInstance.prepare(` INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?) `); diff --git a/tests/unit/tasks/service.test.ts b/tests/unit/tasks/service.test.ts new file mode 100644 index 0000000..fbe279e --- /dev/null +++ b/tests/unit/tasks/service.test.ts @@ -0,0 +1,170 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import Database from 'better-sqlite3'; +import { initializeDatabase, ensureUserExists } from '../../../src/db'; +import { TaskService } from '../../../src/tasks/service'; + +let memDb: Database; + +beforeEach(() => { + memDb = new Database(':memory:'); + initializeDatabase(memDb); + TaskService.dbInstance = memDb; +}); + +afterEach(() => { + try { + memDb.close(); + } catch {} +}); + +describe('TaskService.createTask', () => { + it('crea una tarea mínima con created_by y sin due_date ni group_id', () => { + const creatorRaw = '555111222@s.whatsapp.net'; + const createdBy = ensureUserExists(creatorRaw, memDb); + expect(createdBy).toBeTruthy(); + + const id = TaskService.createTask( + { + description: 'Comprar agua', + created_by: createdBy!, + due_date: null, + group_id: null, + }, + [] // sin asignaciones + ); + + expect(typeof id).toBe('number'); + + const task = memDb + .prepare( + `SELECT id, description, due_date, group_id, created_by, completed + FROM tasks WHERE id = ?` + ) + .get(id) as any; + + expect(task).toBeTruthy(); + expect(task.description).toBe('Comprar agua'); + expect(task.due_date).toBeNull(); + expect(task.group_id).toBeNull(); + expect(task.created_by).toBe(createdBy); + expect(task.completed).toBe(0); // BOOLEAN en SQLite como 0/1 + }); + + it('guarda due_date como cadena YYYY-MM-DD y group_id como JID completo', () => { + const creatorRaw = '555333444@s.whatsapp.net'; + const createdBy = ensureUserExists(creatorRaw, memDb)!; + + const due = '2025-09-10'; + const groupId = '12345@g.us'; + + const id = TaskService.createTask( + { + description: 'Pagar servicios', + created_by: createdBy, + due_date: due, + group_id: groupId, + }, + [] + ); + + const row = memDb + .prepare( + `SELECT description, due_date, group_id, created_by + FROM tasks WHERE id = ?` + ) + .get(id) as any; + + expect(row.due_date).toBe(due); + expect(row.group_id).toBe(groupId); + expect(row.created_by).toBe(createdBy); + }); + + it('inserta asignaciones y evita duplicados por usuario', () => { + const creator = ensureUserExists('555000001@s.whatsapp.net', memDb)!; + const assigneeA = ensureUserExists('555000002@s.whatsapp.net', memDb)!; + const assigneeB = ensureUserExists('555000003@s.whatsapp.net', memDb)!; + + const id = TaskService.createTask( + { + description: 'Organizar reunión', + created_by: creator, + due_date: null, + group_id: null, + }, + [ + { user_id: assigneeA, assigned_by: creator }, + { user_id: assigneeA, assigned_by: creator }, // duplicado + { user_id: assigneeB, assigned_by: creator }, + ] + ); + + const count = memDb + .prepare( + `SELECT COUNT(*) AS c FROM task_assignments WHERE task_id = ?` + ) + .get(id) as any; + + expect(count.c).toBe(2); + + const users = memDb + .prepare( + `SELECT user_id FROM task_assignments WHERE task_id = ? ORDER BY user_id` + ) + .all(id) as any[]; + + expect(users.map(u => u.user_id)).toEqual([assigneeA, assigneeB].sort()); + }); + + it('hace rollback si una asignación viola FK (usuario inexistente)', () => { + const creator = ensureUserExists('555010101@s.whatsapp.net', memDb)!; + + expect(() => + TaskService.createTask( + { + description: 'Tarea con fallo', + created_by: creator, + due_date: null, + group_id: null, + }, + [ + // Usuario no existente: no llamamos ensureUserExists + { user_id: '555099999', assigned_by: creator }, + ] + ) + ).toThrow(); + + const counts = memDb + .prepare( + `SELECT + (SELECT COUNT(*) FROM tasks) AS tasks_count, + (SELECT COUNT(*) FROM task_assignments) AS assigns_count` + ) + .get() as any; + + expect(counts.tasks_count).toBe(0); + expect(counts.assigns_count).toBe(0); + }); + + it('lanza error si created_by no existe (FK) y no inserta la tarea', () => { + // No llamamos a ensureUserExists para este created_by + const nonExisting = '555123123'; + + expect(() => + TaskService.createTask( + { + description: 'No debería insertarse', + created_by: nonExisting, + due_date: null, + group_id: null, + }, + [] + ) + ).toThrow(); + + const count = memDb + .prepare(`SELECT COUNT(*) AS c FROM tasks`) + .get() as any; + + expect(count.c).toBe(0); + }); +});