feat(auth): login JWT en API y las 4 apps
- Modelo Usuario + seed desde lista de vendedores (18 usuarios, bcrypt cost 12) - POST /api/auth/login + /api/auth/me; middleware Bearer en todo /api excepto health, login y webhook Monday; flag AUTH_ENABLED - Username = nombre completo normalizado (case/acentos insensible), password inicial Frasa2026, admin vía ADMIN_PASSWORD - Apps: LoginScreen + AuthGate (StudioBlank), token en localStorage, header Authorization en todas las llamadas, logout en 401 - Tests: api 136, cotizador 71, alta 65, cobranza 71, agente-ia 13, landing 4
This commit is contained in:
78
api-gateway/src/middleware/auth.middleware.test.ts
Normal file
78
api-gateway/src/middleware/auth.middleware.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
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, webhook monday)', () => {
|
||||
const casos: Partial<Request>[] = [
|
||||
{ method: 'GET', path: '/health' },
|
||||
{ method: 'POST', path: '/auth/login' },
|
||||
{ method: 'POST', path: '/webhooks/monday' },
|
||||
]
|
||||
for (const c of casos) {
|
||||
vi.clearAllMocks()
|
||||
authMiddleware(reqFake(c), resFake().res, next)
|
||||
expect(next).toHaveBeenCalledOnce()
|
||||
}
|
||||
})
|
||||
|
||||
it('protege POST /webhooks/monday con otro método no aplica: GET /webhooks/monday pide token', () => {
|
||||
const { res, status } = resFake()
|
||||
authMiddleware(reqFake({ method: 'GET', path: '/webhooks/monday' }), res, next)
|
||||
expect(status).toHaveBeenCalledWith(401)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user