import React from 'react'; import { render, screen, fireEvent, waitFor, } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, } from 'vitest'; import { apiv3Post } from '~/client/util/apiv3-client'; import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider'; import { LoginForm } from './LoginForm'; vi.mock('next-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), })); vi.mock('next/router', () => ({ useRouter: () => ({ push: vi.fn(), }), })); vi.mock('~/client/util/t-with-opt', () => ({ useTWithOpt: () => (key: string) => key, })); vi.mock('~/client/util/apiv3-client', () => ({ apiv3Post: vi.fn(), })); vi.mock('./ExternalAuthButton', () => ({ ExternalAuthButton: ({ authType }: { authType: string }) => ( ), })); vi.mock('../CompleteUserRegistration', () => ({ CompleteUserRegistration: () =>
Complete Registration
, })); const defaultProps = { isEmailAuthenticationEnabled: false, registrationMode: 'Open' as const, registrationWhitelist: [], isPasswordResetEnabled: true, isLocalStrategySetup: true, isLdapStrategySetup: false, isLdapSetupFailed: false, minPasswordLength: 8, isMailerSetup: true, }; const mockApiv3Post = vi.mocked(apiv3Post); describe('LoginForm - Error Display', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('when password login is enabled', () => { it('should display login form', () => { const props = { ...defaultProps, isLocalStrategySetup: true, }; render(); expect(screen.getByTestId('login-form')).toBeInTheDocument(); }); it('should display external account login errors', () => { const externalAccountLoginError = { message: 'jwks must be a JSON Web Key Set formatted object', name: 'ExternalAccountLoginError', }; const props = { ...defaultProps, isLocalStrategySetup: true, externalAccountLoginError, }; render(); expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument(); }); }); describe('when password login is disabled', () => { it('should still display external account login errors', () => { const externalAccountLoginError = { message: 'jwks must be a JSON Web Key Set formatted object', name: 'ExternalAccountLoginError', }; const props = { ...defaultProps, isLocalStrategySetup: false, isLdapStrategySetup: false, enabledExternalAuthType: ['oidc'] satisfies IExternalAuthProviderType[], externalAccountLoginError, }; render(); expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument(); }); it('should not render local/LDAP form but should still show errors', () => { const externalAccountLoginError = { message: 'OIDC authentication failed', name: 'ExternalAccountLoginError', }; const props = { ...defaultProps, isLocalStrategySetup: false, isLdapStrategySetup: false, enabledExternalAuthType: ['oidc'] satisfies IExternalAuthProviderType[], externalAccountLoginError, }; render(); expect(screen.queryByTestId('tiUsernameForLogin')).not.toBeInTheDocument(); expect(screen.queryByTestId('tiPasswordForLogin')).not.toBeInTheDocument(); expect(screen.getByText('OIDC authentication failed')).toBeInTheDocument(); }); }); describe('error display priority and login error handling', () => { it('should show external errors when no login errors exist', () => { const externalAccountLoginError = { message: 'External error message', name: 'ExternalAccountLoginError', }; const props = { ...defaultProps, isLocalStrategySetup: true, externalAccountLoginError, }; render(); expect(screen.getByText('External error message')).toBeInTheDocument(); }); it('should prioritize login errors over external account login errors after failed login', async() => { const externalAccountLoginError = { message: 'External error message', name: 'ExternalAccountLoginError', }; // Mock API call to return error mockApiv3Post.mockRejectedValueOnce([ { message: 'Invalid username or password', code: 'LOGIN_FAILED', args: {}, }, ]); const props = { ...defaultProps, isLocalStrategySetup: true, externalAccountLoginError, }; render(); // Initially, external error should be visible expect(screen.getByText('External error message')).toBeInTheDocument(); // Fill in login form and submit const usernameInput = screen.getByTestId('tiUsernameForLogin'); const passwordInput = screen.getByTestId('tiPasswordForLogin'); const submitButton = screen.getByTestId('btnSubmitForLogin'); fireEvent.change(usernameInput, { target: { value: 'testuser' } }); fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } }); fireEvent.click(submitButton); // Wait for login error to appear and external error to be replaced await waitFor(() => { expect(screen.getByText('Invalid username or password')).toBeInTheDocument(); }); // External error should no longer be visible when login error exists expect(screen.queryByText('External error message')).not.toBeInTheDocument(); }); it('should display dangerouslySetInnerHTML errors for PROVIDER_DUPLICATED_USERNAME_EXCEPTION', async() => { // Mock API call to return PROVIDER_DUPLICATED_USERNAME_EXCEPTION error mockApiv3Post.mockRejectedValueOnce([ { message: 'This username is already taken by another provider', code: 'provider-duplicated-username-exception', args: {}, }, ]); const props = { ...defaultProps, isLocalStrategySetup: true, }; render(); // Fill in login form and submit const usernameInput = screen.getByTestId('tiUsernameForLogin'); const passwordInput = screen.getByTestId('tiPasswordForLogin'); const submitButton = screen.getByTestId('btnSubmitForLogin'); fireEvent.change(usernameInput, { target: { value: 'testuser' } }); fireEvent.change(passwordInput, { target: { value: 'password' } }); fireEvent.click(submitButton); // Wait for the dangerouslySetInnerHTML error to appear await waitFor(() => { // Check that the error with HTML content is rendered expect(screen.getByText(/This username is already taken by/)).toBeInTheDocument(); }); }); it('should handle multiple login errors correctly', async() => { // Mock API call to return multiple errors mockApiv3Post.mockRejectedValueOnce([ { message: 'Username is required', code: 'VALIDATION_ERROR', args: {}, }, { message: 'Password is too short', code: 'VALIDATION_ERROR', args: {}, }, ]); const props = { ...defaultProps, isLocalStrategySetup: true, }; render(); // Submit form without filling inputs const submitButton = screen.getByTestId('btnSubmitForLogin'); fireEvent.click(submitButton); // Wait for multiple errors to appear await waitFor(() => { expect(screen.getByText('Username is required')).toBeInTheDocument(); expect(screen.getByText('Password is too short')).toBeInTheDocument(); }); }); }); describe('error display when both login methods are disabled', () => { it('should still display external errors when no login methods are available', () => { const externalAccountLoginError = { message: 'Authentication service unavailable', name: 'ExternalAccountLoginError', }; const props = { ...defaultProps, isLocalStrategySetup: false, isLdapStrategySetup: false, enabledExternalAuthType: undefined, externalAccountLoginError, }; render(); expect(screen.getByText('Authentication service unavailable')).toBeInTheDocument(); }); }); });