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.
280 lines
8.5 KiB
TypeScript
280 lines
8.5 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { Database } from 'bun:sqlite';
|
|
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.listGroupUnassigned / countGroupUnassigned', () => {
|
|
it('devuelve solo tareas sin dueño del grupo, en orden por fecha (NULL al final) y respeta el límite', () => {
|
|
// Sembrar grupos para FK
|
|
memDb.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`)
|
|
.run('g1@g.us', 'c1', 'G1');
|
|
memDb.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`)
|
|
.run('g2@g.us', 'c1', 'G2');
|
|
|
|
// En g1: 3 sin dueño con distintas fechas, 1 asignada (excluir), 1 sin dueño completada (excluir)
|
|
const tEarly = TaskService.createTask({
|
|
description: 'Sin dueño pronto',
|
|
due_date: '2025-08-01',
|
|
group_id: 'g1@g.us',
|
|
created_by: '1000000000',
|
|
});
|
|
const tLater = TaskService.createTask({
|
|
description: 'Sin dueño tarde',
|
|
due_date: '2025-10-01',
|
|
group_id: 'g1@g.us',
|
|
created_by: '1000000000',
|
|
});
|
|
const tNull = TaskService.createTask({
|
|
description: 'Sin dueño sin fecha',
|
|
due_date: null,
|
|
group_id: 'g1@g.us',
|
|
created_by: '1000000000',
|
|
});
|
|
TaskService.createTask({
|
|
description: 'Asignada (no debe salir)',
|
|
due_date: '2025-09-01',
|
|
group_id: 'g1@g.us',
|
|
created_by: '1000000000',
|
|
}, [{ user_id: '2000000000', assigned_by: '1000000000' }]);
|
|
|
|
// Completar una sin dueño para que no aparezca
|
|
memDb.prepare(`UPDATE tasks SET completed = 1, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(tEarly);
|
|
|
|
// En g2: sin dueño (no deben aparecer al listar g1)
|
|
for (let i = 1; i <= 3; i++) {
|
|
TaskService.createTask({
|
|
description: `G2 ${i}`,
|
|
due_date: '2025-12-31',
|
|
group_id: 'g2@g.us',
|
|
created_by: '1000000000',
|
|
});
|
|
}
|
|
|
|
const list = TaskService.listGroupUnassigned('g1@g.us', 10);
|
|
expect(Array.isArray(list)).toBe(true);
|
|
// Deben quedar 2: 'Sin dueño tarde' (fecha) y 'Sin dueño sin fecha' (NULL al final)
|
|
expect(list.length).toBe(2);
|
|
expect(list[0].description).toBe('Sin dueño tarde');
|
|
expect(list[1].description).toBe('Sin dueño sin fecha');
|
|
expect(list[0].assignees).toEqual([]);
|
|
expect(list[1].assignees).toEqual([]);
|
|
expect(list[0].group_id).toBe('g1@g.us');
|
|
expect(list[1].group_id).toBe('g1@g.us');
|
|
|
|
// Límite
|
|
// Crear más en g1 para forzar límite 1
|
|
TaskService.createTask({
|
|
description: 'Otra sin dueño',
|
|
due_date: '2025-11-01',
|
|
group_id: 'g1@g.us',
|
|
created_by: '1000000000',
|
|
});
|
|
const limited = TaskService.listGroupUnassigned('g1@g.us', 1);
|
|
expect(limited.length).toBe(1);
|
|
});
|
|
|
|
it('countGroupUnassigned cuenta solo sin dueño pendientes (excluye completadas y asignadas)', () => {
|
|
// Limpiar por si acaso
|
|
memDb.exec('DELETE FROM tasks');
|
|
memDb.exec('DELETE FROM task_assignments');
|
|
|
|
// Sembrar grupo
|
|
memDb.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`)
|
|
.run('g1@g.us', 'c1', 'G1');
|
|
|
|
const a = TaskService.createTask({
|
|
description: 'Sin dueño 1',
|
|
due_date: '2025-09-10',
|
|
group_id: 'g1@g.us',
|
|
created_by: '3333333333',
|
|
});
|
|
const b = TaskService.createTask({
|
|
description: 'Sin dueño 2',
|
|
due_date: null,
|
|
group_id: 'g1@g.us',
|
|
created_by: '3333333333',
|
|
});
|
|
// Asignada (no cuenta como sin dueño)
|
|
TaskService.createTask({
|
|
description: 'Asignada',
|
|
due_date: '2025-09-12',
|
|
group_id: 'g1@g.us',
|
|
created_by: '3333333333',
|
|
}, [{ user_id: '4444444444', assigned_by: '3333333333' }]);
|
|
|
|
// Completar una sin dueño
|
|
memDb.prepare(`UPDATE tasks SET completed = 1, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(a);
|
|
|
|
const cnt = TaskService.countGroupUnassigned('g1@g.us');
|
|
expect(cnt).toBe(1); // sólo queda b
|
|
});
|
|
});
|
|
|
|
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';
|
|
|
|
// Sembrar el grupo para satisfacer la FK de tasks.group_id -> groups.id
|
|
memDb
|
|
.prepare(`INSERT INTO groups (id, community_id, name, active) VALUES (?, ?, ?, TRUE)`)
|
|
.run(groupId, 'test-community', 'Grupo de prueba');
|
|
|
|
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('asegura usuarios inexistentes en asignaciones y no viola FK', () => {
|
|
const creator = ensureUserExists('555010101@s.whatsapp.net', memDb)!;
|
|
|
|
const id = TaskService.createTask(
|
|
{
|
|
description: 'Tarea con asignado nuevo',
|
|
created_by: creator,
|
|
due_date: null,
|
|
group_id: null,
|
|
},
|
|
[
|
|
// Usuario no existente: TaskService debe asegurar y crear
|
|
{ user_id: '555099999', assigned_by: creator },
|
|
]
|
|
);
|
|
|
|
const counts = memDb
|
|
.prepare(
|
|
`SELECT
|
|
(SELECT COUNT(*) FROM tasks) AS tasks_count,
|
|
(SELECT COUNT(*) FROM task_assignments WHERE task_id = ?) AS assigns_count`
|
|
)
|
|
.get(id) as any;
|
|
|
|
expect(counts.tasks_count).toBe(1);
|
|
expect(counts.assigns_count).toBe(1);
|
|
});
|
|
|
|
it('lanza error si created_by es inválido (no normalizable) y no inserta la tarea', () => {
|
|
const invalidCreator = 'invalid-id!';
|
|
|
|
expect(() =>
|
|
TaskService.createTask(
|
|
{
|
|
description: 'No debería insertarse',
|
|
created_by: invalidCreator,
|
|
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);
|
|
});
|
|
});
|