- 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.
120 lines
4.2 KiB
TypeScript
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)
|
|
})
|
|
})
|