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.
taskbot/tests/web/api.me.tasks.test.ts

201 lines
6.8 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { Database } from 'bun:sqlite';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { startWebServer } from './helpers/server';
import { initializeDatabase, ensureUserExists } from '../../src/db';
async function sha256Hex(input: string): Promise<string> {
const enc = new TextEncoder().encode(input);
const buf = await crypto.subtle.digest('SHA-256', enc);
const bytes = new Uint8Array(buf);
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function toIsoSql(d = new Date()): string {
return d.toISOString().replace('T', ' ').replace('Z', '');
}
describe('Web API - GET /api/me/tasks', () => {
const USER = '34600123456';
const OTHER = '34600999888';
const GROUP_OK = '123@g.us';
const GROUP_BAD = '999@g.us';
let dbPath: string;
let server: Awaited<ReturnType<typeof startWebServer>> | null = null;
let tmpDir: string;
let sid = 'sid-test-tasks';
beforeAll(async () => {
tmpDir = mkdtempSync(join(tmpdir(), 'webtest-'));
dbPath = join(tmpDir, 'tasks.db');
const db = new Database(dbPath);
initializeDatabase(db);
// Asegurar usuarios
const uid = ensureUserExists(USER, db)!;
const oid = ensureUserExists(OTHER, db)!;
// Sembrar grupos y gating
db.prepare(`
INSERT INTO groups (id, community_id, name, active, last_verified)
VALUES (?, 'comm-1', 'Group OK', 1, strftime('%Y-%m-%d %H:%M:%f','now'))
`).run(GROUP_OK);
db.prepare(`
INSERT INTO groups (id, community_id, name, active, last_verified)
VALUES (?, 'comm-1', 'Group BAD', 1, strftime('%Y-%m-%d %H:%M:%f','now'))
`).run(GROUP_BAD);
db.prepare(`INSERT INTO allowed_groups (group_id, status, updated_at) VALUES (?, 'allowed', strftime('%Y-%m-%d %H:%M:%f','now'))`).run(GROUP_OK);
db.prepare(`INSERT INTO allowed_groups (group_id, status, updated_at) VALUES (?, 'blocked', strftime('%Y-%m-%d %H:%M:%f','now'))`).run(GROUP_BAD);
// Membresía activa solo en GROUP_OK
const nowIso = toIsoSql(new Date());
db.prepare(`
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at, last_role_change_at)
VALUES (?, ?, 0, 1, ?, ?, ?)
`).run(GROUP_OK, uid, nowIso, nowIso, nowIso);
// Tareas en GROUP_OK: una con due_date, otra sin due_date
const insertTask = db.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
VALUES (?, ?, ?, ?, ?)
`);
const insertAssign = db.prepare(`
INSERT INTO task_assignments (task_id, user_id, assigned_by)
VALUES (?, ?, ?)
`);
const r1 = insertTask.run('alpha', '2025-01-02', GROUP_OK, uid, 101) as any;
const t1 = Number(r1.lastInsertRowid);
insertAssign.run(t1, uid, uid);
const r2 = insertTask.run('beta_100% exacta', null, GROUP_OK, uid, 102) as any;
const t2 = Number(r2.lastInsertRowid);
insertAssign.run(t2, uid, uid);
// Tarea en GROUP_BAD asignada al usuario (debe ser filtrada por gating)
const r3 = insertTask.run('gamma filtrada', '2025-02-01', GROUP_BAD, oid, 103) as any;
const t3 = Number(r3.lastInsertRowid);
insertAssign.run(t3, uid, oid);
// Crear sesión válida
const hash = await sha256Hex(sid);
db.prepare(`
INSERT INTO web_sessions (session_hash, user_id, created_at, last_seen_at, expires_at)
VALUES (?, ?, ?, ?, ?)
`).run(hash, uid, nowIso, nowIso, toIsoSql(new Date(Date.now() + 60 * 60 * 1000)));
db.close();
// Arrancar servidor
server = await startWebServer({
port: 19101,
env: { DB_PATH: dbPath, TZ: 'UTC' }
});
});
afterAll(async () => {
try { await server?.stop(); } catch {}
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
});
it('aplica gating y ordena por due_date asc con NULL al final', async () => {
const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10`, {
headers: { Cookie: `sid=${sid}` }
});
expect(res.status).toBe(200);
const json = await res.json();
const ids = json.items.map((it: any) => it.display_code ?? it.id);
// Deben venir solo t1 (101) y t2 (102), en ese orden (t1 con fecha primero, luego NULL)
expect(ids).toEqual([101, 102]);
});
it('filtra por búsqueda escapando % y _ correctamente', async () => {
const q = encodeURIComponent('beta_100%');
const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q}`, {
headers: { Cookie: `sid=${sid}` }
});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.items.length).toBe(1);
expect(json.items[0].description).toContain('beta_100%');
});
it('permite actualizar la descripción de una tarea con PATCH', async () => {
// Localizar la tarea por búsqueda
const q = encodeURIComponent('beta_100%');
const listRes = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q}`, {
headers: { Cookie: `sid=${sid}` }
});
expect(listRes.status).toBe(200);
const list = await listRes.json();
expect(list.items.length).toBe(1);
const taskId = list.items[0].id;
// Actualizar descripción
const newDesc = 'beta renombrada';
const patchRes = await fetch(`${server!.baseUrl}/api/tasks/${taskId}`, {
method: 'PATCH',
headers: {
Cookie: `sid=${sid}`,
'content-type': 'application/json'
},
body: JSON.stringify({ description: newDesc })
});
expect(patchRes.status).toBe(200);
const patched = await patchRes.json();
expect(patched.task.description).toBe(newDesc);
// Verificar en el listado
const q2 = encodeURIComponent('renombrada');
const listRes2 = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q2}`, {
headers: { Cookie: `sid=${sid}` }
});
expect(listRes2.status).toBe(200);
const list2 = await listRes2.json();
expect(list2.items.length).toBe(1);
expect(list2.items[0].description).toContain('renombrada');
});
it('rechaza descripciones vacías o demasiado largas', async () => {
// Reutiliza la misma tarea localizada arriba (buscar por 'renombrada')
const q = encodeURIComponent('renombrada');
const listRes = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q}`, {
headers: { Cookie: `sid=${sid}` }
});
expect(listRes.status).toBe(200);
const list = await listRes.json();
expect(list.items.length).toBe(1);
const taskId = list.items[0].id;
// Vacía/espacios
const resEmpty = await fetch(`${server!.baseUrl}/api/tasks/${taskId}`, {
method: 'PATCH',
headers: {
Cookie: `sid=${sid}`,
'content-type': 'application/json'
},
body: JSON.stringify({ description: ' ' })
});
expect(resEmpty.status).toBe(400);
// Demasiado larga (>1000)
const longText = 'a'.repeat(1001);
const resLong = await fetch(`${server!.baseUrl}/api/tasks/${taskId}`, {
method: 'PATCH',
headers: {
Cookie: `sid=${sid}`,
'content-type': 'application/json'
},
body: JSON.stringify({ description: longText })
});
expect(resLong.status).toBe(400);
});
});