@digitaldefiance/express-suite-react-components 2.9.1 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +11 -7
  3. package/src/auth/Private.tsx +17 -0
  4. package/src/auth/PrivateRoute.tsx +28 -0
  5. package/src/auth/UnAuth.tsx +16 -0
  6. package/src/auth/UnAuthRoute.tsx +30 -0
  7. package/src/auth/{index.d.ts → index.ts} +1 -2
  8. package/src/components/ApiAccess.tsx +134 -0
  9. package/src/components/BackupCodeLoginForm.tsx +314 -0
  10. package/src/components/BackupCodesForm.tsx +198 -0
  11. package/src/components/ChangePasswordForm.tsx +182 -0
  12. package/src/components/ConfirmationDialog.tsx +48 -0
  13. package/src/components/CurrencyCodeSelector.tsx +60 -0
  14. package/src/components/CurrencyInput.tsx +80 -0
  15. package/src/components/DashboardPage.tsx +24 -0
  16. package/src/components/DropdownMenu.tsx +92 -0
  17. package/src/components/ExpirationSecondsSelector.tsx +65 -0
  18. package/src/components/Flag.tsx +53 -0
  19. package/src/components/ForgotPasswordForm.tsx +120 -0
  20. package/src/components/LoginForm.tsx +307 -0
  21. package/src/components/LogoutPage.tsx +21 -0
  22. package/src/components/RegisterForm.tsx +354 -0
  23. package/src/components/ResetPasswordForm.tsx +164 -0
  24. package/src/components/SideMenu.tsx +46 -0
  25. package/src/components/SideMenuListItem.tsx +74 -0
  26. package/src/components/TopMenu.tsx +134 -0
  27. package/src/components/TranslatedTitle.tsx +22 -0
  28. package/src/components/UserLanguageSelector.tsx +45 -0
  29. package/src/components/UserMenu.tsx +15 -0
  30. package/src/components/UserSettingsForm.tsx +328 -0
  31. package/src/components/VerifyEmailPage.tsx +133 -0
  32. package/src/components/{index.d.ts → index.ts} +1 -1
  33. package/src/contexts/AuthProvider.spec.tsx +1060 -0
  34. package/src/contexts/AuthProvider.tsx +741 -0
  35. package/src/contexts/I18nProvider.tsx +85 -0
  36. package/src/contexts/MenuContext.tsx +310 -0
  37. package/src/contexts/SuiteConfigProvider.tsx +93 -0
  38. package/src/contexts/ThemeProvider.tsx +67 -0
  39. package/src/contexts/{index.d.ts → index.ts} +0 -1
  40. package/src/hooks/{index.d.ts → index.ts} +0 -1
  41. package/src/hooks/useBackupCodes.ts +85 -0
  42. package/src/hooks/useEmailVerification.ts +39 -0
  43. package/src/hooks/useExpiringValue.ts +78 -0
  44. package/src/hooks/useLocalStorage.ts +18 -0
  45. package/src/hooks/useUserSettings.ts +216 -0
  46. package/src/{index.d.ts → index.ts} +1 -1
  47. package/src/interfaces/IAppConfig.ts +5 -0
  48. package/src/interfaces/IMenuConfig.ts +11 -0
  49. package/src/interfaces/IMenuOption.ts +55 -0
  50. package/src/interfaces/index.ts +3 -0
  51. package/src/services/__mocks__/authService.ts +14 -0
  52. package/src/services/api.ts +13 -0
  53. package/src/services/authService.ts +422 -0
  54. package/src/services/authenticatedApi.ts +17 -0
  55. package/src/services/index.ts +3 -0
  56. package/src/types/MenuType.ts +15 -0
  57. package/src/types/expirationSeconds.ts +18 -0
  58. package/src/types/index.ts +1 -0
  59. package/src/types/translation.ts +20 -0
  60. package/src/wrappers/BackupCodeLoginWrapper.tsx +35 -0
  61. package/src/wrappers/BackupCodesWrapper.tsx +28 -0
  62. package/src/wrappers/ChangePasswordFormWrapper.tsx +31 -0
  63. package/src/wrappers/LoginFormWrapper.tsx +59 -0
  64. package/src/wrappers/LogoutPageWrapper.tsx +30 -0
  65. package/src/wrappers/RegisterFormWrapper.tsx +48 -0
  66. package/src/wrappers/UserSettingsFormWrapper.tsx +39 -0
  67. package/src/wrappers/VerifyEmailPageWrapper.tsx +27 -0
  68. package/src/wrappers/{index.d.ts → index.tsx} +8 -1
  69. package/src/auth/Private.d.ts +0 -6
  70. package/src/auth/Private.d.ts.map +0 -1
  71. package/src/auth/Private.js +0 -14
  72. package/src/auth/PrivateRoute.d.ts +0 -8
  73. package/src/auth/PrivateRoute.d.ts.map +0 -1
  74. package/src/auth/PrivateRoute.js +0 -23
  75. package/src/auth/UnAuth.d.ts +0 -6
  76. package/src/auth/UnAuth.d.ts.map +0 -1
  77. package/src/auth/UnAuth.js +0 -14
  78. package/src/auth/UnAuthRoute.d.ts +0 -8
  79. package/src/auth/UnAuthRoute.d.ts.map +0 -1
  80. package/src/auth/UnAuthRoute.js +0 -22
  81. package/src/auth/index.d.ts.map +0 -1
  82. package/src/auth/index.js +0 -10
  83. package/src/components/ApiAccess.d.ts +0 -16
  84. package/src/components/ApiAccess.d.ts.map +0 -1
  85. package/src/components/ApiAccess.js +0 -70
  86. package/src/components/BackupCodeLoginForm.d.ts +0 -43
  87. package/src/components/BackupCodeLoginForm.d.ts.map +0 -1
  88. package/src/components/BackupCodeLoginForm.js +0 -106
  89. package/src/components/BackupCodesForm.d.ts +0 -26
  90. package/src/components/BackupCodesForm.d.ts.map +0 -1
  91. package/src/components/BackupCodesForm.js +0 -108
  92. package/src/components/ChangePasswordForm.d.ts +0 -26
  93. package/src/components/ChangePasswordForm.d.ts.map +0 -1
  94. package/src/components/ChangePasswordForm.js +0 -66
  95. package/src/components/ConfirmationDialog.d.ts +0 -13
  96. package/src/components/ConfirmationDialog.d.ts.map +0 -1
  97. package/src/components/ConfirmationDialog.js +0 -10
  98. package/src/components/CurrencyCodeSelector.d.ts +0 -9
  99. package/src/components/CurrencyCodeSelector.d.ts.map +0 -1
  100. package/src/components/CurrencyCodeSelector.js +0 -31
  101. package/src/components/CurrencyInput.d.ts +0 -13
  102. package/src/components/CurrencyInput.d.ts.map +0 -1
  103. package/src/components/CurrencyInput.js +0 -22
  104. package/src/components/DashboardPage.d.ts +0 -8
  105. package/src/components/DashboardPage.d.ts.map +0 -1
  106. package/src/components/DashboardPage.js +0 -10
  107. package/src/components/DropdownMenu.d.ts +0 -9
  108. package/src/components/DropdownMenu.d.ts.map +0 -1
  109. package/src/components/DropdownMenu.js +0 -56
  110. package/src/components/ExpirationSecondsSelector.d.ts +0 -13
  111. package/src/components/ExpirationSecondsSelector.d.ts.map +0 -1
  112. package/src/components/ExpirationSecondsSelector.js +0 -32
  113. package/src/components/Flag.d.ts +0 -20
  114. package/src/components/Flag.d.ts.map +0 -1
  115. package/src/components/Flag.js +0 -43
  116. package/src/components/ForgotPasswordForm.d.ts +0 -18
  117. package/src/components/ForgotPasswordForm.d.ts.map +0 -1
  118. package/src/components/ForgotPasswordForm.js +0 -54
  119. package/src/components/LoginForm.d.ts +0 -44
  120. package/src/components/LoginForm.d.ts.map +0 -1
  121. package/src/components/LoginForm.js +0 -99
  122. package/src/components/LogoutPage.d.ts +0 -8
  123. package/src/components/LogoutPage.d.ts.map +0 -1
  124. package/src/components/LogoutPage.js +0 -16
  125. package/src/components/RegisterForm.d.ts +0 -54
  126. package/src/components/RegisterForm.d.ts.map +0 -1
  127. package/src/components/RegisterForm.js +0 -105
  128. package/src/components/ResetPasswordForm.d.ts +0 -23
  129. package/src/components/ResetPasswordForm.d.ts.map +0 -1
  130. package/src/components/ResetPasswordForm.js +0 -68
  131. package/src/components/SideMenu.d.ts +0 -8
  132. package/src/components/SideMenu.d.ts.map +0 -1
  133. package/src/components/SideMenu.js +0 -25
  134. package/src/components/SideMenuListItem.d.ts +0 -13
  135. package/src/components/SideMenuListItem.d.ts.map +0 -1
  136. package/src/components/SideMenuListItem.js +0 -44
  137. package/src/components/TopMenu.d.ts +0 -24
  138. package/src/components/TopMenu.d.ts.map +0 -1
  139. package/src/components/TopMenu.js +0 -36
  140. package/src/components/TranslatedTitle.d.ts +0 -7
  141. package/src/components/TranslatedTitle.d.ts.map +0 -1
  142. package/src/components/TranslatedTitle.js +0 -15
  143. package/src/components/UserLanguageSelector.d.ts +0 -4
  144. package/src/components/UserLanguageSelector.d.ts.map +0 -1
  145. package/src/components/UserLanguageSelector.js +0 -31
  146. package/src/components/UserMenu.d.ts +0 -4
  147. package/src/components/UserMenu.d.ts.map +0 -1
  148. package/src/components/UserMenu.js +0 -12
  149. package/src/components/UserSettingsForm.d.ts +0 -56
  150. package/src/components/UserSettingsForm.d.ts.map +0 -1
  151. package/src/components/UserSettingsForm.js +0 -93
  152. package/src/components/VerifyEmailPage.d.ts +0 -23
  153. package/src/components/VerifyEmailPage.d.ts.map +0 -1
  154. package/src/components/VerifyEmailPage.js +0 -61
  155. package/src/components/index.d.ts.map +0 -1
  156. package/src/components/index.js +0 -28
  157. package/src/contexts/AuthProvider.d.ts +0 -152
  158. package/src/contexts/AuthProvider.d.ts.map +0 -1
  159. package/src/contexts/AuthProvider.js +0 -446
  160. package/src/contexts/I18nProvider.d.ts +0 -16
  161. package/src/contexts/I18nProvider.d.ts.map +0 -1
  162. package/src/contexts/I18nProvider.js +0 -46
  163. package/src/contexts/MenuContext.d.ts +0 -20
  164. package/src/contexts/MenuContext.d.ts.map +0 -1
  165. package/src/contexts/MenuContext.js +0 -244
  166. package/src/contexts/SuiteConfigProvider.d.ts +0 -44
  167. package/src/contexts/SuiteConfigProvider.d.ts.map +0 -1
  168. package/src/contexts/SuiteConfigProvider.js +0 -43
  169. package/src/contexts/ThemeProvider.d.ts +0 -15
  170. package/src/contexts/ThemeProvider.d.ts.map +0 -1
  171. package/src/contexts/ThemeProvider.js +0 -36
  172. package/src/contexts/index.d.ts.map +0 -1
  173. package/src/contexts/index.js +0 -8
  174. package/src/hooks/index.d.ts.map +0 -1
  175. package/src/hooks/index.js +0 -8
  176. package/src/hooks/useBackupCodes.d.ts +0 -15
  177. package/src/hooks/useBackupCodes.d.ts.map +0 -1
  178. package/src/hooks/useBackupCodes.js +0 -70
  179. package/src/hooks/useEmailVerification.d.ts +0 -10
  180. package/src/hooks/useEmailVerification.d.ts.map +0 -1
  181. package/src/hooks/useEmailVerification.js +0 -36
  182. package/src/hooks/useExpiringValue.d.ts +0 -14
  183. package/src/hooks/useExpiringValue.d.ts.map +0 -1
  184. package/src/hooks/useExpiringValue.js +0 -53
  185. package/src/hooks/useLocalStorage.d.ts +0 -2
  186. package/src/hooks/useLocalStorage.d.ts.map +0 -1
  187. package/src/hooks/useLocalStorage.js +0 -15
  188. package/src/hooks/useUserSettings.d.ts +0 -46
  189. package/src/hooks/useUserSettings.d.ts.map +0 -1
  190. package/src/hooks/useUserSettings.js +0 -152
  191. package/src/index.d.ts.map +0 -1
  192. package/src/index.js +0 -12
  193. package/src/interfaces/IAppConfig.d.ts +0 -6
  194. package/src/interfaces/IAppConfig.d.ts.map +0 -1
  195. package/src/interfaces/IAppConfig.js +0 -2
  196. package/src/interfaces/IMenuConfig.d.ts +0 -11
  197. package/src/interfaces/IMenuConfig.d.ts.map +0 -1
  198. package/src/interfaces/IMenuConfig.js +0 -2
  199. package/src/interfaces/IMenuOption.d.ts +0 -58
  200. package/src/interfaces/IMenuOption.d.ts.map +0 -1
  201. package/src/interfaces/IMenuOption.js +0 -2
  202. package/src/interfaces/index.d.ts +0 -4
  203. package/src/interfaces/index.d.ts.map +0 -1
  204. package/src/interfaces/index.js +0 -6
  205. package/src/services/__mocks__/authService.d.ts +0 -21
  206. package/src/services/__mocks__/authService.d.ts.map +0 -1
  207. package/src/services/__mocks__/authService.js +0 -15
  208. package/src/services/api.d.ts +0 -3
  209. package/src/services/api.d.ts.map +0 -1
  210. package/src/services/api.js +0 -14
  211. package/src/services/authService.d.ts +0 -72
  212. package/src/services/authService.d.ts.map +0 -1
  213. package/src/services/authService.js +0 -347
  214. package/src/services/authenticatedApi.d.ts +0 -3
  215. package/src/services/authenticatedApi.d.ts.map +0 -1
  216. package/src/services/authenticatedApi.js +0 -18
  217. package/src/services/index.d.ts +0 -4
  218. package/src/services/index.d.ts.map +0 -1
  219. package/src/services/index.js +0 -6
  220. package/src/types/MenuType.d.ts +0 -11
  221. package/src/types/MenuType.d.ts.map +0 -1
  222. package/src/types/MenuType.js +0 -12
  223. package/src/types/expirationSeconds.d.ts +0 -3
  224. package/src/types/expirationSeconds.d.ts.map +0 -1
  225. package/src/types/expirationSeconds.js +0 -17
  226. package/src/types/index.d.ts +0 -2
  227. package/src/types/index.d.ts.map +0 -1
  228. package/src/types/index.js +0 -4
  229. package/src/types/translation.d.ts +0 -10
  230. package/src/types/translation.d.ts.map +0 -1
  231. package/src/types/translation.js +0 -9
  232. package/src/wrappers/BackupCodeLoginWrapper.d.ts +0 -8
  233. package/src/wrappers/BackupCodeLoginWrapper.d.ts.map +0 -1
  234. package/src/wrappers/BackupCodeLoginWrapper.js +0 -21
  235. package/src/wrappers/BackupCodesWrapper.d.ts +0 -7
  236. package/src/wrappers/BackupCodesWrapper.d.ts.map +0 -1
  237. package/src/wrappers/BackupCodesWrapper.js +0 -17
  238. package/src/wrappers/ChangePasswordFormWrapper.d.ts +0 -8
  239. package/src/wrappers/ChangePasswordFormWrapper.d.ts.map +0 -1
  240. package/src/wrappers/ChangePasswordFormWrapper.js +0 -21
  241. package/src/wrappers/LoginFormWrapper.d.ts +0 -9
  242. package/src/wrappers/LoginFormWrapper.d.ts.map +0 -1
  243. package/src/wrappers/LoginFormWrapper.js +0 -43
  244. package/src/wrappers/LogoutPageWrapper.d.ts +0 -9
  245. package/src/wrappers/LogoutPageWrapper.d.ts.map +0 -1
  246. package/src/wrappers/LogoutPageWrapper.js +0 -21
  247. package/src/wrappers/RegisterFormWrapper.d.ts +0 -9
  248. package/src/wrappers/RegisterFormWrapper.d.ts.map +0 -1
  249. package/src/wrappers/RegisterFormWrapper.js +0 -26
  250. package/src/wrappers/UserSettingsFormWrapper.d.ts +0 -8
  251. package/src/wrappers/UserSettingsFormWrapper.d.ts.map +0 -1
  252. package/src/wrappers/UserSettingsFormWrapper.js +0 -24
  253. package/src/wrappers/VerifyEmailPageWrapper.d.ts +0 -8
  254. package/src/wrappers/VerifyEmailPageWrapper.d.ts.map +0 -1
  255. package/src/wrappers/VerifyEmailPageWrapper.js +0 -20
  256. package/src/wrappers/index.d.ts.map +0 -1
  257. package/src/wrappers/index.js +0 -20
@@ -0,0 +1,1060 @@
1
+ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
2
+ import React, { ReactNode } from 'react';
3
+
4
+ // Mock PasswordLoginService
5
+ const mockPasswordLoginService = {
6
+ getWalletAndMnemonicFromLocalStorageBundle: jest.fn() as jest.Mock,
7
+ setupPasswordLoginLocalStorageBundle: jest.fn() as jest.Mock,
8
+ };
9
+
10
+ const mockNavigate = jest.fn();
11
+
12
+ // Create shared mock instance
13
+ const mockAuthService = {
14
+ verifyToken: jest.fn() as jest.Mock,
15
+ directLogin: jest.fn() as jest.Mock,
16
+ emailChallengeLogin: jest.fn() as jest.Mock,
17
+ refreshToken: jest.fn() as jest.Mock,
18
+ register: jest.fn() as jest.Mock,
19
+ requestEmailLogin: jest.fn() as jest.Mock,
20
+ backupCodeLogin: jest.fn() as jest.Mock,
21
+ changePassword: jest.fn() as jest.Mock,
22
+ };
23
+
24
+ // Mock dependencies - must be before imports
25
+ jest.mock('../services/authService', () => ({
26
+ mockAuthService,
27
+ createAuthService: jest.fn(() => mockAuthService),
28
+ }));
29
+ jest.mock('../services/authenticatedApi', () => ({
30
+ createAuthenticatedApiClient: jest.fn(() => ({
31
+ post: jest.fn(),
32
+ get: jest.fn(),
33
+ })),
34
+ }));
35
+
36
+
37
+
38
+ jest.mock('@digitaldefiance/i18n-lib', () => {
39
+ const actual = jest.requireActual('@digitaldefiance/i18n-lib');
40
+ const mockI18nEngineInstance = {
41
+ t: jest.fn((key: string) => key),
42
+ translate: jest.fn((componentId: string, key: string) => key),
43
+ setLanguage: jest.fn(),
44
+ };
45
+ return {
46
+ ...actual,
47
+ DefaultCurrencyCode: 'USD',
48
+ CurrencyCode: class MockCurrencyCode {
49
+ constructor(public value: string = 'USD') {}
50
+ },
51
+ I18nEngine: {
52
+ getInstance: () => mockI18nEngineInstance,
53
+ },
54
+ ECIESService: jest.fn(),
55
+ t: jest.fn((key) => key),
56
+ };
57
+ });
58
+
59
+ jest.mock('@digitaldefiance/ecies-lib', () => {
60
+ const actual = jest.requireActual('@digitaldefiance/ecies-lib');
61
+ return {
62
+ ...actual,
63
+ PasswordLoginService: jest.fn().mockImplementation(() => mockPasswordLoginService),
64
+ };
65
+ });
66
+
67
+ jest.mock('./I18nProvider', () => {
68
+ const React = require('react');
69
+ const I18nProvider = ({ children }: { children: any }) => {
70
+ return React.createElement(React.Fragment, null, children);
71
+ };
72
+ I18nProvider.displayName = 'MockI18nProvider';
73
+
74
+ return {
75
+ I18nProvider,
76
+ useI18n: () => ({
77
+ t: (key: string) => key,
78
+ tComponent: (componentId: string, key: string) => key,
79
+ changeLanguage: jest.fn(),
80
+ currentLanguage: 'en-US',
81
+ }),
82
+ };
83
+ });
84
+
85
+ jest.mock('@ethereumjs/wallet', () => ({
86
+ Wallet: {
87
+ generate: jest.fn(() => ({
88
+ getPrivateKey: jest.fn(() => Buffer.from('test')),
89
+ getPublicKey: jest.fn(() => Buffer.from('test')),
90
+ getAddress: jest.fn(() => Buffer.from('test')),
91
+ })),
92
+ },
93
+ }));
94
+
95
+ // Now import after mocks
96
+ import { render, renderHook, act, waitFor } from '@testing-library/react';
97
+ import { MemoryRouter } from 'react-router-dom';
98
+ import { createAuthenticatedApiClient } from '../services/authenticatedApi';
99
+ import { Wallet } from '@ethereumjs/wallet';
100
+ import { Constants } from '@digitaldefiance/suite-core-lib';
101
+ import { localStorageMock } from '../../tests/setup';
102
+ import { SecureString, ECIES as ECIESConstants } from '@digitaldefiance/ecies-lib';
103
+ import { CurrencyCode } from '@digitaldefiance/i18n-lib';
104
+ import { AuthProvider, useAuth } from './AuthProvider';
105
+ import { I18nProvider } from './I18nProvider';
106
+ import { AppThemeProvider } from './ThemeProvider';
107
+
108
+ // Mock localStorage is imported from test-setup
109
+
110
+
111
+
112
+ // Mock console methods
113
+ const consoleMock = {
114
+ error: jest.fn(),
115
+ log: jest.fn(),
116
+ };
117
+ Object.defineProperty(console, 'error', { value: consoleMock.error });
118
+
119
+ // Test wrapper component
120
+ const TestWrapper = ({ children }: { children: ReactNode }) => (
121
+ <I18nProvider i18nEngine={null as any} onLanguageChange={async () => {}}>
122
+ <AppThemeProvider>
123
+ <AuthProvider
124
+ baseUrl="http://localhost:3000"
125
+ constants={Constants}
126
+ eciesConfig={ECIESConstants}
127
+ onLogout={mockNavigate}
128
+ >
129
+ {children}
130
+ </AuthProvider>
131
+ </AppThemeProvider>
132
+ </I18nProvider>
133
+ );
134
+
135
+ // Helper to create mock user
136
+ const createMockUser = (admin = false) => ({
137
+ id: '1',
138
+ username: admin ? 'admin' : 'testuser',
139
+ email: admin ? 'admin@example.com' : 'test@example.com',
140
+ roles: [{
141
+ id: 'role1',
142
+ _id: 'role1',
143
+ name: admin ? 'admin' : 'user',
144
+ member: true,
145
+ child: false,
146
+ system: false,
147
+ admin,
148
+ createdAt: '2023-01-01T00:00:00.000Z',
149
+ updatedAt: '2023-01-01T00:00:00.000Z',
150
+ deletedAt: undefined,
151
+ createdBy: 'system',
152
+ updatedBy: 'system',
153
+ }],
154
+ siteLanguage: 'en-US',
155
+ timezone: 'UTC',
156
+ currency: 'USD',
157
+ darkMode: false,
158
+ directChallenge: false,
159
+ emailVerified: true,
160
+ });
161
+
162
+ describe('AuthProvider', () => {
163
+ beforeEach(() => {
164
+ localStorageMock.getItem.mockReturnValue(null);
165
+ localStorageMock.setItem.mockClear();
166
+ localStorageMock.removeItem.mockClear();
167
+ mockPasswordLoginService.getWalletAndMnemonicFromLocalStorageBundle.mockClear();
168
+ mockPasswordLoginService.setupPasswordLoginLocalStorageBundle.mockClear();
169
+ mockAuthService.verifyToken.mockClear();
170
+ mockAuthService.directLogin.mockClear();
171
+ mockAuthService.emailChallengeLogin.mockClear();
172
+ mockAuthService.refreshToken.mockClear();
173
+ mockAuthService.register.mockClear();
174
+ mockAuthService.requestEmailLogin.mockClear();
175
+ mockAuthService.backupCodeLogin.mockClear();
176
+ });
177
+
178
+ afterEach(() => {
179
+ jest.clearAllTimers();
180
+ });
181
+
182
+ describe('Initial State', () => {
183
+ it('should initialize with default values', async () => {
184
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
185
+
186
+ await waitFor(() => {
187
+ expect(result.current.isAuthenticated).toBe(false);
188
+ expect(result.current.user).toBe(null);
189
+ expect(result.current.userData).toBe(null);
190
+ expect(result.current.token).toBe(null);
191
+ expect(result.current.mnemonic).toBeUndefined();
192
+ expect(result.current.wallet).toBeUndefined();
193
+ expect(result.current.admin).toBe(false);
194
+ });
195
+ });
196
+
197
+ it('should initialize expiration seconds from localStorage', async () => {
198
+ localStorageMock.getItem.mockImplementation((key) => {
199
+ if (key === 'mnemonicExpirationSeconds') return '300';
200
+ if (key === 'walletExpirationSeconds') return '600';
201
+ return null;
202
+ });
203
+
204
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
205
+
206
+ await waitFor(() => {
207
+ expect(localStorageMock.getItem).toHaveBeenCalledWith('mnemonicExpirationSeconds');
208
+ expect(localStorageMock.getItem).toHaveBeenCalledWith('walletExpirationSeconds');
209
+ });
210
+ });
211
+ });
212
+
213
+ describe('checkAuth', () => {
214
+ it('should clear auth state when no token exists', async () => {
215
+ localStorageMock.getItem.mockReturnValue(null);
216
+
217
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
218
+
219
+ await waitFor(() => {
220
+ expect(result.current.loading).toBe(false);
221
+ expect(result.current.isCheckingAuth).toBe(false);
222
+ expect(result.current.isAuthenticated).toBe(false);
223
+ });
224
+ });
225
+
226
+ it('should verify token and set user when token exists', async () => {
227
+ const mockUser = createMockUser();
228
+ mockAuthService.verifyToken.mockResolvedValue(mockUser);
229
+
230
+ localStorageMock.getItem.mockImplementation((key) =>
231
+ key === 'authToken' ? 'valid-token' : null
232
+ );
233
+
234
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
235
+
236
+ await waitFor(() => {
237
+ expect(result.current.loading).toBe(false);
238
+ expect(result.current.isCheckingAuth).toBe(false);
239
+ expect(result.current.isAuthenticated).toBe(true);
240
+ }, { timeout: 5000 });
241
+
242
+ expect(result.current.userData).toEqual(mockUser);
243
+ expect(result.current.token).toBe('valid-token');
244
+ });
245
+
246
+ it('should handle token verification failure', async () => {
247
+ localStorageMock.getItem.mockImplementation((key) => {
248
+ if (key === 'authToken') return 'invalid-token';
249
+ return null;
250
+ });
251
+ mockAuthService.verifyToken.mockRejectedValue(new Error('Token invalid'));
252
+
253
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
254
+
255
+ await waitFor(() => {
256
+ expect(result.current.isAuthenticated).toBe(false);
257
+ expect(result.current.userData).toBe(null);
258
+ expect(result.current.loading).toBe(false);
259
+ expect(result.current.isCheckingAuth).toBe(false);
260
+ expect(localStorageMock.removeItem).toHaveBeenCalledWith('authToken');
261
+ });
262
+ });
263
+
264
+ it('should set admin flag for admin users', async () => {
265
+ const mockAdminUser = createMockUser(true);
266
+ mockAuthService.verifyToken.mockResolvedValue(mockAdminUser);
267
+
268
+ localStorageMock.getItem.mockImplementation((key) =>
269
+ key === 'authToken' ? 'admin-token' : null
270
+ );
271
+
272
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
273
+
274
+ await waitFor(() => {
275
+ expect(result.current.loading).toBe(false);
276
+ expect(result.current.admin).toBe(true);
277
+ }, { timeout: 5000 });
278
+ });
279
+ });
280
+
281
+ describe('directLogin', () => {
282
+ it('should perform successful direct login', async () => {
283
+ const mockMnemonic = new SecureString('test mnemonic');
284
+ const mockWallet = Wallet.generate();
285
+ const mockUser = createMockUser();
286
+ const mockLoginResult = {
287
+ token: 'new-token',
288
+ user: mockUser,
289
+ wallet: mockWallet,
290
+ message: 'Login successful',
291
+ };
292
+
293
+ mockAuthService.directLogin.mockResolvedValue(mockLoginResult);
294
+
295
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
296
+
297
+ await waitFor(() => {
298
+ expect(result.current.directLogin).toBeDefined();
299
+ });
300
+
301
+ let loginResult;
302
+ await act(async () => {
303
+ loginResult = await result.current.directLogin(mockMnemonic, 'testuser');
304
+ });
305
+
306
+ expect(mockAuthService.directLogin).toHaveBeenCalled();
307
+ expect(loginResult).toEqual(mockLoginResult);
308
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('authToken', 'new-token');
309
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('user', JSON.stringify(mockUser));
310
+ });
311
+
312
+ it('should perform successful direct login with custom expiration seconds', async () => {
313
+ const mockMnemonic = new SecureString('test mnemonic');
314
+ const mockWallet = Wallet.generate();
315
+ const mockUser = createMockUser();
316
+ const mockLoginResult = {
317
+ token: 'new-token',
318
+ user: mockUser,
319
+ wallet: mockWallet,
320
+ message: 'Login successful',
321
+ };
322
+
323
+ mockAuthService.directLogin.mockResolvedValue(mockLoginResult);
324
+
325
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
326
+
327
+ let loginResult;
328
+ await act(async () => {
329
+ loginResult = await result.current.directLogin(
330
+ mockMnemonic,
331
+ 'testuser',
332
+ undefined,
333
+ 600, // mnemonicExpirationSeconds
334
+ 1200 // walletExpirationSeconds
335
+ );
336
+ });
337
+
338
+ expect(loginResult).toEqual(mockLoginResult);
339
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('authToken', 'new-token');
340
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('user', JSON.stringify(mockUser));
341
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('mnemonicExpirationSeconds', '600');
342
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('walletExpirationSeconds', '1200');
343
+ });
344
+
345
+ it('should handle direct login failure', async () => {
346
+ const mockMnemonic = new SecureString('test mnemonic');
347
+ const mockError = { error: 'Login failed' };
348
+
349
+ mockAuthService.directLogin.mockResolvedValue(mockError);
350
+
351
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
352
+
353
+ await act(async () => {
354
+ await result.current.directLogin(mockMnemonic, 'testuser');
355
+ });
356
+ expect(result.current.isAuthenticated).toBe(false);
357
+ });
358
+ });
359
+
360
+ describe('emailChallengeLogin', () => {
361
+ it('should perform successful email challenge login', async () => {
362
+ const mockMnemonic = new SecureString('test mnemonic');
363
+ const mockWallet = Wallet.generate();
364
+ const mockUser = createMockUser();
365
+ const mockLoginResult = {
366
+ token: 'new-token',
367
+ user: mockUser,
368
+ wallet: mockWallet,
369
+ message: 'Login successful',
370
+ };
371
+
372
+ mockAuthService.emailChallengeLogin.mockResolvedValue(mockLoginResult);
373
+
374
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
375
+
376
+ let loginResult;
377
+ await act(async () => {
378
+ loginResult = await result.current.emailChallengeLogin(
379
+ mockMnemonic,
380
+ 'challenge-token',
381
+ 'testuser'
382
+ );
383
+ });
384
+
385
+ expect(loginResult).toEqual(mockLoginResult);
386
+ });
387
+
388
+ it('should perform successful email challenge login with custom expiration seconds', async () => {
389
+ const mockMnemonic = new SecureString('test mnemonic');
390
+ const mockWallet = Wallet.generate();
391
+ const mockUser = createMockUser();
392
+ const mockLoginResult = {
393
+ token: 'new-token',
394
+ user: mockUser,
395
+ wallet: mockWallet,
396
+ message: 'Login successful',
397
+ };
398
+
399
+ mockAuthService.emailChallengeLogin.mockResolvedValue(mockLoginResult);
400
+
401
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
402
+
403
+ let loginResult;
404
+ await act(async () => {
405
+ loginResult = await result.current.emailChallengeLogin(
406
+ mockMnemonic,
407
+ 'challenge-token',
408
+ 'testuser',
409
+ undefined,
410
+ 300, // mnemonicExpirationSeconds
411
+ 900 // walletExpirationSeconds
412
+ );
413
+ });
414
+
415
+ expect(loginResult).toEqual(mockLoginResult);
416
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('mnemonicExpirationSeconds', '300');
417
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('walletExpirationSeconds', '900');
418
+ });
419
+ });
420
+
421
+ describe('refreshToken', () => {
422
+ it('should refresh token successfully', async () => {
423
+ const mockUser = createMockUser();
424
+ const mockRefreshResult = {
425
+ token: 'refreshed-token',
426
+ user: mockUser,
427
+ };
428
+
429
+ mockAuthService.refreshToken.mockResolvedValue(mockRefreshResult);
430
+
431
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
432
+
433
+ await waitFor(() => {
434
+ expect(result.current.refreshToken).toBeDefined();
435
+ });
436
+
437
+ let refreshResult;
438
+ await act(async () => {
439
+ refreshResult = await result.current.refreshToken();
440
+ });
441
+
442
+ expect(refreshResult).toEqual(mockRefreshResult);
443
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('authToken', 'refreshed-token');
444
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('user', JSON.stringify(mockUser));
445
+ });
446
+
447
+ it('should handle refresh token failure', async () => {
448
+ mockAuthService.refreshToken.mockRejectedValue(new Error('Refresh failed'));
449
+
450
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
451
+
452
+ await waitFor(() => {
453
+ expect(result.current.refreshToken).toBeDefined();
454
+ });
455
+
456
+ await expect(act(async () => {
457
+ await result.current.refreshToken();
458
+ })).rejects.toThrow('Refresh failed');
459
+
460
+ expect(localStorageMock.removeItem).toHaveBeenCalledWith('authToken');
461
+ expect(localStorageMock.removeItem).toHaveBeenCalledWith('user');
462
+ });
463
+ });
464
+
465
+ describe('register', () => {
466
+ it('should register user successfully', async () => {
467
+ const mockRegisterResult = {
468
+ success: true,
469
+ message: 'Registration successful',
470
+ mnemonic: 'test mnemonic',
471
+ };
472
+
473
+ mockAuthService.register.mockResolvedValue(mockRegisterResult);
474
+
475
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
476
+
477
+ await act(async () => {
478
+ await result.current.register(
479
+ 'testuser',
480
+ 'test@example.com',
481
+ 'UTC',
482
+ 'password'
483
+ );
484
+ });
485
+ });
486
+ });
487
+
488
+ describe('requestEmailLogin', () => {
489
+ it('should request email login successfully', async () => {
490
+ const mockMessage = 'Email sent successfully';
491
+ mockAuthService.requestEmailLogin.mockResolvedValue(mockMessage);
492
+
493
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
494
+
495
+ await act(async () => {
496
+ await result.current.requestEmailLogin('testuser');
497
+ });
498
+ });
499
+ });
500
+
501
+ describe('backupCodeLogin', () => {
502
+ it('should perform backup code login successfully', async () => {
503
+ const mockUser = createMockUser();
504
+ const mockLoginResult = {
505
+ token: 'backup-token',
506
+ user: mockUser,
507
+ codeCount: 5,
508
+ mnemonic: 'recovered mnemonic',
509
+ message: 'Login successful',
510
+ };
511
+
512
+ mockAuthService.backupCodeLogin.mockResolvedValue(mockLoginResult);
513
+
514
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
515
+
516
+ let loginResult;
517
+ await act(async () => {
518
+ loginResult = await result.current.backupCodeLogin(
519
+ 'testuser',
520
+ 'backup-code',
521
+ false,
522
+ true
523
+ );
524
+ });
525
+
526
+ expect(loginResult).toEqual({
527
+ token: 'backup-token',
528
+ codeCount: 5,
529
+ mnemonic: 'recovered mnemonic',
530
+ message: 'Login successful',
531
+ });
532
+ });
533
+ });
534
+
535
+ describe('logout', () => {
536
+ it('should logout user and clear all data', async () => {
537
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
538
+
539
+ await act(async () => {
540
+ await result.current.logout();
541
+ });
542
+
543
+ expect(localStorageMock.removeItem).toHaveBeenCalledWith('user');
544
+ expect(localStorageMock.removeItem).toHaveBeenCalledWith('authToken');
545
+ expect(result.current.isAuthenticated).toBe(false);
546
+ expect(result.current.userData).toBe(null);
547
+ expect(result.current.mnemonic).toBeUndefined();
548
+ expect(result.current.wallet).toBeUndefined();
549
+ expect(mockNavigate).toHaveBeenCalled();
550
+ });
551
+ });
552
+
553
+ describe('verifyToken', () => {
554
+ it('should verify token successfully', async () => {
555
+ const mockUser = createMockUser();
556
+ mockAuthService.verifyToken.mockResolvedValue(mockUser);
557
+
558
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
559
+
560
+ let verifyResult;
561
+ await act(async () => {
562
+ verifyResult = await result.current.verifyToken('test-token');
563
+ });
564
+
565
+ expect(verifyResult).toBe(true);
566
+ expect(mockAuthService.verifyToken).toHaveBeenCalledWith('test-token');
567
+ });
568
+
569
+ it('should handle token verification failure', async () => {
570
+ mockAuthService.verifyToken.mockResolvedValue({ error: 'Invalid token' });
571
+
572
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
573
+
574
+ let verifyResult;
575
+ await act(async () => {
576
+ verifyResult = await result.current.verifyToken('invalid-token');
577
+ });
578
+
579
+ expect(verifyResult).toBe(false);
580
+ expect(mockAuthService.verifyToken).toHaveBeenCalledWith('invalid-token');
581
+ });
582
+ });
583
+
584
+ describe('changePassword', () => {
585
+ it('should change password successfully', async () => {
586
+ // Mock localStorage to have encrypted password (password login available)
587
+ localStorageMock.getItem.mockImplementation((key) => {
588
+ if (key === 'encryptedPassword') return 'mock-encrypted-password';
589
+ return null;
590
+ });
591
+
592
+ // Mock successful password login service operations
593
+ const mockMnemonic = new SecureString('test mnemonic');
594
+ const mockWallet = Wallet.generate();
595
+ mockPasswordLoginService.getWalletAndMnemonicFromLocalStorageBundle.mockResolvedValue({
596
+ mnemonic: mockMnemonic,
597
+ wallet: mockWallet,
598
+ });
599
+ mockPasswordLoginService.setupPasswordLoginLocalStorageBundle.mockResolvedValue(mockWallet);
600
+
601
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
602
+
603
+ await act(async () => {
604
+ await result.current.changePassword('oldpass', 'newpass');
605
+ });
606
+ });
607
+
608
+ it('should handle password change failure', async () => {
609
+ // Mock localStorage to not have encrypted password (password login not available)
610
+ localStorageMock.getItem.mockReturnValue(null);
611
+
612
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
613
+
614
+ let changeResult;
615
+ await act(async () => {
616
+ changeResult = await result.current.changePassword('oldpass', 'newpass');
617
+ });
618
+
619
+ expect(changeResult).toEqual({ error: 'error_login_passwordLoginNotSetup', errorType: 'PasswordLoginNotSetup' });
620
+ });
621
+ });
622
+
623
+ describe('Mnemonic Management', () => {
624
+ it('should set and clear mnemonic', async () => {
625
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
626
+ const mockMnemonic = new SecureString('test mnemonic');
627
+
628
+ await waitFor(() => {
629
+ expect(result.current.setMnemonic).toBeDefined();
630
+ });
631
+
632
+ act(() => {
633
+ result.current.setMnemonic(mockMnemonic, 10);
634
+ });
635
+
636
+ expect(result.current.mnemonic).toEqual(mockMnemonic);
637
+
638
+ act(() => {
639
+ result.current.clearMnemonic();
640
+ });
641
+
642
+ expect(result.current.mnemonic).toBeUndefined();
643
+ });
644
+
645
+ it('should store mnemonic expiration seconds to localStorage when provided', async () => {
646
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
647
+ const mockMnemonic = new SecureString('test mnemonic');
648
+
649
+ await waitFor(() => {
650
+ expect(result.current.setMnemonic).toBeDefined();
651
+ });
652
+
653
+ act(() => {
654
+ result.current.setMnemonic(mockMnemonic, 300);
655
+ });
656
+
657
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('mnemonicExpirationSeconds', '300');
658
+ expect(result.current.mnemonic).toEqual(mockMnemonic);
659
+ });
660
+
661
+ it('should use stored expiration seconds when duration not provided', async () => {
662
+ localStorageMock.getItem.mockImplementation((key) =>
663
+ key === 'mnemonicExpirationSeconds' ? '600' : null
664
+ );
665
+
666
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
667
+ const mockMnemonic = new SecureString('test mnemonic');
668
+
669
+ await waitFor(() => {
670
+ expect(result.current.setMnemonic).toBeDefined();
671
+ });
672
+
673
+ act(() => {
674
+ result.current.setMnemonic(mockMnemonic);
675
+ });
676
+
677
+ expect(result.current.mnemonic).toEqual(mockMnemonic);
678
+ // Should not call setItem when no duration provided
679
+ expect(localStorageMock.setItem).not.toHaveBeenCalledWith('mnemonicExpirationSeconds', expect.any(String));
680
+ });
681
+ });
682
+
683
+ describe('Wallet Management', () => {
684
+ it('should set and clear wallet', async () => {
685
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
686
+ const mockWallet = Wallet.generate();
687
+
688
+ await waitFor(() => {
689
+ expect(result.current.setWallet).toBeDefined();
690
+ });
691
+
692
+ act(() => {
693
+ result.current.setWallet(mockWallet, 10);
694
+ });
695
+
696
+ expect(result.current.wallet).toEqual(mockWallet);
697
+
698
+ act(() => {
699
+ result.current.clearWallet();
700
+ });
701
+
702
+ expect(result.current.wallet).toBeUndefined();
703
+ });
704
+
705
+ it('should store wallet expiration seconds to localStorage when provided', async () => {
706
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
707
+ const mockWallet = Wallet.generate();
708
+
709
+ await waitFor(() => {
710
+ expect(result.current.setWallet).toBeDefined();
711
+ });
712
+
713
+ act(() => {
714
+ result.current.setWallet(mockWallet, 900);
715
+ });
716
+
717
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('walletExpirationSeconds', '900');
718
+ expect(result.current.wallet).toEqual(mockWallet);
719
+ });
720
+
721
+ it('should use stored expiration seconds when duration not provided', async () => {
722
+ localStorageMock.getItem.mockImplementation((key) =>
723
+ key === 'walletExpirationSeconds' ? '1200' : null
724
+ );
725
+
726
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
727
+ const mockWallet = Wallet.generate();
728
+
729
+ await waitFor(() => {
730
+ expect(result.current.setWallet).toBeDefined();
731
+ });
732
+
733
+ act(() => {
734
+ result.current.setWallet(mockWallet);
735
+ });
736
+
737
+ expect(result.current.wallet).toEqual(mockWallet);
738
+ // Should not call setItem when no duration provided
739
+ expect(localStorageMock.setItem).not.toHaveBeenCalledWith('walletExpirationSeconds', expect.any(String));
740
+ });
741
+ });
742
+
743
+ describe('Expiration Settings Management', () => {
744
+ it('should set mnemonic expiration seconds and update localStorage', async () => {
745
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
746
+
747
+ await waitFor(() => {
748
+ expect(result.current.setMnemonicExpirationSeconds).toBeDefined();
749
+ });
750
+
751
+ act(() => {
752
+ result.current.setMnemonicExpirationSeconds(1800);
753
+ });
754
+
755
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('mnemonicExpirationSeconds', '1800');
756
+ });
757
+
758
+ it('should set wallet expiration seconds and update localStorage', async () => {
759
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
760
+
761
+ await waitFor(() => {
762
+ expect(result.current.setWalletExpirationSeconds).toBeDefined();
763
+ });
764
+
765
+ act(() => {
766
+ result.current.setWalletExpirationSeconds(3600);
767
+ });
768
+
769
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('walletExpirationSeconds', '3600');
770
+ });
771
+
772
+ it('should use updated expiration seconds after setting them', async () => {
773
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
774
+ const mockMnemonic = new SecureString('test mnemonic');
775
+ const mockWallet = Wallet.generate();
776
+
777
+ await waitFor(() => {
778
+ expect(result.current.setMnemonicExpirationSeconds).toBeDefined();
779
+ expect(result.current.setWalletExpirationSeconds).toBeDefined();
780
+ });
781
+
782
+ // Set new expiration times
783
+ act(() => {
784
+ result.current.setMnemonicExpirationSeconds(2400);
785
+ result.current.setWalletExpirationSeconds(4800);
786
+ });
787
+
788
+ // Clear the setItem calls from setting expiration times
789
+ localStorageMock.setItem.mockClear();
790
+
791
+ // Now set mnemonic and wallet without duration - should use the stored values
792
+ act(() => {
793
+ result.current.setMnemonic(mockMnemonic);
794
+ });
795
+
796
+ act(() => {
797
+ result.current.setWallet(mockWallet);
798
+ });
799
+
800
+ expect(result.current.mnemonic).toEqual(mockMnemonic);
801
+ expect(result.current.wallet).toEqual(mockWallet);
802
+ // Should not call setItem again since we're using stored defaults
803
+ expect(localStorageMock.setItem).not.toHaveBeenCalledWith('mnemonicExpirationSeconds', expect.any(String));
804
+ expect(localStorageMock.setItem).not.toHaveBeenCalledWith('walletExpirationSeconds', expect.any(String));
805
+ });
806
+ });
807
+
808
+ describe('Timeout Management', () => {
809
+ it('should set expiration times and use them for timeouts', async () => {
810
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
811
+ const mockMnemonic = new SecureString('test mnemonic');
812
+ const mockWallet = Wallet.generate();
813
+
814
+ await waitFor(() => {
815
+ expect(result.current.setMnemonicExpirationSeconds).toBeDefined();
816
+ expect(result.current.setWalletExpirationSeconds).toBeDefined();
817
+ });
818
+
819
+ // Set custom expiration times
820
+ act(() => {
821
+ result.current.setMnemonicExpirationSeconds(120); // 2 minutes
822
+ result.current.setWalletExpirationSeconds(180); // 3 minutes
823
+ });
824
+
825
+ // Verify localStorage was updated
826
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('mnemonicExpirationSeconds', '120');
827
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('walletExpirationSeconds', '180');
828
+
829
+ // Set mnemonic and wallet without explicit duration - should use the custom times
830
+ act(() => {
831
+ result.current.setMnemonic(mockMnemonic);
832
+ result.current.setWallet(mockWallet);
833
+ });
834
+
835
+ expect(result.current.mnemonic).toEqual(mockMnemonic);
836
+ expect(result.current.wallet).toEqual(mockWallet);
837
+
838
+ // Verify timeout functions are set up (they should be active but we won't test timing)
839
+ expect(result.current.mnemonic).toBeDefined();
840
+ expect(result.current.wallet).toBeDefined();
841
+ });
842
+ });
843
+
844
+ describe('passwordLogin', () => {
845
+ it('should perform successful password login', async () => {
846
+ const mockMnemonic = new SecureString('test mnemonic');
847
+ const mockWallet = Wallet.generate();
848
+ const mockUser = createMockUser();
849
+ const mockLoginResult = {
850
+ token: 'password-token',
851
+ message: 'Login successful',
852
+ user: mockUser,
853
+ wallet: mockWallet,
854
+ };
855
+
856
+ localStorageMock.getItem.mockImplementation((key) => {
857
+ if (key === 'encryptedPassword') return 'mock-encrypted-password';
858
+ if (key === 'passwordLoginSalt') return '0'.repeat(64);
859
+ if (key === 'encryptedPrivateKey') return '0'.repeat(128);
860
+ if (key === 'encryptedMnemonic') return '0'.repeat(128);
861
+ if (key === 'pbkdf2Profile') return 'BROWSER_PASSWORD';
862
+ return null;
863
+ });
864
+
865
+ mockPasswordLoginService.getWalletAndMnemonicFromLocalStorageBundle.mockResolvedValue({
866
+ mnemonic: mockMnemonic,
867
+ wallet: mockWallet,
868
+ });
869
+ mockAuthService.directLogin.mockResolvedValue(mockLoginResult);
870
+
871
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
872
+
873
+ let loginResult;
874
+ await act(async () => {
875
+ loginResult = await result.current.passwordLogin(new SecureString('password'));
876
+ });
877
+
878
+ expect(loginResult).toEqual(mockLoginResult);
879
+ expect(mockPasswordLoginService.getWalletAndMnemonicFromLocalStorageBundle).toHaveBeenCalled();
880
+ });
881
+
882
+ it('should handle password login when not available', async () => {
883
+ localStorageMock.getItem.mockReturnValue(null);
884
+
885
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
886
+
887
+ let loginResult;
888
+ await act(async () => {
889
+ loginResult = await result.current.passwordLogin(new SecureString('password'));
890
+ });
891
+
892
+ expect(loginResult).toEqual({ error: 'error_login_passwordLoginNotSetup', errorType: 'PasswordLoginNotSetup' });
893
+ });
894
+ });
895
+
896
+ describe('setUpBrowserPasswordLogin', () => {
897
+ it('should set up password login successfully', async () => {
898
+ const mockMnemonic = new SecureString('test mnemonic');
899
+ const mockWallet = Wallet.generate();
900
+
901
+ mockPasswordLoginService.setupPasswordLoginLocalStorageBundle.mockResolvedValue(mockWallet);
902
+
903
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
904
+
905
+ let setupResult;
906
+ await act(async () => {
907
+ setupResult = await result.current.setUpBrowserPasswordLogin(
908
+ mockMnemonic,
909
+ new SecureString('password')
910
+ );
911
+ });
912
+
913
+ expect(setupResult).toEqual({ success: true, message: expect.any(String) });
914
+ expect(mockPasswordLoginService.setupPasswordLoginLocalStorageBundle).toHaveBeenCalled();
915
+ });
916
+
917
+ it('should handle password setup failure', async () => {
918
+ const mockMnemonic = new SecureString('test mnemonic');
919
+ mockPasswordLoginService.setupPasswordLoginLocalStorageBundle.mockRejectedValue(new Error('Setup failed'));
920
+
921
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
922
+
923
+ let setupResult;
924
+ await act(async () => {
925
+ setupResult = await result.current.setUpBrowserPasswordLogin(
926
+ mockMnemonic,
927
+ new SecureString('password')
928
+ );
929
+ });
930
+
931
+ expect(setupResult).toEqual({
932
+ success: false,
933
+ message: 'passwordLogin_setup_failure',
934
+ });
935
+ });
936
+ });
937
+
938
+ describe('isBrowserPasswordLoginAvailable', () => {
939
+ it('should return true when encrypted password exists', async () => {
940
+ localStorageMock.getItem.mockImplementation((key) =>
941
+ key === 'encryptedPassword' ? 'mock-encrypted-password' : null
942
+ );
943
+
944
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
945
+
946
+ await waitFor(() => {
947
+ expect(result.current.isBrowserPasswordLoginAvailable?.()).toBe(true);
948
+ });
949
+ });
950
+
951
+ it('should return false when no encrypted password exists', async () => {
952
+ localStorageMock.getItem.mockReturnValue(null);
953
+
954
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
955
+
956
+ await waitFor(() => {
957
+ expect(result.current.isBrowserPasswordLoginAvailable?.()).toBe(false);
958
+ });
959
+ });
960
+ });
961
+
962
+ describe('Timeout Setup', () => {
963
+ it('should set up timeouts when setting mnemonic and wallet', async () => {
964
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
965
+ const mockMnemonic = new SecureString('test mnemonic');
966
+ const mockWallet = Wallet.generate();
967
+
968
+ await waitFor(() => {
969
+ expect(result.current.setMnemonic).toBeDefined();
970
+ expect(result.current.setWallet).toBeDefined();
971
+ });
972
+
973
+ act(() => {
974
+ result.current.setMnemonic(mockMnemonic, 300);
975
+ result.current.setWallet(mockWallet, 600);
976
+ });
977
+
978
+ expect(result.current.mnemonic).toEqual(mockMnemonic);
979
+ expect(result.current.wallet).toEqual(mockWallet);
980
+ });
981
+ });
982
+
983
+ describe('State Exposure', () => {
984
+ it('should expose mnemonicExpirationSeconds and walletExpirationSeconds', async () => {
985
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
986
+
987
+ await waitFor(() => {
988
+ expect(typeof result.current.mnemonicExpirationSeconds).toBe('number');
989
+ expect(typeof result.current.walletExpirationSeconds).toBe('number');
990
+ });
991
+ });
992
+ });
993
+
994
+ describe('Edge Cases', () => {
995
+ it('should handle invalid localStorage values gracefully', async () => {
996
+ localStorageMock.getItem.mockImplementation((key) => {
997
+ if (key === 'mnemonicExpirationSeconds') return 'invalid';
998
+ if (key === 'walletExpirationSeconds') return 'invalid';
999
+ return null;
1000
+ });
1001
+
1002
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
1003
+
1004
+ await waitFor(() => {
1005
+ expect(typeof result.current.mnemonicExpirationSeconds).toBe('number');
1006
+ expect(typeof result.current.walletExpirationSeconds).toBe('number');
1007
+ });
1008
+ });
1009
+
1010
+ it('should handle checkAuth with error response from verifyToken', async () => {
1011
+ localStorageMock.getItem.mockImplementation((key) => {
1012
+ if (key === 'authToken') return 'token-with-error';
1013
+ return null;
1014
+ });
1015
+ mockAuthService.verifyToken.mockResolvedValue({ error: 'Token expired' });
1016
+
1017
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
1018
+
1019
+ await waitFor(() => {
1020
+ expect(result.current.isAuthenticated).toBe(false);
1021
+ expect(result.current.token).toBe(null);
1022
+ });
1023
+ });
1024
+
1025
+ it('should handle changePassword with invalid current password', async () => {
1026
+ localStorageMock.getItem.mockImplementation((key) => {
1027
+ if (key === 'encryptedPassword') return 'mock-encrypted-password';
1028
+ return null;
1029
+ });
1030
+
1031
+ mockPasswordLoginService.getWalletAndMnemonicFromLocalStorageBundle.mockResolvedValue({
1032
+ mnemonic: null, // Invalid mnemonic
1033
+ wallet: Wallet.generate(),
1034
+ });
1035
+
1036
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
1037
+
1038
+ await act(async () => {
1039
+ await result.current.changePassword('wrongpass', 'newpass');
1040
+ });
1041
+ });
1042
+ });
1043
+
1044
+ describe('Error Handling', () => {
1045
+ it('should handle console errors gracefully', async () => {
1046
+ localStorageMock.getItem.mockImplementation((key) => {
1047
+ if (key === 'authToken') return 'invalid-token';
1048
+ return null;
1049
+ });
1050
+ mockAuthService.verifyToken.mockRejectedValue(new Error('Network error'));
1051
+
1052
+ const { result } = renderHook(() => useAuth(), { wrapper: TestWrapper });
1053
+
1054
+ await waitFor(() => {
1055
+ expect(result.current.isAuthenticated).toBe(false);
1056
+ expect(consoleMock.error).toHaveBeenCalledWith('Token verification failed:', expect.any(Error));
1057
+ });
1058
+ });
1059
+ });
1060
+ });