frontend/tests/pages/login.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime'
import { ref } from 'vue'
import LoginPage from '../../app/pages/login.vue'
const { mockUseAuth } = vi.hoisted(() => ({
mockUseAuth: vi.fn()
}))
mockNuxtImport('useAuth', () => mockUseAuth)
describe('Login Page', () => {
const mockLoginWithCredentials = vi.fn()
const mockLoginWithOIDC = vi.fn()
const mockFetchSession = vi.fn()
const mockNavigateTo = vi.fn()
let mockIsAuthenticated = ref(false)
let mockIsOIDCEnabled = ref(false)
let mockIsOIDCAutoRedirectEnabled = ref(false)
let mockLoading = ref(false)
beforeEach(() => {
vi.clearAllMocks()
mockIsAuthenticated = ref(false)
mockIsOIDCEnabled = ref(false)
mockIsOIDCAutoRedirectEnabled = ref(false)
mockLoading = ref(false)
mockLoginWithCredentials.mockReset()
mockLoginWithOIDC.mockReset()
mockFetchSession.mockReset()
mockUseAuth.mockReturnValue({
isAuthenticated: mockIsAuthenticated,
isOIDCEnabled: mockIsOIDCEnabled,
isOIDCAutoRedirectEnabled: mockIsOIDCAutoRedirectEnabled,
loginWithCredentials: mockLoginWithCredentials,
loginWithOIDC: mockLoginWithOIDC,
fetchSession: mockFetchSession,
loading: mockLoading
})
})
describe('initial state', () => {
it('shows loading spinner while checking session', () => {
mockLoading.value = true
expect(mockLoading.value).toBe(true)
})
it('redirects to home if already authenticated', async () => {
mockIsAuthenticated.value = true
if (mockIsAuthenticated.value) {
mockNavigateTo('/')
}
expect(mockNavigateTo).toHaveBeenCalledWith('/')
})
it('shows login form when not authenticated', () => {
mockLoading.value = false
mockIsAuthenticated.value = false
expect(mockLoading.value).toBe(false)
expect(mockIsAuthenticated.value).toBe(false)
})
})
describe('credential login', () => {
it('calls loginWithCredentials with email and password', async () => {
const credentials = { email: '[email protected]', password: 'password123' }
mockLoginWithCredentials.mockResolvedValueOnce({ success: true })
await mockLoginWithCredentials(credentials)
expect(mockLoginWithCredentials).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123'
})
})
it('shows error message on failed login', async () => {
mockLoginWithCredentials.mockResolvedValueOnce({
success: false,
error: 'Invalid credentials'
})
const result = await mockLoginWithCredentials({
email: '[email protected]',
password: 'wrong'
})
expect(result.success).toBe(false)
expect(result.error).toBe('Invalid credentials')
})
it('navigates to home on successful login', async () => {
mockLoginWithCredentials.mockResolvedValueOnce({ success: true })
const result = await mockLoginWithCredentials({
email: '[email protected]',
password: 'password123'
})
if (result.success) {
mockIsAuthenticated.value = true
mockNavigateTo('/')
}
expect(mockNavigateTo).toHaveBeenCalledWith('/')
})
it('disables submit button when email is empty', () => {
const email = ''
const password = 'password123'
const isLoading = false
const isDisabled = isLoading || !email || !password
expect(isDisabled).toBe(true)
})
it('disables submit button when password is empty', () => {
const email = '[email protected]'
const password = ''
const isLoading = false
const isDisabled = isLoading || !email || !password
expect(isDisabled).toBe(true)
})
it('disables submit button while loading', () => {
const email = '[email protected]'
const password = 'password123'
const isLoading = true
const isDisabled = isLoading || !email || !password
expect(isDisabled).toBe(true)
})
it('enables submit button with valid credentials', () => {
const email = '[email protected]'
const password = 'password123'
const isLoading = false
const isDisabled = isLoading || !email || !password
expect(isDisabled).toBe(false)
})
})
describe('OIDC login', () => {
it('shows SSO button when OIDC is enabled', () => {
mockIsOIDCEnabled.value = true
expect(mockIsOIDCEnabled.value).toBe(true)
})
it('hides credential form when OIDC is enabled', () => {
mockIsOIDCEnabled.value = true
const showCredentialForm = !mockIsOIDCEnabled.value
expect(showCredentialForm).toBe(false)
})
it('shows credential form when OIDC is disabled', () => {
mockIsOIDCEnabled.value = false
const showCredentialForm = !mockIsOIDCEnabled.value
expect(showCredentialForm).toBe(true)
})
it('calls loginWithOIDC when SSO button is clicked', () => {
mockIsOIDCEnabled.value = true
mockLoginWithOIDC()
expect(mockLoginWithOIDC).toHaveBeenCalled()
})
it('automatically starts OIDC login when forwarding is enabled', async () => {
mockFetchSession.mockResolvedValueOnce(undefined)
mockIsOIDCEnabled.value = true
mockIsOIDCAutoRedirectEnabled.value = true
await mountSuspended(LoginPage, { route: '/login' })
await flushPromises()
expect(mockFetchSession).toHaveBeenCalledOnce()
expect(mockLoginWithOIDC).toHaveBeenCalledOnce()
})
it('does not automatically start OIDC login after logout', async () => {
mockFetchSession.mockResolvedValueOnce(undefined)
mockIsOIDCEnabled.value = true
mockIsOIDCAutoRedirectEnabled.value = true
const wrapper = await mountSuspended(LoginPage, { route: '/login?logout=true' })
await flushPromises()
expect(mockLoginWithOIDC).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('Sign in with SSO')
})
it('keeps the ordinary OIDC login screen when forwarding is disabled', async () => {
mockFetchSession.mockResolvedValueOnce(undefined)
mockIsOIDCEnabled.value = true
const wrapper = await mountSuspended(LoginPage, { route: '/login' })
await flushPromises()
expect(mockLoginWithOIDC).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('Sign in with SSO')
})
})
describe('error handling', () => {
it('clears error before new login attempt', async () => {
let error = 'Previous error'
expect(error).toBe('Previous error')
error = ''
mockLoginWithCredentials.mockResolvedValueOnce({ success: true })
await mockLoginWithCredentials({ email: '[email protected]', password: 'pass' })
expect(error).toBe('')
})
it('displays generic error when no error message provided', async () => {
mockLoginWithCredentials.mockResolvedValueOnce({
success: false,
error: undefined
})
const result = await mockLoginWithCredentials({
email: '[email protected]',
password: 'wrong'
})
const errorMessage = result.error || 'Login failed'
expect(errorMessage).toBe('Login failed')
})
it('handles network errors gracefully', async () => {
mockLoginWithCredentials.mockRejectedValueOnce(new Error('Network error'))
let error = ''
try {
await mockLoginWithCredentials({ email: '[email protected]', password: 'pass' })
} catch (e: unknown) {
const err = e as { message?: string }
error = err.message || 'Login failed'
}
expect(error).toBe('Network error')
})
})
describe('authentication state watching', () => {
it('navigates to home when isAuthenticated becomes true', () => {
mockIsAuthenticated.value = false
const watchCallback = (authenticated: boolean) => {
if (authenticated) {
mockNavigateTo('/')
}
}
mockIsAuthenticated.value = true
watchCallback(mockIsAuthenticated.value)
expect(mockNavigateTo).toHaveBeenCalledWith('/')
})
it('does not navigate when authentication fails', () => {
mockIsAuthenticated.value = false
const watchCallback = (authenticated: boolean) => {
if (authenticated) {
mockNavigateTo('/')
}
}
watchCallback(mockIsAuthenticated.value)
expect(mockNavigateTo).not.toHaveBeenCalled()
})
})
describe('session checking on mount', () => {
it('fetches session on component mount', async () => {
await mockFetchSession()
expect(mockFetchSession).toHaveBeenCalled()
})
it('redirects if session shows authenticated', async () => {
mockFetchSession.mockImplementation(async () => {
mockIsAuthenticated.value = true
})
await mockFetchSession()
if (mockIsAuthenticated.value) {
mockNavigateTo('/')
}
expect(mockNavigateTo).toHaveBeenCalledWith('/')
})
})
})