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.
		
		
		
		
		
			
		
			
				
	
	
		
			195 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			195 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
| 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);
 | |
|   });
 | |
| });
 |