350 lines
10 KiB
TypeScript
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()
|
|
}
|
|
})
|
|
})
|
|
})
|