test: añade pruebas de integración y actualiza docs para Fase 10
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>webui
parent
94a7f19a3c
commit
513099f6ef
@ -0,0 +1,86 @@
|
||||
import { describe, it, beforeAll, afterAll, expect } from 'bun:test';
|
||||
import { createTempDb } from './helpers/db';
|
||||
import { startWebServer, type RunningServer } from './helpers/server';
|
||||
|
||||
describe('API Web - complete auto-assign y recientes', () => {
|
||||
const USER = '34600123456';
|
||||
const GROUP = 'g-1@g.us';
|
||||
let dbHandle: any;
|
||||
let cleanupDb: () => void;
|
||||
let server: RunningServer;
|
||||
let baseUrl: string;
|
||||
let taskId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const tmp = createTempDb();
|
||||
dbHandle = tmp.db;
|
||||
cleanupDb = tmp.cleanup;
|
||||
|
||||
// Semilla mínima
|
||||
dbHandle.prepare(`INSERT INTO users (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(USER);
|
||||
dbHandle
|
||||
.prepare(`INSERT INTO groups (id, community_id, name, active) VALUES (?, 'comm-1', 'Test Group', 1)`)
|
||||
.run(GROUP);
|
||||
dbHandle.prepare(`INSERT INTO allowed_groups (group_id, status) VALUES (?, 'allowed')`).run(GROUP);
|
||||
dbHandle
|
||||
.prepare(
|
||||
`INSERT INTO group_members (group_id, user_id, is_admin, is_active) VALUES (?, ?, 0, 1)
|
||||
`
|
||||
)
|
||||
.run(GROUP, USER);
|
||||
|
||||
const ins = dbHandle
|
||||
.prepare(`INSERT INTO tasks (description, group_id, created_by) VALUES ('Hacer algo', ?, ?)`)
|
||||
.run(GROUP, USER);
|
||||
taskId = Number(ins.lastInsertRowid);
|
||||
|
||||
server = await startWebServer({
|
||||
port: 19101,
|
||||
env: { DB_PATH: tmp.path, DEV_DEFAULT_USER: USER, TZ: 'UTC' }
|
||||
});
|
||||
baseUrl = server.baseUrl;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await server.stop();
|
||||
} catch {}
|
||||
try {
|
||||
cleanupDb?.();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('completar una tarea sin responsables auto-asigna y aparece en recientes', async () => {
|
||||
// Completar
|
||||
const resComplete = await fetch(`${baseUrl}/api/tasks/${taskId}/complete`, { method: 'POST' });
|
||||
expect(resComplete.status).toBe(200);
|
||||
const jsonComplete = await resComplete.json();
|
||||
expect(['updated', 'already']).toContain(jsonComplete?.status);
|
||||
|
||||
// Recientes (24h)
|
||||
const resRecent = await fetch(`${baseUrl}/api/me/tasks?status=recent&limit=50`);
|
||||
expect(resRecent.status).toBe(200);
|
||||
const recentBody = await resRecent.json();
|
||||
const items = Array.isArray(recentBody?.items) ? recentBody.items : [];
|
||||
const found = items.find((it: any) => Number(it?.id) === taskId);
|
||||
expect(found).toBeTruthy();
|
||||
expect(found.completed).toBe(true);
|
||||
expect(typeof found.completed_at === 'string' && found.completed_at.length > 0).toBe(true);
|
||||
expect(Array.isArray(found.assignees)).toBe(true);
|
||||
expect(found.assignees.map(String)).toContain(USER);
|
||||
|
||||
// Uncomplete inmediato permitido
|
||||
const resUncomplete = await fetch(`${baseUrl}/api/tasks/${taskId}/uncomplete`, { method: 'POST' });
|
||||
expect(resUncomplete.status).toBe(200);
|
||||
const jsonUnc = await resUncomplete.json();
|
||||
expect(jsonUnc?.status).toBe('updated');
|
||||
|
||||
// Ya no aparece en recientes tras reabrir
|
||||
const resRecent2 = await fetch(`${baseUrl}/api/me/tasks?status=recent&limit=50`);
|
||||
expect(resRecent2.status).toBe(200);
|
||||
const recentBody2 = await resRecent2.json();
|
||||
const items2 = Array.isArray(recentBody2?.items) ? recentBody2.items : [];
|
||||
const found2 = items2.find((it: any) => Number(it?.id) === taskId);
|
||||
expect(found2).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,91 @@
|
||||
import { describe, it, beforeAll, afterAll, expect } from 'bun:test';
|
||||
import { createTempDb } from './helpers/db';
|
||||
import { startWebServer, type RunningServer } from './helpers/server';
|
||||
|
||||
describe('API Web - carreras en complete con auto-asignación', () => {
|
||||
const U1 = '34600123456';
|
||||
const U2 = '34600123457';
|
||||
const GROUP = 'g-3@g.us';
|
||||
|
||||
let dbHandle: any;
|
||||
let cleanupDb: () => void;
|
||||
let serverA: RunningServer;
|
||||
let serverB: RunningServer;
|
||||
let baseA: string;
|
||||
let baseB: string;
|
||||
let taskId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const tmp = createTempDb();
|
||||
dbHandle = tmp.db;
|
||||
cleanupDb = tmp.cleanup;
|
||||
|
||||
// Semilla mínima
|
||||
dbHandle.prepare(`INSERT INTO users (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(U1);
|
||||
dbHandle.prepare(`INSERT INTO users (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(U2);
|
||||
dbHandle
|
||||
.prepare(`INSERT INTO groups (id, community_id, name, active) VALUES (?, 'comm-1', 'G3', 1)`)
|
||||
.run(GROUP);
|
||||
dbHandle.prepare(`INSERT INTO allowed_groups (group_id, status) VALUES (?, 'allowed')`).run(GROUP);
|
||||
dbHandle
|
||||
.prepare(`INSERT INTO group_members (group_id, user_id, is_admin, is_active) VALUES (?, ?, 0, 1)`)
|
||||
.run(GROUP, U1);
|
||||
dbHandle
|
||||
.prepare(`INSERT INTO group_members (group_id, user_id, is_admin, is_active) VALUES (?, ?, 0, 1)`)
|
||||
.run(GROUP, U2);
|
||||
|
||||
const ins = dbHandle
|
||||
.prepare(`INSERT INTO tasks (description, group_id, created_by) VALUES ('Carrera complete', ?, ?)`)
|
||||
.run(GROUP, U1);
|
||||
taskId = Number(ins.lastInsertRowid);
|
||||
|
||||
// Iniciar dos servidores contra la misma DB
|
||||
serverA = await startWebServer({
|
||||
port: 19111,
|
||||
env: { DB_PATH: tmp.path, DEV_DEFAULT_USER: U1, TZ: 'UTC' }
|
||||
});
|
||||
baseA = serverA.baseUrl;
|
||||
serverB = await startWebServer({
|
||||
port: 19112,
|
||||
env: { DB_PATH: tmp.path, DEV_DEFAULT_USER: U2, TZ: 'UTC' }
|
||||
});
|
||||
baseB = serverB.baseUrl;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await serverA.stop();
|
||||
} catch {}
|
||||
try {
|
||||
await serverB.stop();
|
||||
} catch {}
|
||||
try {
|
||||
cleanupDb?.();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('solo una actualización de completed; la asignación incluye al menos al completador', async () => {
|
||||
const [r1, r2] = await Promise.all([
|
||||
fetch(`${baseA}/api/tasks/${taskId}/complete`, { method: 'POST' }),
|
||||
fetch(`${baseB}/api/tasks/${taskId}/complete`, { method: 'POST' })
|
||||
]);
|
||||
expect([200, 200]).toContain(r1.status);
|
||||
expect([200, 200]).toContain(r2.status);
|
||||
|
||||
const row = dbHandle
|
||||
.prepare(`SELECT COALESCE(completed,0) as completed, completed_by FROM tasks WHERE id = ?`)
|
||||
.get(taskId) as any;
|
||||
expect(Number(row?.completed || 0)).toBe(1);
|
||||
const completedBy = String(row?.completed_by || '');
|
||||
|
||||
const assigns = dbHandle
|
||||
.prepare(`SELECT user_id FROM task_assignments WHERE task_id = ? ORDER BY assigned_at ASC`)
|
||||
.all(taskId) as any[];
|
||||
|
||||
// Debe existir al menos una asignación y contener al que completó
|
||||
expect(assigns.length >= 1 && assigns.length <= 2).toBe(true);
|
||||
const assignedUsers = assigns.map((r) => String(r.user_id));
|
||||
expect([U1, U2]).toContain(completedBy);
|
||||
expect(assignedUsers).toContain(completedBy);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
import { describe, it, beforeAll, afterAll, expect } from 'bun:test';
|
||||
import { createTempDb } from './helpers/db';
|
||||
import { startWebServer, type RunningServer } from './helpers/server';
|
||||
|
||||
describe('API Web - uncomplete dentro de ventana', () => {
|
||||
const USER = '34600123456';
|
||||
const GROUP = 'g-2@g.us';
|
||||
let dbHandle: any;
|
||||
let cleanupDb: () => void;
|
||||
let server: RunningServer;
|
||||
let baseUrl: string;
|
||||
let taskId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const tmp = createTempDb();
|
||||
dbHandle = tmp.db;
|
||||
cleanupDb = tmp.cleanup;
|
||||
|
||||
// Semilla mínima
|
||||
dbHandle.prepare(`INSERT INTO users (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(USER);
|
||||
dbHandle
|
||||
.prepare(`INSERT INTO groups (id, community_id, name, active) VALUES (?, 'comm-1', 'G2', 1)`)
|
||||
.run(GROUP);
|
||||
dbHandle.prepare(`INSERT INTO allowed_groups (group_id, status) VALUES (?, 'allowed')`).run(GROUP);
|
||||
dbHandle
|
||||
.prepare(`INSERT INTO group_members (group_id, user_id, is_admin, is_active) VALUES (?, ?, 0, 1)`)
|
||||
.run(GROUP, USER);
|
||||
|
||||
const ins = dbHandle
|
||||
.prepare(`INSERT INTO tasks (description, group_id, created_by) VALUES ('Probar ventana', ?, ?)`)
|
||||
.run(GROUP, USER);
|
||||
taskId = Number(ins.lastInsertRowid);
|
||||
|
||||
server = await startWebServer({
|
||||
port: 19102,
|
||||
env: { DB_PATH: tmp.path, DEV_DEFAULT_USER: USER, TZ: 'UTC' }
|
||||
});
|
||||
baseUrl = server.baseUrl;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await server.stop();
|
||||
} catch {}
|
||||
try {
|
||||
cleanupDb?.();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('uncomplete permitido inmediatamente después de completar y es idempotente', async () => {
|
||||
// Completar
|
||||
const resComplete = await fetch(`${baseUrl}/api/tasks/${taskId}/complete`, { method: 'POST' });
|
||||
expect(resComplete.status).toBe(200);
|
||||
|
||||
// Uncomplete (dentro de ventana)
|
||||
const resUncomplete = await fetch(`${baseUrl}/api/tasks/${taskId}/uncomplete`, { method: 'POST' });
|
||||
expect(resUncomplete.status).toBe(200);
|
||||
const jsonUnc = await resUncomplete.json();
|
||||
expect(jsonUnc?.status).toBe('updated');
|
||||
|
||||
// Uncomplete de nuevo (idempotente → already)
|
||||
const resUncomplete2 = await fetch(`${baseUrl}/api/tasks/${taskId}/uncomplete`, { method: 'POST' });
|
||||
expect(resUncomplete2.status).toBe(200);
|
||||
const jsonUnc2 = await resUncomplete2.json();
|
||||
expect(jsonUnc2?.status).toBe('already');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue