From 9fb4a0d03aa46069e2848ed07c99f12edc2b8af2 Mon Sep 17 00:00:00 2001 From: borja Date: Sat, 6 Sep 2025 23:02:23 +0200 Subject: [PATCH] =?UTF-8?q?test:=20a=C3=B1ade=20pruebas=20de=20claim/unass?= =?UTF-8?q?ign=20para=20TaskService=20y=20CommandService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- .../services/command.claim-unassign.test.ts | 110 ++++++++++++++++++ tests/unit/tasks/claim-unassign.test.ts | 92 +++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 tests/unit/services/command.claim-unassign.test.ts create mode 100644 tests/unit/tasks/claim-unassign.test.ts diff --git a/tests/unit/services/command.claim-unassign.test.ts b/tests/unit/services/command.claim-unassign.test.ts new file mode 100644 index 0000000..88cad2f --- /dev/null +++ b/tests/unit/services/command.claim-unassign.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, beforeAll, beforeEach } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { initializeDatabase, ensureUserExists } from '../../../src/db'; +import { TaskService } from '../../../src/tasks/service'; +import { CommandService } from '../../../src/services/command'; + +describe('CommandService - /t tomar y /t soltar', () => { + let memdb: Database; + + beforeAll(() => { + memdb = new Database(':memory:'); + initializeDatabase(memdb); + TaskService.dbInstance = memdb; + CommandService.dbInstance = memdb; + }); + + beforeEach(() => { + memdb.exec(` + DELETE FROM task_assignments; + DELETE FROM tasks; + DELETE FROM users; + `); + }); + + function createTask(description: string, creator: string, due?: string | null, assignees: string[] = []): number { + const createdBy = ensureUserExists(creator, memdb)!; + const taskId = TaskService.createTask( + { + description, + due_date: due ?? null, + group_id: null, + created_by: createdBy, + }, + assignees.map(uid => ({ user_id: ensureUserExists(uid, memdb)!, assigned_by: createdBy })) + ); + return taskId; + } + + const ctx = (sender: string, message: string) => ({ + sender, + groupId: '', // DM o vacío; sin relevancia para tomar/soltar + message, + mentions: [] as string[], + }); + + it('tomar: uso inválido (sin id)', async () => { + const res = await CommandService.handle(ctx('111', '/t tomar')); + expect(res).toHaveLength(1); + expect(res[0].recipient).toBe('111'); + expect(res[0].message).toContain('Uso: /t tomar '); + }); + + it('tomar: not_found', async () => { + const res = await CommandService.handle(ctx('111', '/t tomar 99999')); + expect(res[0].message).toContain('no encontrada'); + }); + + it('tomar: happy y luego already', async () => { + const taskId = createTask('Desc tomar', '999', '2025-09-12'); + const r1 = await CommandService.handle(ctx('111', `/t tomar ${taskId}`)); + expect(r1[0].message).toContain('Has tomado'); + expect(r1[0].message).toContain(String(taskId)); + expect(r1[0].message).toContain('Desc tomar'); + expect(r1[0].message).toContain('📅'); // formato dd/MM + + const r2 = await CommandService.handle(ctx('111', `/t tomar ${taskId}`)); + expect(r2[0].message).toContain('ya la tenías'); + }); + + it('tomar: completed', async () => { + const taskId = createTask('Tarea completa', '111', '2025-10-10'); + const comp = TaskService.completeTask(taskId, '111'); + expect(comp.status).toBe('updated'); + + const res = await CommandService.handle(ctx('222', `/t tomar ${taskId}`)); + expect(res[0].message).toContain('ya estaba completada'); + }); + + it('soltar: uso inválido (sin id)', async () => { + const res = await CommandService.handle(ctx('111', '/t soltar')); + expect(res[0].message).toContain('Uso: /t soltar '); + }); + + it('soltar: not_found', async () => { + const res = await CommandService.handle(ctx('111', '/t soltar 123456')); + expect(res[0].message).toContain('no encontrada'); + }); + + it('soltar: si queda sin dueño, mensaje adecuado', async () => { + const taskId = createTask('Desc soltar', '999', '2025-09-12', ['111']); + const res = await CommandService.handle(ctx('111', `/t soltar ${taskId}`)); + expect(res[0].message).toContain('queda sin dueño'); + expect(res[0].message).toContain(String(taskId)); + }); + + it('soltar: not_assigned muestra mensaje informativo', async () => { + const taskId = createTask('Nunca asignada a 111', '999', null, ['222']); + const res = await CommandService.handle(ctx('111', `/t soltar ${taskId}`)); + expect(res[0].message).toContain('no la tenías asignada'); + }); + + it('soltar: completed', async () => { + const taskId = createTask('Completa y soltar', '111', null, ['111']); + const comp = TaskService.completeTask(taskId, '111'); + expect(comp.status).toBe('updated'); + + const res = await CommandService.handle(ctx('111', `/t soltar ${taskId}`)); + expect(res[0].message).toContain('ya estaba completada'); + }); +}); diff --git a/tests/unit/tasks/claim-unassign.test.ts b/tests/unit/tasks/claim-unassign.test.ts new file mode 100644 index 0000000..3f8d85b --- /dev/null +++ b/tests/unit/tasks/claim-unassign.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, beforeAll, beforeEach } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { initializeDatabase, ensureUserExists } from '../../../src/db'; +import { TaskService } from '../../../src/tasks/service'; + +describe('TaskService - claim/unassign', () => { + let memdb: Database; + + beforeAll(() => { + memdb = new Database(':memory:'); + initializeDatabase(memdb); + TaskService.dbInstance = memdb; + }); + + beforeEach(() => { + memdb.exec(` + DELETE FROM task_assignments; + DELETE FROM tasks; + DELETE FROM users; + `); + }); + + function createTask(description: string, createdBy: string, due?: string | null, assignees: string[] = []): number { + const creator = ensureUserExists(createdBy, memdb)!; + const taskId = TaskService.createTask( + { + description, + due_date: due ?? null, + group_id: null, + created_by: creator, + }, + assignees.map(uid => ({ user_id: ensureUserExists(uid, memdb)!, assigned_by: creator })) + ); + return taskId; + } + + it('claim: happy path then idempotent', () => { + const taskId = createTask('Probar claim', '111', '2025-09-12'); + const res1 = TaskService.claimTask(taskId, '222'); + expect(res1.status).toBe('claimed'); + expect(res1.task?.id).toBe(taskId); + + const res2 = TaskService.claimTask(taskId, '222'); + expect(res2.status).toBe('already'); + expect(res2.task?.id).toBe(taskId); + }); + + it('claim: not_found', () => { + const res = TaskService.claimTask(999999, '111'); + expect(res.status).toBe('not_found'); + }); + + it('claim: completed', () => { + const taskId = createTask('Tarea ya completada', '111', '2025-10-10'); + // marcar como completada + const comp = TaskService.completeTask(taskId, '111'); + expect(comp.status).toBe('updated'); + + const res = TaskService.claimTask(taskId, '222'); + expect(res.status).toBe('completed'); + expect(res.task?.id).toBe(taskId); + }); + + it('unassign: happy path; luego not_assigned; now_unassigned=true', () => { + const taskId = createTask('Soltar luego de asignar', '111', '2025-09-20', ['222']); + // soltar por el mismo usuario + const res1 = TaskService.unassignTask(taskId, '222'); + expect(res1.status).toBe('unassigned'); + expect(res1.task?.id).toBe(taskId); + expect(res1.now_unassigned).toBe(true); + + // idempotente si no estaba asignado + const res2 = TaskService.unassignTask(taskId, '222'); + expect(res2.status).toBe('not_assigned'); + expect(res2.now_unassigned).toBe(true); + }); + + it('unassign: not_found', () => { + const res = TaskService.unassignTask(424242, '111'); + expect(res.status).toBe('not_found'); + }); + + it('unassign: completed', () => { + const taskId = createTask('Unassign bloqueada por completada', '111', null, ['222']); + const comp = TaskService.completeTask(taskId, '111'); + expect(comp.status).toBe('updated'); + + const res = TaskService.unassignTask(taskId, '222'); + expect(res.status).toBe('completed'); + expect(res.task?.id).toBe(taskId); + }); +});