14 servicios eliminados, 7 controllers local-only, crons locales, config sin MONDAY_*, frontend sin banners/avisos Monday. 552 tests API + 7 apps construidas.
165 lines
5.5 KiB
TypeScript
165 lines
5.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { NextFunction, Request, Response } from 'express'
|
|
import jwt from 'jsonwebtoken'
|
|
import { authMiddleware } from './auth.middleware.js'
|
|
|
|
// Auth forzada a activa para este archivo (en vitest el default global es false)
|
|
vi.mock('../config/index.js', () => ({
|
|
config: {
|
|
auth: {
|
|
enabled: true,
|
|
jwtSecret: 'test-jwt-secret',
|
|
jwtExpiresIn: '7d',
|
|
adminPassword: 'Frasa2026',
|
|
},
|
|
internalSyncToken: 'test-internal-token',
|
|
},
|
|
}))
|
|
|
|
const SECRET = 'test-jwt-secret'
|
|
|
|
function reqFake(overrides: Partial<Request> = {}): Request {
|
|
return { method: 'GET', path: '/unidades', headers: {}, ...overrides } as Request
|
|
}
|
|
|
|
function resFake(): { res: Response; status: ReturnType<typeof vi.fn>; json: ReturnType<typeof vi.fn> } {
|
|
const json = vi.fn()
|
|
const status = vi.fn().mockReturnValue({ json })
|
|
return { res: { status } as unknown as Response, status, json }
|
|
}
|
|
|
|
const next: NextFunction = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('authMiddleware (AUTH_ENABLED=true)', () => {
|
|
it('401 sin header Authorization', () => {
|
|
const { res, status } = resFake()
|
|
authMiddleware(reqFake(), res, next)
|
|
expect(status).toHaveBeenCalledWith(401)
|
|
expect(next).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('401 con token inválido', () => {
|
|
const { res, status } = resFake()
|
|
authMiddleware(reqFake({ headers: { authorization: 'Bearer basura' } }), res, next)
|
|
expect(status).toHaveBeenCalledWith(401)
|
|
expect(next).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('pasa con un Bearer token válido y deja el payload en req', () => {
|
|
const token = jwt.sign({ sub: 'u-1', username: 'admin', rol: 'admin' }, SECRET)
|
|
const req = reqFake({ headers: { authorization: `Bearer ${token}` } })
|
|
const { res } = resFake()
|
|
authMiddleware(req, res, next)
|
|
expect(next).toHaveBeenCalledOnce()
|
|
expect(req.usuarioToken?.username).toBe('admin')
|
|
})
|
|
|
|
it('deja pasar las rutas públicas sin token (health, login)', () => {
|
|
const casos: Partial<Request>[] = [
|
|
{ method: 'GET', path: '/health' },
|
|
{ method: 'POST', path: '/auth/login' },
|
|
]
|
|
for (const c of casos) {
|
|
vi.clearAllMocks()
|
|
authMiddleware(reqFake(c), resFake().res, next)
|
|
expect(next).toHaveBeenCalledOnce()
|
|
}
|
|
})
|
|
|
|
it('deja pasar GET /cotizaciones/:id/publico sin token (destino del QR)', () => {
|
|
vi.clearAllMocks()
|
|
authMiddleware(
|
|
reqFake({ method: 'GET', path: '/cotizaciones/abc-123/publico' }),
|
|
resFake().res,
|
|
next,
|
|
)
|
|
expect(next).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('deja pasar los endpoints públicos de la liga de firma remota (el scope se verifica en el controlador)', () => {
|
|
const casos: Partial<Request>[] = [
|
|
{ method: 'GET', path: '/clientes/publico/token-abc' },
|
|
{ method: 'POST', path: '/clientes/publico/token-abc/firma' },
|
|
{ method: 'POST', path: '/clientes/publico/token-abc/documentos' },
|
|
]
|
|
for (const c of casos) {
|
|
vi.clearAllMocks()
|
|
authMiddleware(reqFake(c), resFake().res, next)
|
|
expect(next).toHaveBeenCalledOnce()
|
|
}
|
|
})
|
|
|
|
it('el patrón público de la liga no abre otras rutas de clientes', () => {
|
|
const casos: Partial<Request>[] = [
|
|
{ method: 'GET', path: '/clientes/abc-123' },
|
|
{ method: 'POST', path: '/clientes/abc-123/liga' },
|
|
{ method: 'POST', path: '/clientes/publico/token-abc' },
|
|
{ method: 'GET', path: '/clientes/publico/token-abc/firma' },
|
|
{ method: 'DELETE', path: '/clientes/publico/token-abc' },
|
|
{ method: 'GET', path: '/clientes/publico/token-abc/extra' },
|
|
{ method: 'GET', path: '/clientes/publico' },
|
|
]
|
|
for (const c of casos) {
|
|
const { res, status } = resFake()
|
|
authMiddleware(reqFake(c), res, next)
|
|
expect(status).toHaveBeenCalledWith(401)
|
|
}
|
|
})
|
|
|
|
it('el patrón público no abre otras rutas de cotizaciones', () => {
|
|
const casos: Partial<Request>[] = [
|
|
{ method: 'GET', path: '/cotizaciones/abc-123' },
|
|
{ method: 'GET', path: '/cotizaciones/mias' },
|
|
{ method: 'POST', path: '/cotizaciones/abc-123/publico' },
|
|
{ method: 'GET', path: '/cotizaciones/abc-123/publico/extra' },
|
|
]
|
|
for (const c of casos) {
|
|
const { res, status } = resFake()
|
|
authMiddleware(reqFake(c), res, next)
|
|
expect(status).toHaveBeenCalledWith(401)
|
|
}
|
|
})
|
|
|
|
it('el token interno abre solo POST /cotizaciones/alertas-inventario', () => {
|
|
authMiddleware(
|
|
reqFake({
|
|
method: 'POST',
|
|
path: '/cotizaciones/alertas-inventario',
|
|
headers: { 'x-internal-token': 'test-internal-token' },
|
|
}),
|
|
resFake().res,
|
|
next,
|
|
)
|
|
expect(next).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('token interno incorrecto o en otra ruta → 401', () => {
|
|
const casos: Partial<Request>[] = [
|
|
// token incorrecto en el endpoint correcto
|
|
{
|
|
method: 'POST',
|
|
path: '/cotizaciones/alertas-inventario',
|
|
headers: { 'x-internal-token': 'otro-token' },
|
|
},
|
|
// token correcto en otra ruta
|
|
{ method: 'POST', path: '/clientes', headers: { 'x-internal-token': 'test-internal-token' } },
|
|
{ method: 'GET', path: '/unidades', headers: { 'x-internal-token': 'test-internal-token' } },
|
|
// otro método sobre el endpoint
|
|
{
|
|
method: 'GET',
|
|
path: '/cotizaciones/alertas-inventario',
|
|
headers: { 'x-internal-token': 'test-internal-token' },
|
|
},
|
|
]
|
|
for (const c of casos) {
|
|
const { res, status } = resFake()
|
|
authMiddleware(reqFake(c), res, next)
|
|
expect(status).toHaveBeenCalledWith(401)
|
|
}
|
|
})
|
|
})
|