Files
FRASA-CAS/api-gateway/src/services/auth.service.test.ts
Consult AS Technology c8357fe582 feat(7-mejoras): notas persisten + soporte + captura visible
1. Notas propagan entre etapas (convertirLead/apartar updateMany)
2. Afluencia: conexión → aviso amarillo
3. Escrituración: click nombre abre bitácora
4. Co-acreditado: opción 'no tiene'
5. Nota del alta de lead → bitácora
6. Módulo Soporte: formulario + listado solicitudes
7. Captura visible para todos
2026-09-11 23:17:25 +00:00

176 lines
7.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import bcrypt from 'bcryptjs'
import type { Usuario } from '@prisma/client'
import { cambiarPassword, login, me, normalizarUsername, validarPasswordNueva, verificarToken } from './auth.service.js'
// ---------- Prisma mockeado (los tests no tocan la BD ni la red) ----------
const prismaMock = vi.hoisted(() => ({
usuario: {
findUnique: vi.fn(),
update: 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,
mondayUserId: null,
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',
mondayUserId: null,
modulos: ['mi-dia', 'alta-cliente', 'alta-afluencia', 'cotizador', 'mi-tablero', 'contactos', 'chat', 'clientes-maestro', 'captura', 'soporte'],
acciones: [],
})
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('incluye los módulos visibles del rol (matriz de config/roles)', async () => {
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake({ rol: 'direccion_general' }))
const resultado = await me({ sub: 'u-1', username: 'x', rol: 'direccion_general' })
expect(resultado.modulos).toEqual(['mi-dia', 'tablero-comercial', 'cobranza', 'chat', 'clientes-maestro', 'captura', 'soporte'])
})
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')
})
})
describe('validarPasswordNueva', () => {
it('exige al menos 8 caracteres y un número', () => {
expect(validarPasswordNueva('corta1')).toContain('8 caracteres')
expect(validarPasswordNueva('sinnumeros')).toContain('número')
expect(validarPasswordNueva('')).toBeTruthy()
})
it('acepta passwords que cumplen la regla', () => {
expect(validarPasswordNueva('Frasa2026')).toBeNull()
expect(validarPasswordNueva('12345678')).toBeNull()
})
})
describe('cambiarPassword', () => {
const payload = { sub: 'u-1', username: 'erick israel pena ramos', rol: 'vendedor' }
it('con actual correcta re-hashea y guarda la nueva (cost 12)', async () => {
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake())
prismaMock.usuario.update.mockResolvedValue(usuarioFake())
await cambiarPassword(payload, 'Frasa2026', 'NuevaPass2026')
expect(prismaMock.usuario.update).toHaveBeenCalledTimes(1)
const llamada = prismaMock.usuario.update.mock.calls[0]?.[0] as {
where: { id: string }
data: { passwordHash: string }
}
expect(llamada.where).toEqual({ id: 'u-1' })
expect(llamada.data.passwordHash).not.toBe('NuevaPass2026')
expect(await bcrypt.compare('NuevaPass2026', llamada.data.passwordHash)).toBe(true)
// Cost 12: el hash bcrypt lleva el factor en el propio string ($2b$12$…)
expect(llamada.data.passwordHash).toMatch(/^\$2[aby]\$12\$/)
})
it('rechaza una nueva que no cumple la política sin tocar la BD', async () => {
await expect(cambiarPassword(payload, 'Frasa2026', 'corta')).rejects.toThrow('8 caracteres')
expect(prismaMock.usuario.findUnique).not.toHaveBeenCalled()
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
})
it('con actual incorrecta responde 400 y no actualiza', async () => {
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake())
const error = await cambiarPassword(payload, 'mala', 'NuevaPass2026').catch((e: unknown) => e)
expect(error).toMatchObject({ status: 400, message: 'La contraseña actual no es correcta' })
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
})
it('usuario inexistente o inactivo: 401 genérico', async () => {
prismaMock.usuario.findUnique.mockResolvedValue(null)
await expect(cambiarPassword(payload, 'Frasa2026', 'NuevaPass2026')).rejects.toThrow(
'Credenciales inválidas',
)
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake({ activo: false }))
await expect(cambiarPassword(payload, 'Frasa2026', 'NuevaPass2026')).rejects.toThrow(
'Credenciales inválidas',
)
})
})