Files
FRASA-CAS/api-gateway/src/controllers/unidades.controller.test.ts
Consult AS Technology 60649890eb feat(modo-local): fallback completo sin Monday + cola de sincronización
- Circuit breaker saludMonday (3 fallos → modo local; ping cada 60s)
- Endpoint GET /api/sistema/estado {mondayOk, pendientesSync}
- LeadCaptura extendida (board, etapa, origen, motivo, pendienteSync)
- Tablas Unidad (espejo inventario, 288 seed) + SyncPendiente
- crmLocal: crearLead, afluencia, cambio etapa, descarte, conversión, apartar
- Ruteo en controllers: fallback local ante 502 o Monday caído
- Mi día local: resumen completo desde SQLite + unión con Monday
- Fallbacks: unidades, contactos, reportes, prefill, buscador
- Sync worker cada 2min + handlers 15 tipos (con DependenciaPendienteError)
- Gate automatizaciones: skip si !mondayOk
- Banner 'Modo local' en 5 apps + badge pendiente en mi-dia
- 597 API tests + 263 front tests verdes

Env MODO_LOCAL_FORZADO=true para testing sin Monday.
2026-09-06 08:08:56 +00:00

120 lines
4.2 KiB
TypeScript

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(() => ({
unidad: {
findMany: vi.fn(),
findUnique: vi.fn(),
},
}))
const mondayMock = vi.hoisted(() => ({
getUnidades: vi.fn(),
getUnidad: vi.fn(),
}))
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
import { listarUnidades, obtenerUnidad } from './unidades.controller.js'
import { saludMonday } from '../services/salud-monday.service.js'
const filaLocal = {
codigo: 'U-101',
desarrollo: 'DIAGONAL',
modelo: '2RA',
supConstruccion: 70,
supExcedente: 0,
supEstacionamiento: 12.5,
precioLista: 2_000_000,
precioAcabados: 2_100_000,
status: 'DISPONIBLE',
etapa: 'Entrega inmediata',
}
function resFake() {
const json = vi.fn()
return { res: { json } as unknown as Response, json }
}
beforeEach(() => {
vi.clearAllMocks()
vi.restoreAllMocks() // restaura los spyOn(saludMonday, 'estaDisponible')
saludMonday.restablecer()
prismaMock.unidad.findMany.mockResolvedValue([filaLocal])
prismaMock.unidad.findUnique.mockResolvedValue(filaLocal)
})
describe('unidades.controller (fallback local)', () => {
it('Monday vivo: devuelve Monday y no toca la tabla local', async () => {
mondayMock.getUnidades.mockResolvedValue([{ codigo: 'M-1', status: 'DISPONIBLE', desarrollo: 'SENDA' }])
const { res, json } = resFake()
await listarUnidades({ query: {} } as unknown as Request, res)
expect(json).toHaveBeenCalledWith([{ codigo: 'M-1', status: 'DISPONIBLE', desarrollo: 'SENDA' }])
expect(prismaMock.unidad.findMany).not.toHaveBeenCalled()
})
it('Monday caído (circuit breaker): lista desde la tabla Unidad con la misma forma de DTO', async () => {
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false)
const { res, json } = resFake()
await listarUnidades({ query: {} } as unknown as Request, res)
expect(mondayMock.getUnidades).not.toHaveBeenCalled()
const lista = json.mock.calls[0]?.[0] as Record<string, unknown>[]
expect(lista).toHaveLength(1)
expect(lista[0]).toMatchObject({
codigo: 'U-101',
modelo: '2RA',
precioSinAcabados: 2_000_000,
precioConAcabados: 2_100_000,
status: 'DISPONIBLE',
desarrollo: 'DIAGONAL',
etapa: 'Entrega inmediata',
})
})
it('Monday caído: los filtros ?disponibles y ?desarrollo se aplican igual', async () => {
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false)
prismaMock.unidad.findMany.mockResolvedValue([
filaLocal,
{ ...filaLocal, codigo: 'U-102', status: 'APARTADO' },
{ ...filaLocal, codigo: 'S-1', desarrollo: 'SENDA' },
])
const { res, json } = resFake()
await listarUnidades({ query: { disponibles: 'true', desarrollo: 'senda' } } as unknown as Request, res)
const lista = json.mock.calls[0]?.[0] as Record<string, unknown>[]
expect(lista.map((u) => u['codigo'])).toEqual(['S-1'])
})
it('Monday lanza 502: fallback local; otro error se propaga', async () => {
mondayMock.getUnidades.mockRejectedValue(new AppError(502, 'Monday no respondió'))
const { res, json } = resFake()
await listarUnidades({ query: {} } as unknown as Request, res)
expect((json.mock.calls[0]?.[0] as unknown[]).length).toBe(1)
mondayMock.getUnidades.mockRejectedValue(new AppError(500, 'bug'))
await expect(listarUnidades({ query: {} } as unknown as Request, resFake().res)).rejects.toThrow('bug')
})
it('obtenerUnidad: Monday caído → local; inexistente en ambos → 404', async () => {
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false)
const { res, json } = resFake()
await obtenerUnidad({ params: { codigo: 'U-101' } } as unknown as Request, res)
expect((json.mock.calls[0]?.[0] as Record<string, unknown>)['codigo']).toBe('U-101')
prismaMock.unidad.findUnique.mockResolvedValue(null)
await expect(
obtenerUnidad({ params: { codigo: 'U-X' } } as unknown as Request, resFake().res),
).rejects.toThrow(NotFoundError)
})
})