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.
231 lines
7.9 KiB
TypeScript
231 lines
7.9 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);
|
|
});
|
|
|
|
it('permite completar una tarea asignada y aparece en recent', async () => {
|
|
// Buscar una tarea abierta por texto
|
|
const q = encodeURIComponent('alpha');
|
|
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;
|
|
|
|
// Completar
|
|
const resComplete = await fetch(`${server!.baseUrl}/api/tasks/${taskId}/complete`, {
|
|
method: 'POST',
|
|
headers: { Cookie: `sid=${sid}` }
|
|
});
|
|
expect(resComplete.status).toBe(200);
|
|
const done = await resComplete.json();
|
|
expect(done.status === 'updated' || done.status === 'already').toBe(true);
|
|
|
|
// Ver en recent
|
|
const recentRes = await fetch(`${server!.baseUrl}/api/me/tasks?status=recent&limit=10`, {
|
|
headers: { Cookie: `sid=${sid}` }
|
|
});
|
|
expect(recentRes.status).toBe(200);
|
|
const recent = await recentRes.json();
|
|
const ids = recent.items.map((it: any) => it.id);
|
|
expect(ids.includes(taskId)).toBe(true);
|
|
});
|
|
});
|