Compare commits
8 Commits
5efcbbc98b
...
773a0e8912
| Author | SHA1 | Date |
|---|---|---|
|
|
773a0e8912 | 1 week ago |
|
|
fceb6baa04 | 1 week ago |
|
|
9c4498c5cb | 1 week ago |
|
|
d7bf328db5 | 1 week ago |
|
|
01c274a8ca | 1 week ago |
|
|
39c3f97e4c | 1 week ago |
|
|
60cb5877d8 | 1 week ago |
|
|
7a5f933b8c | 1 week ago |
@ -0,0 +1,97 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||||
|
import { ResponseQueue } from '../../../src/services/response-queue';
|
||||||
|
|
||||||
|
describe('ResponseQueue - payload de reacción', () => {
|
||||||
|
const OLD_FETCH = globalThis.fetch;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.EVOLUTION_API_URL = 'http://evolution.local';
|
||||||
|
process.env.EVOLUTION_API_INSTANCE = 'instance-1';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = OLD_FETCH;
|
||||||
|
delete process.env.EVOLUTION_API_URL;
|
||||||
|
delete process.env.EVOLUTION_API_INSTANCE;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incluye participant y fromMe cuando están presentes', async () => {
|
||||||
|
const calls: any[] = [];
|
||||||
|
globalThis.fetch = (async (url: any, init?: any) => {
|
||||||
|
calls.push({ url, init });
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => ''
|
||||||
|
} as any;
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: 1,
|
||||||
|
recipient: '12345-67890@g.us',
|
||||||
|
message: '',
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
kind: 'reaction',
|
||||||
|
emoji: '✅',
|
||||||
|
chatId: '12345-67890@g.us',
|
||||||
|
messageId: 'MSG-123',
|
||||||
|
fromMe: true,
|
||||||
|
participant: '34600123456@s.whatsapp.net'
|
||||||
|
}),
|
||||||
|
attempts: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await ResponseQueue.sendOne(item as any);
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
expect(calls.length).toBe(1);
|
||||||
|
const { url, init } = calls[0];
|
||||||
|
expect(String(url)).toBe('http://evolution.local/message/sendReaction/instance-1');
|
||||||
|
const payload = JSON.parse(String(init.body || '{}'));
|
||||||
|
expect(payload).toBeTruthy();
|
||||||
|
expect(payload.reaction).toBe('✅');
|
||||||
|
expect(payload.key).toEqual({
|
||||||
|
remoteJid: '12345-67890@g.us',
|
||||||
|
fromMe: true,
|
||||||
|
id: 'MSG-123',
|
||||||
|
participant: '34600123456@s.whatsapp.net'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omite participant y usa fromMe=false por defecto cuando no se proveen', async () => {
|
||||||
|
const calls: any[] = [];
|
||||||
|
globalThis.fetch = (async (url: any, init?: any) => {
|
||||||
|
calls.push({ url, init });
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => ''
|
||||||
|
} as any;
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: 2,
|
||||||
|
recipient: '12345-67890@g.us',
|
||||||
|
message: '',
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
kind: 'reaction',
|
||||||
|
emoji: '✅',
|
||||||
|
chatId: '12345-67890@g.us',
|
||||||
|
messageId: 'MSG-456'
|
||||||
|
}),
|
||||||
|
attempts: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await ResponseQueue.sendOne(item as any);
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
const { url, init } = calls[0];
|
||||||
|
expect(String(url)).toBe('http://evolution.local/message/sendReaction/instance-1');
|
||||||
|
const payload = JSON.parse(String(init.body || '{}'));
|
||||||
|
expect(payload.reaction).toBe('✅');
|
||||||
|
expect(payload.key.remoteJid).toBe('12345-67890@g.us');
|
||||||
|
expect(payload.key.id).toBe('MSG-456');
|
||||||
|
expect(payload.key.fromMe).toBe(false);
|
||||||
|
expect('participant' in payload.key).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
import { beforeEach, afterEach, describe, expect, it } from 'bun:test';
|
||||||
|
import { createTempDb } from './helpers/db';
|
||||||
|
// Los imports del handler y closeDb se hacen dinámicos dentro de cada test/teardown
|
||||||
|
|
||||||
|
function toIsoSql(d: Date): string {
|
||||||
|
return d.toISOString().replace('T', ' ').replace('Z', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Web API - completar tarea encola reacción ✅', () => {
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Sembrar usuario y grupo permitido + membresía activa
|
||||||
|
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-1', 'Group', 1)`).run(GROUP_ID);
|
||||||
|
db.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, label, status) VALUES (?, 'Test', 'allowed')`).run(GROUP_ID);
|
||||||
|
db.prepare(`INSERT OR REPLACE INTO group_members (group_id, user_id, is_admin, is_active) VALUES (?, ?, 0, 1)`).run(GROUP_ID, USER);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Cerrar la conexión singleton de la web antes de borrar el archivo
|
||||||
|
try {
|
||||||
|
const { closeDb } = await import('../../apps/web/src/lib/server/db.ts');
|
||||||
|
closeDb();
|
||||||
|
} catch {}
|
||||||
|
if (cleanup) cleanup();
|
||||||
|
// Limpiar env relevantes
|
||||||
|
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('caso feliz: encola 1 reacción ✅ con metadata canónica', async () => {
|
||||||
|
// Crear tarea en grupo (no completada)
|
||||||
|
const ins = db.prepare(`
|
||||||
|
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
|
||||||
|
VALUES ('Probar reacción', NULL, ?, ?, 0, NULL)
|
||||||
|
`).run(GROUP_ID, USER) as any;
|
||||||
|
const taskId = Number(ins.lastInsertRowid);
|
||||||
|
|
||||||
|
// Origen reciente con participant y from_me=1
|
||||||
|
const messageId = 'MSG-abc-123';
|
||||||
|
db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at, participant, from_me)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(taskId, GROUP_ID, messageId, toIsoSql(new Date()), `${USER}@s.whatsapp.net`, 1);
|
||||||
|
|
||||||
|
// Ejecutar endpoint
|
||||||
|
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(200);
|
||||||
|
const payload = await res.json();
|
||||||
|
expect(payload.status).toBe('updated');
|
||||||
|
|
||||||
|
// Verificar encolado
|
||||||
|
const row = db.prepare(`SELECT recipient, message, metadata FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any;
|
||||||
|
expect(row).toBeTruthy();
|
||||||
|
expect(String(row.recipient)).toBe(GROUP_ID);
|
||||||
|
expect(String(row.message)).toBe('');
|
||||||
|
|
||||||
|
const meta = JSON.parse(String(row.metadata || '{}'));
|
||||||
|
expect(meta).toEqual({
|
||||||
|
kind: 'reaction',
|
||||||
|
emoji: '✅',
|
||||||
|
chatId: GROUP_ID,
|
||||||
|
messageId,
|
||||||
|
fromMe: true,
|
||||||
|
participant: `${USER}@s.whatsapp.net`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Idempotencia del endpoint: segunda llamada no crea nuevo job
|
||||||
|
const res2 = await completeHandler(event);
|
||||||
|
expect(res2.status).toBe(200);
|
||||||
|
const body2 = await res2.json();
|
||||||
|
expect(body2.status).toBe('already');
|
||||||
|
|
||||||
|
const cnt = db.prepare(`SELECT COUNT(*) AS c FROM response_queue WHERE metadata = ?`).get(JSON.stringify(meta)) as any;
|
||||||
|
expect(Number(cnt.c || 0)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TTL vencido: no encola reacción', async () => {
|
||||||
|
const ins = db.prepare(`
|
||||||
|
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
|
||||||
|
VALUES ('Vieja', NULL, ?, ?, 0, NULL)
|
||||||
|
`).run(GROUP_ID, USER) as any;
|
||||||
|
const taskId = Number(ins.lastInsertRowid);
|
||||||
|
|
||||||
|
const messageId = 'MSG-old-001';
|
||||||
|
const old = new Date(Date.now() - 20 * 24 * 60 * 60 * 1000); // 20 días
|
||||||
|
db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`).run(taskId, GROUP_ID, messageId, toIsoSql(old));
|
||||||
|
|
||||||
|
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(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.status).toBe('updated');
|
||||||
|
|
||||||
|
const cnt = db.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any;
|
||||||
|
expect(Number(cnt.c || 0)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scope=groups: origen DM no encola', async () => {
|
||||||
|
const ins = db.prepare(`
|
||||||
|
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
|
||||||
|
VALUES ('DM scope', NULL, ?, ?, 0, NULL)
|
||||||
|
`).run(GROUP_ID, USER) as any;
|
||||||
|
const taskId = Number(ins.lastInsertRowid);
|
||||||
|
|
||||||
|
const messageId = 'MSG-dm-001';
|
||||||
|
const dmChat = `${USER}@s.whatsapp.net`; // no @g.us
|
||||||
|
db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`).run(taskId, dmChat, messageId, toIsoSql(new Date()));
|
||||||
|
|
||||||
|
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(200);
|
||||||
|
|
||||||
|
const cnt = db.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any;
|
||||||
|
expect(Number(cnt.c || 0)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sin participant/from_me: metadata no incluye claves opcionales', async () => {
|
||||||
|
const ins = db.prepare(`
|
||||||
|
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
|
||||||
|
VALUES ('Sin opcionales', NULL, ?, ?, 0, NULL)
|
||||||
|
`).run(GROUP_ID, USER) as any;
|
||||||
|
const taskId = Number(ins.lastInsertRowid);
|
||||||
|
|
||||||
|
const messageId = 'MSG-nopts-001';
|
||||||
|
db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`).run(taskId, GROUP_ID, messageId, toIsoSql(new Date()));
|
||||||
|
|
||||||
|
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(200);
|
||||||
|
|
||||||
|
const row = db.prepare(`SELECT metadata FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any;
|
||||||
|
const meta = JSON.parse(String(row.metadata || '{}'));
|
||||||
|
expect(meta).toEqual({
|
||||||
|
kind: 'reaction',
|
||||||
|
emoji: '✅',
|
||||||
|
chatId: GROUP_ID,
|
||||||
|
messageId
|
||||||
|
});
|
||||||
|
expect('fromMe' in meta).toBe(false);
|
||||||
|
expect('participant' in meta).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue