UDT-008: Gestión completa de usuarios #11
28
src/web/src/components/routing/MustChangePasswordGate.tsx
Normal file
28
src/web/src/components/routing/MustChangePasswordGate.tsx
Normal 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}</>
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface LoginResponseDto {
|
||||
nombre: string
|
||||
rol: string
|
||||
permisos: string[]
|
||||
mustChangePassword: boolean // UDT-008
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export function useLogin() {
|
||||
nombre: data.usuario.nombre,
|
||||
rol: data.usuario.rol,
|
||||
permisos: data.usuario.permisos ?? [],
|
||||
mustChangePassword: data.usuario.mustChangePassword ?? false, // UDT-008
|
||||
},
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface AuthUser {
|
||||
nombre: string
|
||||
rol: string
|
||||
permisos: string[]
|
||||
mustChangePassword: boolean // UDT-008
|
||||
}
|
||||
|
||||
interface SetAuthPayload {
|
||||
@@ -22,6 +23,7 @@ interface AuthState {
|
||||
refreshToken: string | null
|
||||
expiresAt: number | null // ms epoch UTC
|
||||
setAuth: (payload: SetAuthPayload) => void
|
||||
updateUser: (patch: Partial<AuthUser>) => void // UDT-008
|
||||
updateAccess: (accessToken: string, refreshToken: string, expiresAt: number) => void
|
||||
clearAuth: () => void
|
||||
logout: () => Promise<void>
|
||||
@@ -43,6 +45,11 @@ export const useAuthStore = create<AuthState>()(
|
||||
expiresAt: Date.now() + payload.expiresIn * 1000,
|
||||
}),
|
||||
|
||||
updateUser: (patch) =>
|
||||
set((s) => ({
|
||||
user: s.user ? { ...s.user, ...patch } : null,
|
||||
})),
|
||||
|
||||
updateAccess: (accessToken, refreshToken, expiresAt) =>
|
||||
set({ accessToken, refreshToken, expiresAt }),
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ afterEach(() => {
|
||||
|
||||
function setAuth(accessToken: string, refreshToken: string) {
|
||||
useAuthStore.setState({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [], mustChangePassword: false },
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresAt: Date.now() + 3600 * 1000,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -16,6 +16,7 @@ describe('CanPerform', () => {
|
||||
nombre: 'Admin',
|
||||
rol: 'admin',
|
||||
permisos: ['administracion:usuarios:gestionar'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -36,6 +37,7 @@ describe('CanPerform', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -68,6 +70,7 @@ describe('CanPerform', () => {
|
||||
nombre: 'Reportes',
|
||||
rol: 'reportes',
|
||||
permisos: [],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -89,6 +92,7 @@ describe('CanPerform', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const mockLoginResponse = {
|
||||
accessToken: 'eyJhbGciOiJSUzI1NiJ9.payload.sig',
|
||||
refreshToken: 'refresh-token-abc',
|
||||
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(
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('ProtectedRoute', () => {
|
||||
|
||||
it('F-03-02: user autenticado sin restricciones → renderiza children', () => {
|
||||
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(
|
||||
@@ -101,6 +101,7 @@ describe('ProtectedRoute', () => {
|
||||
nombre: 'Admin',
|
||||
rol: 'admin',
|
||||
permisos: ['administracion:usuarios:gestionar'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -126,7 +127,7 @@ describe('ProtectedRoute', () => {
|
||||
|
||||
it('F-03-04: requiredRoles no coincide → redirect a /', () => {
|
||||
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(
|
||||
@@ -158,6 +159,7 @@ describe('ProtectedRoute', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -191,6 +193,7 @@ describe('ProtectedRoute', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -223,6 +226,7 @@ describe('ProtectedRoute', () => {
|
||||
nombre: 'Admin',
|
||||
rol: 'admin',
|
||||
permisos: ['administracion:usuarios:gestionar'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -254,6 +258,7 @@ describe('ProtectedRoute', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const mockLoginResponseWithPermisos = {
|
||||
nombre: 'Admin Sistema',
|
||||
rol: 'admin',
|
||||
permisos: ['administracion:usuarios:gestionar', 'administracion:roles:gestionar'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -32,6 +33,21 @@ const mockLoginResponseEmptyPermisos = {
|
||||
nombre: 'Cajero Test',
|
||||
rol: 'cajero',
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ describe('usePermission', () => {
|
||||
nombre: 'Admin',
|
||||
rol: 'admin',
|
||||
permisos: ['administracion:usuarios:gestionar'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -31,6 +32,7 @@ describe('usePermission', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -46,6 +48,7 @@ describe('usePermission', () => {
|
||||
nombre: 'Reportes',
|
||||
rol: 'reportes',
|
||||
permisos: [],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -68,6 +71,7 @@ describe('usePermission', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -85,6 +89,7 @@ describe('usePermission', () => {
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: ['ventas:contado:crear'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
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
|
||||
@@ -28,7 +47,7 @@ describe('authStore', () => {
|
||||
describe('setAuth', () => {
|
||||
it('stores user and accessToken in state', () => {
|
||||
const payload = {
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature',
|
||||
refreshToken: 'opaque-refresh-token',
|
||||
expiresIn: 3600,
|
||||
@@ -43,7 +62,7 @@ describe('authStore', () => {
|
||||
|
||||
it('persists auth data to localStorage under auth-storage key', () => {
|
||||
const payload = {
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'eyJhbGciOiJSUzI1NiJ9.test.signature',
|
||||
refreshToken: 'opaque-refresh-token',
|
||||
expiresIn: 3600,
|
||||
@@ -61,7 +80,7 @@ describe('authStore', () => {
|
||||
it('setAuth_persistsRefreshTokenAndExpiresAt', () => {
|
||||
const before = Date.now()
|
||||
const payload = {
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'access-token-abc',
|
||||
refreshToken: 'opaque-refresh-xyz',
|
||||
expiresIn: 3600,
|
||||
@@ -92,6 +111,7 @@ describe('authStore', () => {
|
||||
nombre: 'Admin',
|
||||
rol: 'admin',
|
||||
permisos: ['administracion:usuarios:gestionar', 'administracion:roles:gestionar'],
|
||||
mustChangePassword: false,
|
||||
},
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
@@ -108,7 +128,7 @@ describe('authStore', () => {
|
||||
|
||||
it('F-04-02: setAuth con permisos vacíos → user.permisos es [] (no null)', () => {
|
||||
const payload = {
|
||||
user: { id: 2, username: 'cajero', nombre: 'Cajero', rol: 'cajero', permisos: [] },
|
||||
user: cajeroUser,
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresIn: 3600,
|
||||
@@ -120,12 +140,83 @@ describe('authStore', () => {
|
||||
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'] },
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: ['administracion:usuarios:gestionar'], mustChangePassword: false },
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresIn: 3600,
|
||||
@@ -139,7 +230,7 @@ describe('authStore', () => {
|
||||
|
||||
it('clearAuth_removesAllFields', () => {
|
||||
useAuthStore.getState().setAuth({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresIn: 3600,
|
||||
@@ -157,9 +248,8 @@ describe('authStore', () => {
|
||||
|
||||
describe('updateAccess', () => {
|
||||
it('updateAccess_updatesOnlyTokens_preservesUser', () => {
|
||||
const originalUser = { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] }
|
||||
useAuthStore.getState().setAuth({
|
||||
user: originalUser,
|
||||
user: adminUser,
|
||||
accessToken: 'old-access',
|
||||
refreshToken: 'old-refresh',
|
||||
expiresIn: 3600,
|
||||
@@ -173,7 +263,7 @@ describe('authStore', () => {
|
||||
expect(state.refreshToken).toBe('new-refresh')
|
||||
expect(state.expiresAt).toBe(newExpiresAt)
|
||||
// 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 () => {
|
||||
// Set up auth state with a token so logout() will try to call the API
|
||||
useAuthStore.getState().setAuth({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresIn: 3600,
|
||||
@@ -201,14 +291,13 @@ describe('authStore', () => {
|
||||
|
||||
it('logout_apiFails_stillClearsAuth', async () => {
|
||||
useAuthStore.getState().setAuth({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresIn: 3600,
|
||||
})
|
||||
|
||||
// Should NOT throw even if the dynamic import fails
|
||||
// (We test this by verifying clearAuth is always called)
|
||||
let threw = false
|
||||
try {
|
||||
await useAuthStore.getState().logout()
|
||||
@@ -226,7 +315,7 @@ describe('authStore', () => {
|
||||
describe('legacy logout compatibility (via clearAuth)', () => {
|
||||
it('clearAuth clears user and accessToken from state', () => {
|
||||
useAuthStore.getState().setAuth({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'some-token',
|
||||
refreshToken: 'some-refresh',
|
||||
expiresIn: 3600,
|
||||
@@ -241,7 +330,7 @@ describe('authStore', () => {
|
||||
|
||||
it('clearAuth removes auth-storage from localStorage', () => {
|
||||
useAuthStore.getState().setAuth({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin', permisos: [] },
|
||||
user: adminUser,
|
||||
accessToken: 'some-token',
|
||||
refreshToken: 'some-refresh',
|
||||
expiresIn: 3600,
|
||||
|
||||
Reference in New Issue
Block a user