test(udt-001): frontend tests (authStore, authApi, LoginPage - 11 tests)

This commit is contained in:
2026-04-13 21:36:40 -03:00
parent a692576bc3
commit f4f063f5f0
4 changed files with 254 additions and 0 deletions

View 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')
})
})
})

View 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()
})
})

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom'

View 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()
}
})
})
})