import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Request, Response } from 'express' import { AppError, NotFoundError } from '../utils/errors.js' // ---------- Prisma y Monday mockeados (los tests no tocan BD ni red) ---------- const prismaMock = vi.hoisted(() => ({ cliente: { findFirst: vi.fn(), findMany: vi.fn(), }, leadCaptura: { findUnique: vi.fn(), findMany: vi.fn(), }, cotizacion: { findMany: vi.fn(), }, documento: { findMany: vi.fn(), }, usuario: { findMany: vi.fn(), }, })) const mondayMock = vi.hoisted(() => ({ getContactos: vi.fn(), getContactoDetalle: vi.fn(), getUsuariosWorkspace: vi.fn(), })) vi.mock('../services/prisma.js', () => ({ prisma: prismaMock })) vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock })) import { detalleContactoEndpoint, listarContactosEndpoint } from './contactos.controller.js' import { saludMonday } from '../services/salud-monday.service.js' function resFake() { const json = vi.fn() const status = vi.fn().mockReturnValue({ json }) return { res: { json, status } as unknown as Response, status, json } } const capturaLocal = { id: 'row-1', leadId: 'loc_1', nombreLead: 'ANA DEMO', telefono: '6649998877', email: null, origen: 'WhatsApp', desarrollo: 'SENDA', modelo: '1R', board: 'OPORTUNIDADES', etapa: 'Afluencia', motivo: null, submotivo: null, datosJson: null, mondayItemId: null, pendienteSync: true, usuarioId: 'u-1', createdAt: new Date('2026-09-02T10:00:00'), updatedAt: new Date('2026-09-03T10:00:00'), } beforeEach(() => { vi.clearAllMocks() vi.restoreAllMocks() // restaura los spyOn(saludMonday, 'estaDisponible') saludMonday.restablecer() prismaMock.cliente.findFirst.mockResolvedValue(null) prismaMock.cliente.findMany.mockResolvedValue([]) prismaMock.leadCaptura.findUnique.mockResolvedValue(null) prismaMock.leadCaptura.findMany.mockResolvedValue([capturaLocal]) prismaMock.cotizacion.findMany.mockResolvedValue([]) prismaMock.documento.findMany.mockResolvedValue([]) prismaMock.usuario.findMany.mockResolvedValue([{ id: 'u-1', nombre: 'Martin Zavala' }]) mondayMock.getUsuariosWorkspace.mockResolvedValue([]) }) describe('listarContactosEndpoint (fallback local)', () => { it('Monday caído (circuit breaker): lista local sin llamar a Monday', async () => { vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false) const { res, json } = resFake() await listarContactosEndpoint({} as Request, res) expect(mondayMock.getContactos).not.toHaveBeenCalled() const lista = json.mock.calls[0]?.[0] as Record[] expect(lista[0]).toMatchObject({ id: 'loc_1', nombre: 'ANA DEMO', estado: 'Afluencia', fuente: 'local' }) }) it('Monday lanza 502: fallback local', async () => { mondayMock.getContactos.mockRejectedValue(new AppError(502, 'Monday no respondió')) const { res, json } = resFake() await listarContactosEndpoint({} as Request, res) const lista = json.mock.calls[0]?.[0] as Record[] expect(lista[0]).toMatchObject({ id: 'loc_1', fuente: 'local' }) }) }) describe('detalleContactoEndpoint (fallback local)', () => { it('Monday caído: detalle 100% local con la misma forma', async () => { vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false) prismaMock.leadCaptura.findUnique.mockResolvedValue(capturaLocal) const { res, json } = resFake() await detalleContactoEndpoint({ params: { id: 'loc_1' } } as unknown as Request, res) expect(mondayMock.getContactoDetalle).not.toHaveBeenCalled() const detalle = json.mock.calls[0]?.[0] as Record expect(detalle['contacto']).toMatchObject({ nombre: 'ANA DEMO', Teléfono: '6649998877' }) expect(detalle['propiedad']).toBeNull() expect((detalle['relaciones'] as Record)['oportunidades']).toHaveLength(1) expect(Array.isArray(detalle['historial'])).toBe(true) }) it('Monday caído y el id no existe en local → 404', async () => { vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false) await expect( detalleContactoEndpoint({ params: { id: 'nope' } } as unknown as Request, resFake().res), ).rejects.toThrow(NotFoundError) }) it('Monday lanza 502: cae al detalle local; el 404 de Monday se propaga intacto', async () => { mondayMock.getContactoDetalle.mockRejectedValue(new AppError(502, 'Monday no respondió')) prismaMock.leadCaptura.findUnique.mockResolvedValue(capturaLocal) const { res, json } = resFake() await detalleContactoEndpoint({ params: { id: 'loc_1' } } as unknown as Request, res) expect(json).toHaveBeenCalledOnce() mondayMock.getContactoDetalle.mockRejectedValue(new NotFoundError('Contacto no encontrado')) await expect( detalleContactoEndpoint({ params: { id: 'x' } } as unknown as Request, resFake().res), ).rejects.toThrow(NotFoundError) }) })