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:
104
api-gateway/src/services/auth.service.test.ts
Normal file
104
api-gateway/src/services/auth.service.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import type { Usuario } from '@prisma/client'
|
||||
import { login, me, normalizarUsername, verificarToken } from './auth.service.js'
|
||||
|
||||
// ---------- Prisma mockeado (los tests no tocan la BD ni la red) ----------
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
usuario: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
|
||||
|
||||
function usuarioFake(overrides: Partial<Usuario> = {}): Usuario {
|
||||
return {
|
||||
id: 'u-1',
|
||||
username: 'erick israel pena ramos',
|
||||
nombre: 'ERICK ISRAEL PEÑA RAMOS',
|
||||
passwordHash: bcrypt.hashSync('Frasa2026', 12),
|
||||
rol: 'vendedor',
|
||||
asignacion: 'MONTEBELLO',
|
||||
empleadoId: 13676,
|
||||
activo: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('normalizarUsername', () => {
|
||||
it('minúsculas, sin acentos ni ñ, espacios simples', () => {
|
||||
expect(normalizarUsername(' ERICK ISRAEL PEÑA RAMOS ')).toBe('erick israel pena ramos')
|
||||
expect(normalizarUsername('Ma. Cristina Barrera López')).toBe('ma. cristina barrera lopez')
|
||||
})
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
it('devuelve token y usuario público con credenciales correctas', async () => {
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake())
|
||||
|
||||
const resultado = await login('erick israel pena ramos', 'Frasa2026')
|
||||
|
||||
expect(resultado.token).toBeTruthy()
|
||||
expect(resultado.usuario).toEqual({
|
||||
nombre: 'ERICK ISRAEL PEÑA RAMOS',
|
||||
rol: 'vendedor',
|
||||
asignacion: 'MONTEBELLO',
|
||||
})
|
||||
expect(verificarToken(resultado.token).username).toBe('erick israel pena ramos')
|
||||
})
|
||||
|
||||
it('es case-insensitive y tolerante a espacios extra y acentos', async () => {
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake())
|
||||
|
||||
const resultado = await login(' ERICK Israel PEÑA ramos ', 'Frasa2026')
|
||||
|
||||
expect(resultado.token).toBeTruthy()
|
||||
expect(prismaMock.usuario.findUnique).toHaveBeenCalledWith({
|
||||
where: { username: 'erick israel pena ramos' },
|
||||
})
|
||||
})
|
||||
|
||||
it('rechaza password incorrecto con error genérico', async () => {
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake())
|
||||
await expect(login('erick israel pena ramos', 'otra')).rejects.toThrow('Credenciales inválidas')
|
||||
})
|
||||
|
||||
it('rechaza usuario inexistente con el mismo error genérico', async () => {
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(null)
|
||||
await expect(login('nadie', 'Frasa2026')).rejects.toThrow('Credenciales inválidas')
|
||||
})
|
||||
|
||||
it('rechaza usuario inactivo con el mismo error genérico', async () => {
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake({ activo: false }))
|
||||
await expect(login('erick israel pena ramos', 'Frasa2026')).rejects.toThrow('Credenciales inválidas')
|
||||
})
|
||||
})
|
||||
|
||||
describe('verificarToken', () => {
|
||||
it('lanza 401 con token basura', () => {
|
||||
expect(() => verificarToken('no-es-un-jwt')).toThrow('Token inválido o expirado')
|
||||
})
|
||||
})
|
||||
|
||||
describe('me', () => {
|
||||
it('devuelve el usuario público del token', async () => {
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake())
|
||||
const resultado = await me({ sub: 'u-1', username: 'erick israel pena ramos', rol: 'vendedor' })
|
||||
expect(resultado.nombre).toBe('ERICK ISRAEL PEÑA RAMOS')
|
||||
})
|
||||
|
||||
it('rechaza si el usuario ya no existe o está inactivo', async () => {
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(null)
|
||||
await expect(me({ sub: 'x', username: 'x', rol: 'vendedor' })).rejects.toThrow('Credenciales inválidas')
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake({ activo: false }))
|
||||
await expect(me({ sub: 'u-1', username: 'x', rol: 'vendedor' })).rejects.toThrow('Credenciales inválidas')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user