Files
SIG-CM2.0/src/web/src/tests/stores/authStore.test.ts

350 lines
10 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { useAuthStore } from '../../stores/authStore'
// Canonical test user fixtures
const adminUser = {
id: 1,
username: 'admin',
nombre: 'Admin',
rol: 'admin',
permisos: [] as string[],
mustChangePassword: false,
}
const cajeroUser = {
id: 2,
username: 'cajero',
nombre: 'Cajero',
rol: 'cajero',
permisos: [] as string[],
mustChangePassword: false,
}
describe('authStore', () => {
beforeEach(() => {
// Reset store state before each test
useAuthStore.setState({
user: null,
accessToken: null,
refreshToken: null,
expiresAt: null,
})
localStorage.clear()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('initial state', () => {
it('starts with null user and null accessToken', () => {
const state = useAuthStore.getState()
expect(state.user).toBeNull()
expect(state.accessToken).toBeNull()
})
})
describe('setAuth', () => {
it('stores user and accessToken in state', () => {
const payload = {
user: adminUser,
accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature',
refreshToken: 'opaque-refresh-token',
expiresIn: 3600,
}
useAuthStore.getState().setAuth(payload)
const state = useAuthStore.getState()
expect(state.user).toEqual(payload.user)
expect(state.accessToken).toBe(payload.accessToken)
})
it('persists auth data to localStorage under auth-storage key', () => {
const payload = {
user: adminUser,
accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature',
refreshToken: 'opaque-refresh-token',
expiresIn: 3600,
}
useAuthStore.getState().setAuth(payload)
const stored = localStorage.getItem('auth-storage')
expect(stored).not.toBeNull()
const parsed = JSON.parse(stored!)
expect(parsed.state.accessToken).toBe(payload.accessToken)
expect(parsed.state.user.username).toBe('admin')
})
it('setAuth_persistsRefreshTokenAndExpiresAt', () => {
const before = Date.now()
const payload = {
user: adminUser,
accessToken: 'access-token-abc',
refreshToken: 'opaque-refresh-xyz',
expiresIn: 3600,
}
useAuthStore.getState().setAuth(payload)
const after = Date.now()
const state = useAuthStore.getState()
expect(state.refreshToken).toBe('opaque-refresh-xyz')
expect(state.expiresAt).not.toBeNull()
// expiresAt should be ~Date.now() + 3600*1000
expect(state.expiresAt!).toBeGreaterThanOrEqual(before + 3600 * 1000)
expect(state.expiresAt!).toBeLessThanOrEqual(after + 3600 * 1000)
// Should also be persisted in localStorage
const stored = localStorage.getItem('auth-storage')
const parsed = JSON.parse(stored!)
expect(parsed.state.refreshToken).toBe('opaque-refresh-xyz')
expect(parsed.state.expiresAt).toBeGreaterThan(0)
})
it('F-04-01: setAuth con permisos → user.permisos contiene los valores', () => {
const payload = {
user: {
id: 1,
username: 'admin',
nombre: 'Admin',
rol: 'admin',
permisos: ['administracion:usuarios:gestionar', 'administracion:roles:gestionar'],
mustChangePassword: false,
},
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
}
useAuthStore.getState().setAuth(payload)
const state = useAuthStore.getState()
expect(state.user?.permisos).toContain('administracion:usuarios:gestionar')
expect(state.user?.permisos).toContain('administracion:roles:gestionar')
expect(state.user?.permisos).toHaveLength(2)
})
it('F-04-02: setAuth con permisos vacíos → user.permisos es [] (no null)', () => {
const payload = {
user: cajeroUser,
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
}
useAuthStore.getState().setAuth(payload)
const state = useAuthStore.getState()
expect(state.user?.permisos).toEqual([])
expect(state.user?.permisos).not.toBeNull()
})
it('persists mustChangePassword=true in state and localStorage', () => {
const payload = {
user: { ...adminUser, mustChangePassword: true },
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
}
useAuthStore.getState().setAuth(payload)
const state = useAuthStore.getState()
expect(state.user?.mustChangePassword).toBe(true)
const stored = localStorage.getItem('auth-storage')
const parsed = JSON.parse(stored!)
expect(parsed.state.user.mustChangePassword).toBe(true)
})
it('persists mustChangePassword=false in state', () => {
const payload = {
user: { ...adminUser, mustChangePassword: false },
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
}
useAuthStore.getState().setAuth(payload)
const state = useAuthStore.getState()
expect(state.user?.mustChangePassword).toBe(false)
})
})
describe('updateUser', () => {
it('updateUser_patches_mustChangePassword_preserves_rest', () => {
useAuthStore.getState().setAuth({
user: { ...adminUser, mustChangePassword: true },
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
})
useAuthStore.getState().updateUser({ mustChangePassword: false })
const state = useAuthStore.getState()
expect(state.user?.mustChangePassword).toBe(false)
// Other fields preserved
expect(state.user?.username).toBe('admin')
expect(state.user?.rol).toBe('admin')
expect(state.user?.id).toBe(1)
})
it('updateUser_noops_when_user_null', () => {
// user is null — should not throw
expect(() => useAuthStore.getState().updateUser({ mustChangePassword: false })).not.toThrow()
expect(useAuthStore.getState().user).toBeNull()
})
it('updateUser_can_patch_username', () => {
useAuthStore.getState().setAuth({
user: adminUser,
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
})
useAuthStore.getState().updateUser({ username: 'new-admin' })
expect(useAuthStore.getState().user?.username).toBe('new-admin')
})
})
describe('clearAuth', () => {
it('F-04-03: clearAuth → user = null (permisos se limpian con el user)', () => {
useAuthStore.getState().setAuth({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: ['administracion:usuarios:gestionar'], mustChangePassword: false },
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
})
useAuthStore.getState().clearAuth()
const state = useAuthStore.getState()
expect(state.user).toBeNull()
})
it('clearAuth_removesAllFields', () => {
useAuthStore.getState().setAuth({
user: adminUser,
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
})
useAuthStore.getState().clearAuth()
const state = useAuthStore.getState()
expect(state.user).toBeNull()
expect(state.accessToken).toBeNull()
expect(state.refreshToken).toBeNull()
expect(state.expiresAt).toBeNull()
})
})
describe('updateAccess', () => {
it('updateAccess_updatesOnlyTokens_preservesUser', () => {
useAuthStore.getState().setAuth({
user: adminUser,
accessToken: 'old-access',
refreshToken: 'old-refresh',
expiresIn: 3600,
})
const newExpiresAt = Date.now() + 7200 * 1000
useAuthStore.getState().updateAccess('new-access', 'new-refresh', newExpiresAt)
const state = useAuthStore.getState()
expect(state.accessToken).toBe('new-access')
expect(state.refreshToken).toBe('new-refresh')
expect(state.expiresAt).toBe(newExpiresAt)
// user should be preserved
expect(state.user).toEqual(adminUser)
})
})
describe('logout (async)', () => {
it('logout_callsApi_thenClearsAuth', async () => {
// Set up auth state with a token so logout() will try to call the API
useAuthStore.getState().setAuth({
user: adminUser,
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
})
// logout() does a lazy import of authApi and calls logout()
// The API call may succeed or fail (we don't control the real import here),
// but clearAuth() is ALWAYS called after regardless.
await useAuthStore.getState().logout()
const state = useAuthStore.getState()
expect(state.user).toBeNull()
expect(state.accessToken).toBeNull()
expect(state.refreshToken).toBeNull()
expect(state.expiresAt).toBeNull()
})
it('logout_apiFails_stillClearsAuth', async () => {
useAuthStore.getState().setAuth({
user: adminUser,
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 3600,
})
// Should NOT throw even if the dynamic import fails
let threw = false
try {
await useAuthStore.getState().logout()
} catch {
threw = true
}
expect(threw).toBe(false)
const state = useAuthStore.getState()
expect(state.user).toBeNull()
expect(state.accessToken).toBeNull()
})
})
describe('legacy logout compatibility (via clearAuth)', () => {
it('clearAuth clears user and accessToken from state', () => {
useAuthStore.getState().setAuth({
user: adminUser,
accessToken: 'some-token',
refreshToken: 'some-refresh',
expiresIn: 3600,
})
useAuthStore.getState().clearAuth()
const state = useAuthStore.getState()
expect(state.user).toBeNull()
expect(state.accessToken).toBeNull()
})
it('clearAuth removes auth-storage from localStorage', () => {
useAuthStore.getState().setAuth({
user: adminUser,
accessToken: 'some-token',
refreshToken: 'some-refresh',
expiresIn: 3600,
})
useAuthStore.getState().clearAuth()
const stored = localStorage.getItem('auth-storage')
if (stored !== null) {
const parsed = JSON.parse(stored)
expect(parsed.state.user).toBeNull()
expect(parsed.state.accessToken).toBeNull()
}
})
})
})