import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Request, Response } from 'express' const mocks = vi.hoisted(() => ({ runRoundRobin: vi.fn(), escaneoOportunidades: vi.fn(), moverAGrupo: vi.fn(), })) vi.mock('../services/automatizaciones.service.js', () => ({ automatizacionesService: { runRoundRobin: mocks.runRoundRobin, escaneoOportunidades: mocks.escaneoOportunidades, }, })) vi.mock('../services/grupos-estado.service.js', () => ({ // Leads (111) y Oportunidades (222) con mapeo; el resto de boards, sin mapeo GRUPOS_POR_ESTADO: { '111': { columna: 'Estado', destinos: { NUEVO: 'topics' } }, '222': { columna: 'Estado', destinos: { Ganado: 'group_mm6scqz3' } }, }, moverAGrupoSegunEstado: mocks.moverAGrupo, })) vi.mock('../config/index.js', () => ({ config: { monday: { boards: { leads: '111', oportunidades: '222' }, }, }, })) import { webhookMonday } from './webhooks.controller.js' function resFake(): { res: Response; json: ReturnType; sendStatus: ReturnType } { const json = vi.fn() const sendStatus = vi.fn() return { res: { json, sendStatus } as unknown as Response, json, sendStatus } } const reqCon = (body: unknown): Request => ({ body } as Request) beforeEach(() => { vi.clearAllMocks() mocks.runRoundRobin.mockResolvedValue({ asignados: 1 }) mocks.escaneoOportunidades.mockResolvedValue({ apartadosCreados: ['x'], ventasCreadas: [], errores: [] }) mocks.moverAGrupo.mockResolvedValue({ movido: true, grupo: 'topics' }) }) describe('webhookMonday', () => { it('responde el challenge del handshake tal cual', () => { const { res, json, sendStatus } = resFake() webhookMonday(reqCon({ challenge: 'abc-123' }), res) expect(json).toHaveBeenCalledWith({ challenge: 'abc-123' }) expect(sendStatus).not.toHaveBeenCalled() }) it('ignora payloads sin estructura de evento (200 sin disparar nada)', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ foo: 'bar' }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.runRoundRobin).not.toHaveBeenCalled() expect(mocks.escaneoOportunidades).not.toHaveBeenCalled() }) it('create_item en Oportunidades dispara el round-robin del item', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'create_item', boardId: 222, itemId: 999 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.runRoundRobin).toHaveBeenCalledWith('999') }) it('create_pulse (nombre real del evento) con pulseId también dispara el round-robin', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'create_pulse', boardId: 222, pulseId: 777 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.runRoundRobin).toHaveBeenCalledWith('777') }) it('create_item en Leads ya NO dispara nada (tablero archivo histórico)', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'create_item', boardId: 111, itemId: 999 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.runRoundRobin).not.toHaveBeenCalled() }) it('create_item en otro board no dispara round-robin', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'create_item', boardId: 555, itemId: 999 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.runRoundRobin).not.toHaveBeenCalled() }) it('change_status_column_value en Oportunidades dispara el escaneo', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'change_status_column_value', boardId: '222', itemId: 1 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.escaneoOportunidades).toHaveBeenCalledOnce() }) it('change_column_value en Oportunidades también dispara el escaneo', () => { const { res } = resFake() webhookMonday(reqCon({ event: { type: 'change_column_value', boardId: 222, itemId: 1 } }), res) expect(mocks.escaneoOportunidades).toHaveBeenCalledOnce() }) it('update_column_value (nombre real del evento) en Oportunidades dispara el escaneo', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'update_column_value', boardId: 222, pulseId: 5 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.escaneoOportunidades).toHaveBeenCalledOnce() }) it('cambio de status en otro board no dispara el escaneo de oportunidades', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'change_status_column_value', boardId: 333, itemId: 1 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.escaneoOportunidades).not.toHaveBeenCalled() }) it('update_column_value en Leads dispara el mover a grupo según estado', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'update_column_value', boardId: 111, itemId: 999 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.moverAGrupo).toHaveBeenCalledWith(expect.anything(), '111', '999') }) it('update_column_value en Oportunidades dispara escaneo Y mover a grupo', () => { const { res } = resFake() webhookMonday(reqCon({ event: { type: 'update_column_value', boardId: 222, itemId: 5 } }), res) expect(mocks.escaneoOportunidades).toHaveBeenCalledOnce() expect(mocks.moverAGrupo).toHaveBeenCalledWith(expect.anything(), '222', '5') }) it('update_column_value en un board sin mapeo no dispara el mover a grupo', () => { const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'update_column_value', boardId: 333, itemId: 1 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) expect(mocks.moverAGrupo).not.toHaveBeenCalled() }) it('un error del servicio no rompe la respuesta 200 (fire-and-forget)', async () => { mocks.runRoundRobin.mockRejectedValue(new Error('monday caído')) const { res, sendStatus } = resFake() webhookMonday(reqCon({ event: { type: 'create_item', boardId: 222, itemId: 1 } }), res) expect(sendStatus).toHaveBeenCalledWith(200) // Deja que el rechazo se procese para no ensuciar otros tests await new Promise((r) => setImmediate(r)) }) })