UDT-008: Gestión completa de usuarios #11

Merged
dmolinari merged 16 commits from feature/UDT-008 into main 2026-04-16 00:01:36 +00:00
12 changed files with 306 additions and 18 deletions
Showing only changes of commit d998d215e0 - Show all commits

View File

@@ -0,0 +1,28 @@
import type { ReactNode } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/stores/authStore'
interface MustChangePasswordGateProps {
children: ReactNode
}
/**
* Router guard for the "must change password" flow (UDT-008).
*
* If the authenticated user has mustChangePassword=true and is NOT already
* on /perfil/contrasena, redirects them there.
*
* Place this INSIDE ProtectedRoute so it only fires for authenticated users.
* The /perfil/contrasena route itself must NOT be wrapped with this gate
* to avoid redirect loops.
*/
export function MustChangePasswordGate({ children }: MustChangePasswordGateProps) {
const user = useAuthStore((s) => s.user)
const location = useLocation()
if (user?.mustChangePassword && location.pathname !== '/perfil/contrasena') {
return <Navigate to="/perfil/contrasena" replace />
}
return <>{children}</>
}

View File

@@ -10,6 +10,7 @@ export interface LoginResponseDto {
nombre: string nombre: string
rol: string rol: string
permisos: string[] permisos: string[]
mustChangePassword: boolean // UDT-008
} }
} }

View File

@@ -20,6 +20,7 @@ export function useLogin() {
nombre: data.usuario.nombre, nombre: data.usuario.nombre,
rol: data.usuario.rol, rol: data.usuario.rol,
permisos: data.usuario.permisos ?? [], permisos: data.usuario.permisos ?? [],
mustChangePassword: data.usuario.mustChangePassword ?? false, // UDT-008
}, },
accessToken: data.accessToken, accessToken: data.accessToken,
refreshToken: data.refreshToken, refreshToken: data.refreshToken,

View File

@@ -7,6 +7,7 @@ export interface AuthUser {
nombre: string nombre: string
rol: string rol: string
permisos: string[] permisos: string[]
mustChangePassword: boolean // UDT-008
} }
interface SetAuthPayload { interface SetAuthPayload {
@@ -22,6 +23,7 @@ interface AuthState {
refreshToken: string | null refreshToken: string | null
expiresAt: number | null // ms epoch UTC expiresAt: number | null // ms epoch UTC
setAuth: (payload: SetAuthPayload) => void setAuth: (payload: SetAuthPayload) => void
updateUser: (patch: Partial<AuthUser>) => void // UDT-008
updateAccess: (accessToken: string, refreshToken: string, expiresAt: number) => void updateAccess: (accessToken: string, refreshToken: string, expiresAt: number) => void
clearAuth: () => void clearAuth: () => void
logout: () => Promise<void> logout: () => Promise<void>
@@ -43,6 +45,11 @@ export const useAuthStore = create<AuthState>()(
expiresAt: Date.now() + payload.expiresIn * 1000, expiresAt: Date.now() + payload.expiresIn * 1000,
}), }),
updateUser: (patch) =>
set((s) => ({
user: s.user ? { ...s.user, ...patch } : null,
})),
updateAccess: (accessToken, refreshToken, expiresAt) => updateAccess: (accessToken, refreshToken, expiresAt) =>
set({ accessToken, refreshToken, expiresAt }), set({ accessToken, refreshToken, expiresAt }),

View File

@@ -49,7 +49,7 @@ afterEach(() => {
function setAuth(accessToken: string, refreshToken: string) { function setAuth(accessToken: string, refreshToken: string) {
useAuthStore.setState({ useAuthStore.setState({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' }, user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [], mustChangePassword: false },
accessToken, accessToken,
refreshToken, refreshToken,
expiresAt: Date.now() + 3600 * 1000, expiresAt: Date.now() + 3600 * 1000,

View File

@@ -0,0 +1,91 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import { useAuthStore } from '../../../stores/authStore'
import { MustChangePasswordGate } from '../../../components/routing/MustChangePasswordGate'
const adminUser = {
id: 1,
username: 'admin',
nombre: 'Admin',
rol: 'admin',
permisos: ['administracion:usuarios:gestionar'],
mustChangePassword: false,
}
beforeEach(() => {
useAuthStore.setState({ user: null, accessToken: null, refreshToken: null, expiresAt: null })
})
function renderGate(initialPath: string, mustChangePassword: boolean | null) {
if (mustChangePassword !== null) {
useAuthStore.setState({ user: { ...adminUser, mustChangePassword } })
}
return render(
<MemoryRouter initialEntries={[initialPath]}>
<Routes>
<Route path="/perfil/contrasena" element={<div>Change Password Page</div>} />
<Route
path="*"
element={
<MustChangePasswordGate>
<div>Protected Content</div>
</MustChangePasswordGate>
}
/>
</Routes>
</MemoryRouter>,
)
}
describe('MustChangePasswordGate', () => {
it('redirects to /perfil/contrasena when mustChangePassword=true and on different route', () => {
renderGate('/usuarios', true)
expect(screen.getByText('Change Password Page')).toBeInTheDocument()
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument()
})
it('redirects to /perfil/contrasena when mustChangePassword=true on root', () => {
renderGate('/', true)
expect(screen.getByText('Change Password Page')).toBeInTheDocument()
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument()
})
it('renders children when mustChangePassword=false', () => {
renderGate('/usuarios', false)
expect(screen.getByText('Protected Content')).toBeInTheDocument()
expect(screen.queryByText('Change Password Page')).not.toBeInTheDocument()
})
it('renders children when user is null (let ProtectedRoute handle auth)', () => {
// user is null — gate should pass through, ProtectedRoute will handle it
renderGate('/usuarios', null)
expect(screen.getByText('Protected Content')).toBeInTheDocument()
})
it('allows render on /perfil/contrasena when mustChangePassword=true (no redirect loop)', () => {
useAuthStore.setState({ user: { ...adminUser, mustChangePassword: true } })
render(
<MemoryRouter initialEntries={['/perfil/contrasena']}>
<Routes>
<Route
path="/perfil/contrasena"
element={
<MustChangePasswordGate>
<div>Change Password Page Content</div>
</MustChangePasswordGate>
}
/>
</Routes>
</MemoryRouter>,
)
expect(screen.getByText('Change Password Page Content')).toBeInTheDocument()
})
})

View File

@@ -16,6 +16,7 @@ describe('CanPerform', () => {
nombre: 'Admin', nombre: 'Admin',
rol: 'admin', rol: 'admin',
permisos: ['administracion:usuarios:gestionar'], permisos: ['administracion:usuarios:gestionar'],
mustChangePassword: false,
}, },
}) })
@@ -36,6 +37,7 @@ describe('CanPerform', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })
@@ -68,6 +70,7 @@ describe('CanPerform', () => {
nombre: 'Reportes', nombre: 'Reportes',
rol: 'reportes', rol: 'reportes',
permisos: [], permisos: [],
mustChangePassword: false,
}, },
}) })
@@ -89,6 +92,7 @@ describe('CanPerform', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })

View File

@@ -21,7 +21,7 @@ const mockLoginResponse = {
accessToken: 'eyJhbGciOiJSUzI1NiJ9.payload.sig', accessToken: 'eyJhbGciOiJSUzI1NiJ9.payload.sig',
refreshToken: 'refresh-token-abc', refreshToken: 'refresh-token-abc',
expiresIn: 3600, expiresIn: 3600,
usuario: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: ['administracion:usuarios:gestionar'] }, usuario: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: ['administracion:usuarios:gestionar'], mustChangePassword: false },
} }
const server = setupServer( const server = setupServer(

View File

@@ -71,7 +71,7 @@ describe('ProtectedRoute', () => {
it('F-03-02: user autenticado sin restricciones → renderiza children', () => { it('F-03-02: user autenticado sin restricciones → renderiza children', () => {
useAuthStore.setState({ useAuthStore.setState({
user: { id: 2, username: 'cajero', nombre: 'Cajero', rol: 'cajero', permisos: [] }, user: { id: 2, username: 'cajero', nombre: 'Cajero', rol: 'cajero', permisos: [], mustChangePassword: false },
}) })
render( render(
@@ -101,6 +101,7 @@ describe('ProtectedRoute', () => {
nombre: 'Admin', nombre: 'Admin',
rol: 'admin', rol: 'admin',
permisos: ['administracion:usuarios:gestionar'], permisos: ['administracion:usuarios:gestionar'],
mustChangePassword: false,
}, },
}) })
@@ -126,7 +127,7 @@ describe('ProtectedRoute', () => {
it('F-03-04: requiredRoles no coincide → redirect a /', () => { it('F-03-04: requiredRoles no coincide → redirect a /', () => {
useAuthStore.setState({ useAuthStore.setState({
user: { id: 2, username: 'cajero', nombre: 'Cajero', rol: 'cajero', permisos: [] }, user: { id: 2, username: 'cajero', nombre: 'Cajero', rol: 'cajero', permisos: [], mustChangePassword: false },
}) })
render( render(
@@ -158,6 +159,7 @@ describe('ProtectedRoute', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })
@@ -191,6 +193,7 @@ describe('ProtectedRoute', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })
@@ -223,6 +226,7 @@ describe('ProtectedRoute', () => {
nombre: 'Admin', nombre: 'Admin',
rol: 'admin', rol: 'admin',
permisos: ['administracion:usuarios:gestionar'], permisos: ['administracion:usuarios:gestionar'],
mustChangePassword: false,
}, },
}) })
@@ -254,6 +258,7 @@ describe('ProtectedRoute', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })

View File

@@ -19,6 +19,7 @@ const mockLoginResponseWithPermisos = {
nombre: 'Admin Sistema', nombre: 'Admin Sistema',
rol: 'admin', rol: 'admin',
permisos: ['administracion:usuarios:gestionar', 'administracion:roles:gestionar'], permisos: ['administracion:usuarios:gestionar', 'administracion:roles:gestionar'],
mustChangePassword: false,
}, },
} }
@@ -32,6 +33,21 @@ const mockLoginResponseEmptyPermisos = {
nombre: 'Cajero Test', nombre: 'Cajero Test',
rol: 'cajero', rol: 'cajero',
permisos: [], permisos: [],
mustChangePassword: false,
},
}
const mockLoginResponseMustChange = {
accessToken: 'eyJhbGciOiJSUzI1NiJ9.payload.sig',
refreshToken: 'refresh-token-abc',
expiresIn: 3600,
usuario: {
id: 3,
username: 'newuser',
nombre: 'New User',
rol: 'cajero',
permisos: [],
mustChangePassword: true,
}, },
} }
@@ -94,3 +110,44 @@ describe('useLogin — permisos propagation', () => {
expect(state.user?.permisos).not.toBeNull() expect(state.user?.permisos).not.toBeNull()
}) })
}) })
describe('useLogin — mustChangePassword propagation', () => {
it('F-login-03: persists mustChangePassword=false from login response', async () => {
server.use(
http.post(`${API_URL}/api/v1/auth/login`, () =>
HttpResponse.json(mockLoginResponseWithPermisos, { status: 200 }),
),
)
const { result } = renderHook(() => useLogin(), { wrapper: createWrapper() })
act(() => {
result.current.mutate({ username: 'admin', password: 'password' })
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
const state = useAuthStore.getState()
expect(state.user?.mustChangePassword).toBe(false)
})
it('F-login-04: persists mustChangePassword=true from login response', async () => {
server.use(
http.post(`${API_URL}/api/v1/auth/login`, () =>
HttpResponse.json(mockLoginResponseMustChange, { status: 200 }),
),
)
const { result } = renderHook(() => useLogin(), { wrapper: createWrapper() })
act(() => {
result.current.mutate({ username: 'newuser', password: 'password' })
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
const state = useAuthStore.getState()
expect(state.user?.mustChangePassword).toBe(true)
expect(state.user?.username).toBe('newuser')
})
})

View File

@@ -16,6 +16,7 @@ describe('usePermission', () => {
nombre: 'Admin', nombre: 'Admin',
rol: 'admin', rol: 'admin',
permisos: ['administracion:usuarios:gestionar'], permisos: ['administracion:usuarios:gestionar'],
mustChangePassword: false,
}, },
}) })
@@ -31,6 +32,7 @@ describe('usePermission', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })
@@ -46,6 +48,7 @@ describe('usePermission', () => {
nombre: 'Reportes', nombre: 'Reportes',
rol: 'reportes', rol: 'reportes',
permisos: [], permisos: [],
mustChangePassword: false,
}, },
}) })
@@ -68,6 +71,7 @@ describe('usePermission', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })
@@ -85,6 +89,7 @@ describe('usePermission', () => {
nombre: 'Cajero', nombre: 'Cajero',
rol: 'cajero', rol: 'cajero',
permisos: ['ventas:contado:crear'], permisos: ['ventas:contado:crear'],
mustChangePassword: false,
}, },
}) })

View File

@@ -1,6 +1,25 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { useAuthStore } from '../../stores/authStore' 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', () => { describe('authStore', () => {
beforeEach(() => { beforeEach(() => {
// Reset store state before each test // Reset store state before each test
@@ -28,7 +47,7 @@ describe('authStore', () => {
describe('setAuth', () => { describe('setAuth', () => {
it('stores user and accessToken in state', () => { it('stores user and accessToken in state', () => {
const payload = { const payload = {
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature', accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature',
refreshToken: 'opaque-refresh-token', refreshToken: 'opaque-refresh-token',
expiresIn: 3600, expiresIn: 3600,
@@ -43,7 +62,7 @@ describe('authStore', () => {
it('persists auth data to localStorage under auth-storage key', () => { it('persists auth data to localStorage under auth-storage key', () => {
const payload = { const payload = {
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature', accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature',
refreshToken: 'opaque-refresh-token', refreshToken: 'opaque-refresh-token',
expiresIn: 3600, expiresIn: 3600,
@@ -61,7 +80,7 @@ describe('authStore', () => {
it('setAuth_persistsRefreshTokenAndExpiresAt', () => { it('setAuth_persistsRefreshTokenAndExpiresAt', () => {
const before = Date.now() const before = Date.now()
const payload = { const payload = {
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'access-token-abc', accessToken: 'access-token-abc',
refreshToken: 'opaque-refresh-xyz', refreshToken: 'opaque-refresh-xyz',
expiresIn: 3600, expiresIn: 3600,
@@ -92,6 +111,7 @@ describe('authStore', () => {
nombre: 'Admin', nombre: 'Admin',
rol: 'admin', rol: 'admin',
permisos: ['administracion:usuarios:gestionar', 'administracion:roles:gestionar'], permisos: ['administracion:usuarios:gestionar', 'administracion:roles:gestionar'],
mustChangePassword: false,
}, },
accessToken: 'access-token', accessToken: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
@@ -108,7 +128,7 @@ describe('authStore', () => {
it('F-04-02: setAuth con permisos vacíos → user.permisos es [] (no null)', () => { it('F-04-02: setAuth con permisos vacíos → user.permisos es [] (no null)', () => {
const payload = { const payload = {
user: { id: 2, username: 'cajero', nombre: 'Cajero', rol: 'cajero', permisos: [] }, user: cajeroUser,
accessToken: 'access-token', accessToken: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
expiresIn: 3600, expiresIn: 3600,
@@ -120,12 +140,83 @@ describe('authStore', () => {
expect(state.user?.permisos).toEqual([]) expect(state.user?.permisos).toEqual([])
expect(state.user?.permisos).not.toBeNull() 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', () => { describe('clearAuth', () => {
it('F-04-03: clearAuth → user = null (permisos se limpian con el user)', () => { it('F-04-03: clearAuth → user = null (permisos se limpian con el user)', () => {
useAuthStore.getState().setAuth({ useAuthStore.getState().setAuth({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: ['administracion:usuarios:gestionar'] }, user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: ['administracion:usuarios:gestionar'], mustChangePassword: false },
accessToken: 'access-token', accessToken: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
expiresIn: 3600, expiresIn: 3600,
@@ -139,7 +230,7 @@ describe('authStore', () => {
it('clearAuth_removesAllFields', () => { it('clearAuth_removesAllFields', () => {
useAuthStore.getState().setAuth({ useAuthStore.getState().setAuth({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'access-token', accessToken: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
expiresIn: 3600, expiresIn: 3600,
@@ -157,9 +248,8 @@ describe('authStore', () => {
describe('updateAccess', () => { describe('updateAccess', () => {
it('updateAccess_updatesOnlyTokens_preservesUser', () => { it('updateAccess_updatesOnlyTokens_preservesUser', () => {
const originalUser = { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }
useAuthStore.getState().setAuth({ useAuthStore.getState().setAuth({
user: originalUser, user: adminUser,
accessToken: 'old-access', accessToken: 'old-access',
refreshToken: 'old-refresh', refreshToken: 'old-refresh',
expiresIn: 3600, expiresIn: 3600,
@@ -173,7 +263,7 @@ describe('authStore', () => {
expect(state.refreshToken).toBe('new-refresh') expect(state.refreshToken).toBe('new-refresh')
expect(state.expiresAt).toBe(newExpiresAt) expect(state.expiresAt).toBe(newExpiresAt)
// user should be preserved // user should be preserved
expect(state.user).toEqual(originalUser) expect(state.user).toEqual(adminUser)
}) })
}) })
@@ -181,7 +271,7 @@ describe('authStore', () => {
it('logout_callsApi_thenClearsAuth', async () => { it('logout_callsApi_thenClearsAuth', async () => {
// Set up auth state with a token so logout() will try to call the API // Set up auth state with a token so logout() will try to call the API
useAuthStore.getState().setAuth({ useAuthStore.getState().setAuth({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'access-token', accessToken: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
expiresIn: 3600, expiresIn: 3600,
@@ -201,14 +291,13 @@ describe('authStore', () => {
it('logout_apiFails_stillClearsAuth', async () => { it('logout_apiFails_stillClearsAuth', async () => {
useAuthStore.getState().setAuth({ useAuthStore.getState().setAuth({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'access-token', accessToken: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
expiresIn: 3600, expiresIn: 3600,
}) })
// Should NOT throw even if the dynamic import fails // Should NOT throw even if the dynamic import fails
// (We test this by verifying clearAuth is always called)
let threw = false let threw = false
try { try {
await useAuthStore.getState().logout() await useAuthStore.getState().logout()
@@ -226,7 +315,7 @@ describe('authStore', () => {
describe('legacy logout compatibility (via clearAuth)', () => { describe('legacy logout compatibility (via clearAuth)', () => {
it('clearAuth clears user and accessToken from state', () => { it('clearAuth clears user and accessToken from state', () => {
useAuthStore.getState().setAuth({ useAuthStore.getState().setAuth({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'some-token', accessToken: 'some-token',
refreshToken: 'some-refresh', refreshToken: 'some-refresh',
expiresIn: 3600, expiresIn: 3600,
@@ -241,7 +330,7 @@ describe('authStore', () => {
it('clearAuth removes auth-storage from localStorage', () => { it('clearAuth removes auth-storage from localStorage', () => {
useAuthStore.getState().setAuth({ useAuthStore.getState().setAuth({
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }, user: adminUser,
accessToken: 'some-token', accessToken: 'some-token',
refreshToken: 'some-refresh', refreshToken: 'some-refresh',
expiresIn: 3600, expiresIn: 3600,