test: agregar pruebas de datetime, mantenimiento y API de completar

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 1 month ago
parent c1f12ff953
commit df27161216

@ -0,0 +1,92 @@
import { beforeEach, describe, expect, it } from 'bun:test';
import Database from 'bun:sqlite';
import { initializeDatabase } from '../../../src/db';
import { MaintenanceService } from '../../../src/services/maintenance';
import { toIsoSqlUTC } from '../../../src/utils/datetime';
function makeMem(): any {
const db = new Database(':memory:');
initializeDatabase(db);
return db;
}
describe('MaintenanceService', () => {
let memdb: any;
beforeEach(() => {
memdb = makeMem();
});
it('cleanupInactiveMembersOnce elimina miembros inactivos más antiguos que el umbral', async () => {
// Grupo y usuarios
memdb.exec(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('g1@g.us','comm','G',1);`);
memdb.exec(`INSERT OR IGNORE INTO users (id) VALUES ('111');`);
memdb.exec(`INSERT OR IGNORE INTO users (id) VALUES ('222');`);
// last_seen_at muy antiguo → debe borrarse
memdb.exec(`
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
VALUES ('g1@g.us','111',0,0,'2020-01-01 00:00:00','2020-01-01 00:00:00');
`);
// last_seen_at reciente → no debe borrarse
const recent = toIsoSqlUTC(new Date(Date.now() - 12 * 60 * 60 * 1000));
memdb.exec(`
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
VALUES ('g1@g.us','222',0,0,'${recent}','${recent}');
`);
const deleted = await MaintenanceService.cleanupInactiveMembersOnce(memdb, 1);
expect(deleted).toBe(1);
const rows = memdb.prepare(`SELECT user_id FROM group_members ORDER BY user_id`).all() as any[];
const remaining = rows.map(r => r.user_id);
expect(remaining).toEqual(['222']);
});
it('cleanupInactiveMembersOnce no hace nada si retención <= 0', async () => {
const deleted = await MaintenanceService.cleanupInactiveMembersOnce(memdb, 0);
expect(deleted).toBe(0);
});
it('reconcileAliasUsersOnce fusiona alias a usuario real en tablas principales', async () => {
// Sembrar usuarios
memdb.exec(`INSERT OR IGNORE INTO users (id, created_at, updated_at) VALUES ('alias-1', '2024-01-01 00:00:00', '2024-01-01 00:00:00');`);
memdb.exec(`INSERT OR IGNORE INTO users (id, created_at, updated_at) VALUES ('real-1', '2024-01-01 00:00:00', '2024-01-01 00:00:00');`);
// Tarea creada por alias y asignaciones usando alias
const res = memdb.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
VALUES ('T1', NULL, NULL, 'alias-1', 0, NULL)
`).run() as any;
const taskId = Number(res.lastInsertRowid);
memdb.exec(`
INSERT OR IGNORE INTO task_assignments (task_id, user_id, assigned_by)
VALUES (${taskId}, 'alias-1', 'alias-1');
`);
// Tabla de alias
memdb.exec(`
INSERT OR IGNORE INTO user_aliases (alias, user_id, source)
VALUES ('alias-1', 'real-1', 'test');
`);
const merged = await MaintenanceService.reconcileAliasUsersOnce(memdb);
expect(merged).toBeGreaterThanOrEqual(1);
const tRow = memdb.prepare(`SELECT created_by FROM tasks WHERE id = ?`).get(taskId) as any;
expect(String(tRow.created_by)).toBe('real-1');
const aRows = memdb.prepare(`SELECT user_id, assigned_by FROM task_assignments WHERE task_id = ?`).all(taskId) as any[];
expect(aRows.length).toBe(1);
expect(String(aRows[0].user_id)).toBe('real-1');
expect(String(aRows[0].assigned_by)).toBe('real-1');
});
it('reconcileAliasUsersOnce retorna 0 si no existe la tabla user_aliases', async () => {
try {
memdb.exec(`DROP TABLE IF EXISTS user_aliases;`);
} catch {}
const merged = await MaintenanceService.reconcileAliasUsersOnce(memdb);
expect(merged).toBe(0);
});
});

@ -0,0 +1,28 @@
import { describe, it, expect } from 'bun:test';
import { toIsoSqlUTC, ymdUTC, addMonthsUTC } from '../../apps/web/src/lib/server/datetime.ts';
describe('apps/web datetime wrapper', () => {
it('toIsoSqlUTC serializa en UTC con milisegundos', () => {
const d = new Date(Date.UTC(2024, 0, 31, 23, 59, 59, 123));
expect(toIsoSqlUTC(d)).toBe('2024-01-31 23:59:59.123');
});
it('ymdUTC devuelve YYYY-MM-DD en UTC', () => {
const d = new Date(Date.UTC(2024, 7, 9, 10, 11, 12)); // 2024-08-09
expect(ymdUTC(d)).toBe('2024-08-09');
});
it('addMonthsUTC suma meses en UTC preservando día cuando es posible', () => {
const d1 = new Date(Date.UTC(2024, 0, 15)); // 2024-01-15
const plus1 = addMonthsUTC(d1, 1);
expect(ymdUTC(plus1)).toBe('2024-02-15');
const d2 = new Date(Date.UTC(2024, 1, 29)); // 2024-02-29 (bisiesto)
const plus1b = addMonthsUTC(d2, 1);
expect(ymdUTC(plus1b)).toBe('2024-03-29');
const d3 = new Date(Date.UTC(2024, 11, 15)); // 2024-12-15
const plus2 = addMonthsUTC(d3, 2);
expect(ymdUTC(plus2)).toBe('2025-02-15');
});
});

@ -0,0 +1,144 @@
import { beforeEach, afterEach, describe, expect, it } from 'bun:test';
import { createTempDb } from './helpers/db';
// Reutilizamos el estilo de import dinámico del handler como en otros tests web.
describe('Web API - completar tarea (rutas de error y gating)', () => {
let cleanup: () => void;
let db: any;
let path: string;
const USER = '34600123456';
const GROUP_ID = '12345-67890@g.us';
beforeEach(() => {
const tmp = createTempDb();
cleanup = tmp.cleanup;
db = tmp.db;
path = tmp.path;
process.env.NODE_ENV = 'test';
process.env.DB_PATH = path;
process.env.REACTIONS_ENABLED = 'true';
process.env.REACTIONS_SCOPE = 'groups';
process.env.REACTIONS_TTL_DAYS = '14';
process.env.GROUP_GATING_MODE = 'enforce';
});
afterEach(async () => {
try {
const { closeDb } = await import('../../apps/web/src/lib/server/db.ts');
closeDb();
} catch {}
if (cleanup) cleanup();
delete process.env.DB_PATH;
delete process.env.REACTIONS_ENABLED;
delete process.env.REACTIONS_SCOPE;
delete process.env.REACTIONS_TTL_DAYS;
delete process.env.GROUP_GATING_MODE;
});
it('401 si no hay session (userId)', async () => {
const event: any = {
locals: { userId: null },
params: { id: '1' },
request: new Request('http://localhost', { method: 'POST' })
};
const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts');
const res = await completeHandler(event);
expect(res.status).toBe(401);
});
it('400 si id no es válido', async () => {
const event: any = {
locals: { userId: USER },
params: { id: 'abc' },
request: new Request('http://localhost', { method: 'POST' })
};
const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts');
const res = await completeHandler(event);
expect(res.status).toBe(400);
});
it('404 si la tarea no existe', async () => {
const event: any = {
locals: { userId: USER },
params: { id: '999999' },
request: new Request('http://localhost', { method: 'POST' })
};
// Sembrar usuario para evitar FKs indirectas (no imprescindible aquí)
db.prepare(`INSERT OR IGNORE INTO users (id) VALUES (?)`).run(USER);
const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts');
const res = await completeHandler(event);
expect(res.status).toBe(404);
});
it('403 si grupo no allowed', async () => {
db.prepare(`INSERT OR IGNORE INTO users (id) VALUES (?)`).run(USER);
// Tarea con group_id; no insertamos en allowed_groups
const taskId = Number(
db.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
VALUES ('No allowed', NULL, ?, ?, 0, NULL)
`).run(GROUP_ID, USER).lastInsertRowid
);
// El usuario podría incluso ser miembro activo; sigue siendo 403 por falta de allowed
db.prepare(`INSERT OR REPLACE INTO group_members (group_id, user_id, is_admin, is_active) VALUES (?, ?, 0, 1)`).run(GROUP_ID, USER);
const event: any = {
locals: { userId: USER },
params: { id: String(taskId) },
request: new Request('http://localhost', { method: 'POST' })
};
const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts');
const res = await completeHandler(event);
expect(res.status).toBe(403);
});
it('403 si grupo allowed pero usuario no es miembro activo', async () => {
db.prepare(`INSERT OR IGNORE INTO users (id) VALUES (?)`).run(USER);
db.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, 'comm', 'G', 1)`).run(GROUP_ID);
db.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, label, status) VALUES (?, 'G', 'allowed')`).run(GROUP_ID);
const taskId = Number(
db.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
VALUES ('No active member', NULL, ?, ?, 0, NULL)
`).run(GROUP_ID, USER).lastInsertRowid
);
// No sembramos group_members activo para USER
const event: any = {
locals: { userId: USER },
params: { id: String(taskId) },
request: new Request('http://localhost', { method: 'POST' })
};
const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts');
const res = await completeHandler(event);
expect(res.status).toBe(403);
});
it('403 en tarea personal si no está asignada al usuario', async () => {
db.prepare(`INSERT OR IGNORE INTO users (id) VALUES (?)`).run(USER);
const taskId = Number(
db.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
VALUES ('Personal sin asignación', NULL, NULL, ?, 0, NULL)
`).run(USER).lastInsertRowid
);
// No insertamos task_assignments
const event: any = {
locals: { userId: USER },
params: { id: String(taskId) },
request: new Request('http://localhost', { method: 'POST' })
};
const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts');
const res = await completeHandler(event);
expect(res.status).toBe(403);
});
});
Loading…
Cancel
Save