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 { return { method: 'GET', path: '/unidades', headers: {}, ...overrides } as Request } function resFake(): { res: Response; status: ReturnType; json: ReturnType } { 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[] = [ { 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[] = [ { 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[] = [ { 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[] = [ { 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[] = [ // 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) } }) })