LoginForm.spec.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import React from 'react';
  2. import {
  3. render, screen, fireEvent, waitFor,
  4. } from '@testing-library/react';
  5. import {
  6. describe, it, expect, vi, beforeEach,
  7. } from 'vitest';
  8. import { apiv3Post } from '~/client/util/apiv3-client';
  9. import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
  10. import { LoginForm } from './LoginForm';
  11. vi.mock('next-i18next', () => ({
  12. useTranslation: () => ({
  13. t: (key: string) => key,
  14. }),
  15. }));
  16. vi.mock('next/router', () => ({
  17. useRouter: () => ({
  18. push: vi.fn(),
  19. }),
  20. }));
  21. vi.mock('~/client/util/t-with-opt', () => ({
  22. useTWithOpt: () => (key: string) => key,
  23. }));
  24. vi.mock('~/client/util/apiv3-client', () => ({
  25. apiv3Post: vi.fn(),
  26. }));
  27. vi.mock('./ExternalAuthButton', () => ({
  28. ExternalAuthButton: ({ authType }: { authType: string }) => (
  29. <button type="button" data-testid={`external-auth-${authType}`}>
  30. External Auth {authType}
  31. </button>
  32. ),
  33. }));
  34. vi.mock('../CompleteUserRegistration', () => ({
  35. CompleteUserRegistration: () => <div>Complete Registration</div>,
  36. }));
  37. const defaultProps = {
  38. isEmailAuthenticationEnabled: false,
  39. registrationMode: 'Open' as const,
  40. registrationWhitelist: [],
  41. isPasswordResetEnabled: true,
  42. isLocalStrategySetup: true,
  43. isLdapStrategySetup: false,
  44. isLdapSetupFailed: false,
  45. minPasswordLength: 8,
  46. isMailerSetup: true,
  47. };
  48. const mockApiv3Post = vi.mocked(apiv3Post);
  49. describe('LoginForm - Error Display', () => {
  50. beforeEach(() => {
  51. vi.clearAllMocks();
  52. });
  53. describe('when password login is enabled', () => {
  54. it('should display login form', () => {
  55. const props = {
  56. ...defaultProps,
  57. isLocalStrategySetup: true,
  58. };
  59. render(<LoginForm {...props} />);
  60. expect(screen.getByTestId('login-form')).toBeInTheDocument();
  61. });
  62. it('should display external account login errors', () => {
  63. const externalAccountLoginError = {
  64. message: 'jwks must be a JSON Web Key Set formatted object',
  65. name: 'ExternalAccountLoginError',
  66. };
  67. const props = {
  68. ...defaultProps,
  69. isLocalStrategySetup: true,
  70. externalAccountLoginError,
  71. };
  72. render(<LoginForm {...props} />);
  73. expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument();
  74. });
  75. });
  76. describe('when password login is disabled', () => {
  77. it('should still display external account login errors', () => {
  78. const externalAccountLoginError = {
  79. message: 'jwks must be a JSON Web Key Set formatted object',
  80. name: 'ExternalAccountLoginError',
  81. };
  82. const props = {
  83. ...defaultProps,
  84. isLocalStrategySetup: false,
  85. isLdapStrategySetup: false,
  86. enabledExternalAuthType: ['oidc'] satisfies IExternalAuthProviderType[],
  87. externalAccountLoginError,
  88. };
  89. render(<LoginForm {...props} />);
  90. expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument();
  91. });
  92. it('should not render local/LDAP form but should still show errors', () => {
  93. const externalAccountLoginError = {
  94. message: 'OIDC authentication failed',
  95. name: 'ExternalAccountLoginError',
  96. };
  97. const props = {
  98. ...defaultProps,
  99. isLocalStrategySetup: false,
  100. isLdapStrategySetup: false,
  101. enabledExternalAuthType: ['oidc'] satisfies IExternalAuthProviderType[],
  102. externalAccountLoginError,
  103. };
  104. render(<LoginForm {...props} />);
  105. expect(screen.queryByTestId('tiUsernameForLogin')).not.toBeInTheDocument();
  106. expect(screen.queryByTestId('tiPasswordForLogin')).not.toBeInTheDocument();
  107. expect(screen.getByText('OIDC authentication failed')).toBeInTheDocument();
  108. });
  109. });
  110. describe('error display priority and login error handling', () => {
  111. it('should show external errors when no login errors exist', () => {
  112. const externalAccountLoginError = {
  113. message: 'External error message',
  114. name: 'ExternalAccountLoginError',
  115. };
  116. const props = {
  117. ...defaultProps,
  118. isLocalStrategySetup: true,
  119. externalAccountLoginError,
  120. };
  121. render(<LoginForm {...props} />);
  122. expect(screen.getByText('External error message')).toBeInTheDocument();
  123. });
  124. it('should prioritize login errors over external account login errors after failed login', async() => {
  125. const externalAccountLoginError = {
  126. message: 'External error message',
  127. name: 'ExternalAccountLoginError',
  128. };
  129. // Mock API call to return error
  130. mockApiv3Post.mockRejectedValueOnce([
  131. {
  132. message: 'Invalid username or password',
  133. code: 'LOGIN_FAILED',
  134. args: {},
  135. },
  136. ]);
  137. const props = {
  138. ...defaultProps,
  139. isLocalStrategySetup: true,
  140. externalAccountLoginError,
  141. };
  142. render(<LoginForm {...props} />);
  143. // Initially, external error should be visible
  144. expect(screen.getByText('External error message')).toBeInTheDocument();
  145. // Fill in login form and submit
  146. const usernameInput = screen.getByTestId('tiUsernameForLogin');
  147. const passwordInput = screen.getByTestId('tiPasswordForLogin');
  148. const submitButton = screen.getByTestId('btnSubmitForLogin');
  149. fireEvent.change(usernameInput, { target: { value: 'testuser' } });
  150. fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
  151. fireEvent.click(submitButton);
  152. // Wait for login error to appear and external error to be replaced
  153. await waitFor(() => {
  154. expect(screen.getByText('Invalid username or password')).toBeInTheDocument();
  155. });
  156. // External error should no longer be visible when login error exists
  157. expect(screen.queryByText('External error message')).not.toBeInTheDocument();
  158. });
  159. it('should display dangerouslySetInnerHTML errors for PROVIDER_DUPLICATED_USERNAME_EXCEPTION', async() => {
  160. // Mock API call to return PROVIDER_DUPLICATED_USERNAME_EXCEPTION error
  161. mockApiv3Post.mockRejectedValueOnce([
  162. {
  163. message: 'This username is already taken by <a href="/login">another provider</a>',
  164. code: 'provider-duplicated-username-exception',
  165. args: {},
  166. },
  167. ]);
  168. const props = {
  169. ...defaultProps,
  170. isLocalStrategySetup: true,
  171. };
  172. render(<LoginForm {...props} />);
  173. // Fill in login form and submit
  174. const usernameInput = screen.getByTestId('tiUsernameForLogin');
  175. const passwordInput = screen.getByTestId('tiPasswordForLogin');
  176. const submitButton = screen.getByTestId('btnSubmitForLogin');
  177. fireEvent.change(usernameInput, { target: { value: 'testuser' } });
  178. fireEvent.change(passwordInput, { target: { value: 'password' } });
  179. fireEvent.click(submitButton);
  180. // Wait for the dangerouslySetInnerHTML error to appear
  181. await waitFor(() => {
  182. // Check that the error with HTML content is rendered
  183. expect(screen.getByText(/This username is already taken by/)).toBeInTheDocument();
  184. });
  185. });
  186. it('should handle multiple login errors correctly', async() => {
  187. // Mock API call to return multiple errors
  188. mockApiv3Post.mockRejectedValueOnce([
  189. {
  190. message: 'Username is required',
  191. code: 'VALIDATION_ERROR',
  192. args: {},
  193. },
  194. {
  195. message: 'Password is too short',
  196. code: 'VALIDATION_ERROR',
  197. args: {},
  198. },
  199. ]);
  200. const props = {
  201. ...defaultProps,
  202. isLocalStrategySetup: true,
  203. };
  204. render(<LoginForm {...props} />);
  205. // Submit form without filling inputs
  206. const submitButton = screen.getByTestId('btnSubmitForLogin');
  207. fireEvent.click(submitButton);
  208. // Wait for multiple errors to appear
  209. await waitFor(() => {
  210. expect(screen.getByText('Username is required')).toBeInTheDocument();
  211. expect(screen.getByText('Password is too short')).toBeInTheDocument();
  212. });
  213. });
  214. });
  215. describe('error display when both login methods are disabled', () => {
  216. it('should still display external errors when no login methods are available', () => {
  217. const externalAccountLoginError = {
  218. message: 'Authentication service unavailable',
  219. name: 'ExternalAccountLoginError',
  220. };
  221. const props = {
  222. ...defaultProps,
  223. isLocalStrategySetup: false,
  224. isLdapStrategySetup: false,
  225. enabledExternalAuthType: undefined,
  226. externalAccountLoginError,
  227. };
  228. render(<LoginForm {...props} />);
  229. expect(screen.getByText('Authentication service unavailable')).toBeInTheDocument();
  230. });
  231. });
  232. });