test(udt-001): frontend tests (authStore, authApi, LoginPage - 11 tests)
This commit is contained in:
117
src/web/src/tests/features/auth/LoginPage.test.tsx
Normal file
117
src/web/src/tests/features/auth/LoginPage.test.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import { LoginPage } from '../../../features/auth/pages/LoginPage'
|
||||||
|
import { useAuthStore } from '../../../stores/authStore'
|
||||||
|
|
||||||
|
// Must be at top level for Vitest hoisting
|
||||||
|
const mockNavigate = vi.fn()
|
||||||
|
vi.mock('react-router-dom', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('react-router-dom')>()
|
||||||
|
return { ...actual, useNavigate: () => mockNavigate }
|
||||||
|
})
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:5000'
|
||||||
|
|
||||||
|
const mockLoginResponse = {
|
||||||
|
accessToken: 'eyJhbGciOiJSUzI1NiJ9.payload.sig',
|
||||||
|
refreshToken: 'refresh-token-abc',
|
||||||
|
expiresIn: 3600,
|
||||||
|
usuario: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = setupServer(
|
||||||
|
http.post(`${API_URL}/api/v1/auth/login`, async ({ request }) => {
|
||||||
|
const body = await request.json() as { username: string; password: string }
|
||||||
|
if (body.username === 'admin' && body.password === '@Diego550@') {
|
||||||
|
return HttpResponse.json(mockLoginResponse, { status: 200 })
|
||||||
|
}
|
||||||
|
return HttpResponse.json({ error: 'Credenciales inválidas' }, { status: 401 })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||||
|
afterEach(() => {
|
||||||
|
server.resetHandlers()
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
localStorage.clear()
|
||||||
|
mockNavigate.mockClear()
|
||||||
|
})
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
function renderLoginPage() {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||||
|
})
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<LoginPage />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('LoginPage', () => {
|
||||||
|
it('renders username and password inputs and submit button', () => {
|
||||||
|
renderLoginPage()
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/usuario/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText(/contraseña/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /ingresar/i })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error message on 401 invalid credentials', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderLoginPage()
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText(/usuario/i), 'admin')
|
||||||
|
await user.type(screen.getByLabelText(/contraseña/i), 'wrongpassword')
|
||||||
|
await user.click(screen.getByRole('button', { name: /ingresar/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('alert')).toHaveTextContent(/credenciales/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables submit button while loading', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderLoginPage()
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.post(`${API_URL}/api/v1/auth/login`, async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
|
return HttpResponse.json(mockLoginResponse)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText(/usuario/i), 'admin')
|
||||||
|
await user.type(screen.getByLabelText(/contraseña/i), '@Diego550@')
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: /ingresar/i })
|
||||||
|
await user.click(button)
|
||||||
|
|
||||||
|
// Button should be disabled during the pending request
|
||||||
|
expect(button).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saves auth to store on successful login', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderLoginPage()
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText(/usuario/i), 'admin')
|
||||||
|
await user.type(screen.getByLabelText(/contraseña/i), '@Diego550@')
|
||||||
|
await user.click(screen.getByRole('button', { name: /ingresar/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const state = useAuthStore.getState()
|
||||||
|
expect(state.accessToken).toBe(mockLoginResponse.accessToken)
|
||||||
|
expect(state.user?.username).toBe('admin')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
47
src/web/src/tests/features/auth/authApi.test.ts
Normal file
47
src/web/src/tests/features/auth/authApi.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
import { login } from '../../../features/auth/api/authApi'
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:5000'
|
||||||
|
|
||||||
|
const mockLoginResponse = {
|
||||||
|
accessToken: 'eyJhbGciOiJSUzI1NiJ9.payload.signature',
|
||||||
|
refreshToken: 'opaque-refresh-token-abc123',
|
||||||
|
expiresIn: 3600,
|
||||||
|
usuario: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
nombre: 'Admin',
|
||||||
|
rol: 'admin',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = setupServer(
|
||||||
|
http.post(`${API_URL}/api/v1/auth/login`, async ({ request }) => {
|
||||||
|
const body = await request.json() as { username: string; password: string }
|
||||||
|
if (body.username === 'admin' && body.password === '@Diego550@') {
|
||||||
|
return HttpResponse.json(mockLoginResponse, { status: 200 })
|
||||||
|
}
|
||||||
|
return HttpResponse.json({ error: 'Credenciales inválidas' }, { status: 401 })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||||
|
afterEach(() => server.resetHandlers())
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
describe('login()', () => {
|
||||||
|
it('returns auth data on valid credentials', async () => {
|
||||||
|
const result = await login('admin', '@Diego550@')
|
||||||
|
|
||||||
|
expect(result.accessToken).toBe(mockLoginResponse.accessToken)
|
||||||
|
expect(result.refreshToken).toBe(mockLoginResponse.refreshToken)
|
||||||
|
expect(result.expiresIn).toBe(3600)
|
||||||
|
expect(result.usuario.username).toBe('admin')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on invalid credentials (401)', async () => {
|
||||||
|
await expect(login('admin', 'wrongpassword')).rejects.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
1
src/web/src/tests/setup.ts
Normal file
1
src/web/src/tests/setup.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom'
|
||||||
89
src/web/src/tests/stores/authStore.test.ts
Normal file
89
src/web/src/tests/stores/authStore.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { useAuthStore } from '../../stores/authStore'
|
||||||
|
|
||||||
|
describe('authStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset store state before each test
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
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: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||||
|
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: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('logout', () => {
|
||||||
|
it('clears user and accessToken from state', () => {
|
||||||
|
// Setup: set auth first
|
||||||
|
useAuthStore.getState().setAuth({
|
||||||
|
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||||
|
accessToken: 'some-token',
|
||||||
|
refreshToken: 'some-refresh',
|
||||||
|
expiresIn: 3600,
|
||||||
|
})
|
||||||
|
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
|
||||||
|
const state = useAuthStore.getState()
|
||||||
|
expect(state.user).toBeNull()
|
||||||
|
expect(state.accessToken).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes auth-storage from localStorage on logout', () => {
|
||||||
|
useAuthStore.getState().setAuth({
|
||||||
|
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||||
|
accessToken: 'some-token',
|
||||||
|
refreshToken: 'some-refresh',
|
||||||
|
expiresIn: 3600,
|
||||||
|
})
|
||||||
|
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
|
||||||
|
const stored = localStorage.getItem('auth-storage')
|
||||||
|
// After logout the persisted state should have null user/token
|
||||||
|
if (stored !== null) {
|
||||||
|
const parsed = JSON.parse(stored)
|
||||||
|
expect(parsed.state.user).toBeNull()
|
||||||
|
expect(parsed.state.accessToken).toBeNull()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user