@digitaldefiance/express-suite-react-components 2.9.1 → 2.9.2
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.
- package/LICENSE +21 -0
- package/package.json +10 -6
- package/src/auth/Private.tsx +17 -0
- package/src/auth/PrivateRoute.tsx +28 -0
- package/src/auth/UnAuth.tsx +16 -0
- package/src/auth/UnAuthRoute.tsx +30 -0
- package/src/auth/{index.d.ts → index.ts} +1 -2
- package/src/components/ApiAccess.tsx +134 -0
- package/src/components/BackupCodeLoginForm.tsx +314 -0
- package/src/components/BackupCodesForm.tsx +198 -0
- package/src/components/ChangePasswordForm.tsx +182 -0
- package/src/components/ConfirmationDialog.tsx +48 -0
- package/src/components/CurrencyCodeSelector.tsx +60 -0
- package/src/components/CurrencyInput.tsx +80 -0
- package/src/components/DashboardPage.tsx +24 -0
- package/src/components/DropdownMenu.tsx +92 -0
- package/src/components/ExpirationSecondsSelector.tsx +65 -0
- package/src/components/Flag.tsx +53 -0
- package/src/components/ForgotPasswordForm.tsx +120 -0
- package/src/components/LoginForm.tsx +307 -0
- package/src/components/LogoutPage.tsx +21 -0
- package/src/components/RegisterForm.tsx +354 -0
- package/src/components/ResetPasswordForm.tsx +164 -0
- package/src/components/SideMenu.tsx +46 -0
- package/src/components/SideMenuListItem.tsx +74 -0
- package/src/components/TopMenu.tsx +134 -0
- package/src/components/TranslatedTitle.tsx +22 -0
- package/src/components/UserLanguageSelector.tsx +45 -0
- package/src/components/UserMenu.tsx +15 -0
- package/src/components/UserSettingsForm.tsx +328 -0
- package/src/components/VerifyEmailPage.tsx +133 -0
- package/src/components/{index.d.ts → index.ts} +1 -1
- package/src/contexts/AuthProvider.spec.tsx +1060 -0
- package/src/contexts/AuthProvider.tsx +741 -0
- package/src/contexts/I18nProvider.tsx +85 -0
- package/src/contexts/MenuContext.tsx +310 -0
- package/src/contexts/SuiteConfigProvider.tsx +93 -0
- package/src/contexts/ThemeProvider.tsx +67 -0
- package/src/contexts/{index.d.ts → index.ts} +0 -1
- package/src/hooks/{index.d.ts → index.ts} +0 -1
- package/src/hooks/useBackupCodes.ts +85 -0
- package/src/hooks/useEmailVerification.ts +39 -0
- package/src/hooks/useExpiringValue.ts +78 -0
- package/src/hooks/useLocalStorage.ts +18 -0
- package/src/hooks/useUserSettings.ts +216 -0
- package/src/{index.d.ts → index.ts} +1 -1
- package/src/interfaces/IAppConfig.ts +5 -0
- package/src/interfaces/IMenuConfig.ts +11 -0
- package/src/interfaces/IMenuOption.ts +55 -0
- package/src/interfaces/index.ts +3 -0
- package/src/services/__mocks__/authService.ts +14 -0
- package/src/services/api.ts +13 -0
- package/src/services/authService.ts +422 -0
- package/src/services/authenticatedApi.ts +17 -0
- package/src/services/index.ts +3 -0
- package/src/types/MenuType.ts +15 -0
- package/src/types/expirationSeconds.ts +18 -0
- package/src/types/index.ts +1 -0
- package/src/types/translation.ts +20 -0
- package/src/wrappers/BackupCodeLoginWrapper.tsx +35 -0
- package/src/wrappers/BackupCodesWrapper.tsx +28 -0
- package/src/wrappers/ChangePasswordFormWrapper.tsx +31 -0
- package/src/wrappers/LoginFormWrapper.tsx +59 -0
- package/src/wrappers/LogoutPageWrapper.tsx +30 -0
- package/src/wrappers/RegisterFormWrapper.tsx +48 -0
- package/src/wrappers/UserSettingsFormWrapper.tsx +39 -0
- package/src/wrappers/VerifyEmailPageWrapper.tsx +27 -0
- package/src/wrappers/{index.d.ts → index.tsx} +8 -1
- package/src/auth/Private.d.ts +0 -6
- package/src/auth/Private.d.ts.map +0 -1
- package/src/auth/Private.js +0 -14
- package/src/auth/PrivateRoute.d.ts +0 -8
- package/src/auth/PrivateRoute.d.ts.map +0 -1
- package/src/auth/PrivateRoute.js +0 -23
- package/src/auth/UnAuth.d.ts +0 -6
- package/src/auth/UnAuth.d.ts.map +0 -1
- package/src/auth/UnAuth.js +0 -14
- package/src/auth/UnAuthRoute.d.ts +0 -8
- package/src/auth/UnAuthRoute.d.ts.map +0 -1
- package/src/auth/UnAuthRoute.js +0 -22
- package/src/auth/index.d.ts.map +0 -1
- package/src/auth/index.js +0 -10
- package/src/components/ApiAccess.d.ts +0 -16
- package/src/components/ApiAccess.d.ts.map +0 -1
- package/src/components/ApiAccess.js +0 -70
- package/src/components/BackupCodeLoginForm.d.ts +0 -43
- package/src/components/BackupCodeLoginForm.d.ts.map +0 -1
- package/src/components/BackupCodeLoginForm.js +0 -106
- package/src/components/BackupCodesForm.d.ts +0 -26
- package/src/components/BackupCodesForm.d.ts.map +0 -1
- package/src/components/BackupCodesForm.js +0 -108
- package/src/components/ChangePasswordForm.d.ts +0 -26
- package/src/components/ChangePasswordForm.d.ts.map +0 -1
- package/src/components/ChangePasswordForm.js +0 -66
- package/src/components/ConfirmationDialog.d.ts +0 -13
- package/src/components/ConfirmationDialog.d.ts.map +0 -1
- package/src/components/ConfirmationDialog.js +0 -10
- package/src/components/CurrencyCodeSelector.d.ts +0 -9
- package/src/components/CurrencyCodeSelector.d.ts.map +0 -1
- package/src/components/CurrencyCodeSelector.js +0 -31
- package/src/components/CurrencyInput.d.ts +0 -13
- package/src/components/CurrencyInput.d.ts.map +0 -1
- package/src/components/CurrencyInput.js +0 -22
- package/src/components/DashboardPage.d.ts +0 -8
- package/src/components/DashboardPage.d.ts.map +0 -1
- package/src/components/DashboardPage.js +0 -10
- package/src/components/DropdownMenu.d.ts +0 -9
- package/src/components/DropdownMenu.d.ts.map +0 -1
- package/src/components/DropdownMenu.js +0 -56
- package/src/components/ExpirationSecondsSelector.d.ts +0 -13
- package/src/components/ExpirationSecondsSelector.d.ts.map +0 -1
- package/src/components/ExpirationSecondsSelector.js +0 -32
- package/src/components/Flag.d.ts +0 -20
- package/src/components/Flag.d.ts.map +0 -1
- package/src/components/Flag.js +0 -43
- package/src/components/ForgotPasswordForm.d.ts +0 -18
- package/src/components/ForgotPasswordForm.d.ts.map +0 -1
- package/src/components/ForgotPasswordForm.js +0 -54
- package/src/components/LoginForm.d.ts +0 -44
- package/src/components/LoginForm.d.ts.map +0 -1
- package/src/components/LoginForm.js +0 -99
- package/src/components/LogoutPage.d.ts +0 -8
- package/src/components/LogoutPage.d.ts.map +0 -1
- package/src/components/LogoutPage.js +0 -16
- package/src/components/RegisterForm.d.ts +0 -54
- package/src/components/RegisterForm.d.ts.map +0 -1
- package/src/components/RegisterForm.js +0 -105
- package/src/components/ResetPasswordForm.d.ts +0 -23
- package/src/components/ResetPasswordForm.d.ts.map +0 -1
- package/src/components/ResetPasswordForm.js +0 -68
- package/src/components/SideMenu.d.ts +0 -8
- package/src/components/SideMenu.d.ts.map +0 -1
- package/src/components/SideMenu.js +0 -25
- package/src/components/SideMenuListItem.d.ts +0 -13
- package/src/components/SideMenuListItem.d.ts.map +0 -1
- package/src/components/SideMenuListItem.js +0 -44
- package/src/components/TopMenu.d.ts +0 -24
- package/src/components/TopMenu.d.ts.map +0 -1
- package/src/components/TopMenu.js +0 -36
- package/src/components/TranslatedTitle.d.ts +0 -7
- package/src/components/TranslatedTitle.d.ts.map +0 -1
- package/src/components/TranslatedTitle.js +0 -15
- package/src/components/UserLanguageSelector.d.ts +0 -4
- package/src/components/UserLanguageSelector.d.ts.map +0 -1
- package/src/components/UserLanguageSelector.js +0 -31
- package/src/components/UserMenu.d.ts +0 -4
- package/src/components/UserMenu.d.ts.map +0 -1
- package/src/components/UserMenu.js +0 -12
- package/src/components/UserSettingsForm.d.ts +0 -56
- package/src/components/UserSettingsForm.d.ts.map +0 -1
- package/src/components/UserSettingsForm.js +0 -93
- package/src/components/VerifyEmailPage.d.ts +0 -23
- package/src/components/VerifyEmailPage.d.ts.map +0 -1
- package/src/components/VerifyEmailPage.js +0 -61
- package/src/components/index.d.ts.map +0 -1
- package/src/components/index.js +0 -28
- package/src/contexts/AuthProvider.d.ts +0 -152
- package/src/contexts/AuthProvider.d.ts.map +0 -1
- package/src/contexts/AuthProvider.js +0 -446
- package/src/contexts/I18nProvider.d.ts +0 -16
- package/src/contexts/I18nProvider.d.ts.map +0 -1
- package/src/contexts/I18nProvider.js +0 -46
- package/src/contexts/MenuContext.d.ts +0 -20
- package/src/contexts/MenuContext.d.ts.map +0 -1
- package/src/contexts/MenuContext.js +0 -244
- package/src/contexts/SuiteConfigProvider.d.ts +0 -44
- package/src/contexts/SuiteConfigProvider.d.ts.map +0 -1
- package/src/contexts/SuiteConfigProvider.js +0 -43
- package/src/contexts/ThemeProvider.d.ts +0 -15
- package/src/contexts/ThemeProvider.d.ts.map +0 -1
- package/src/contexts/ThemeProvider.js +0 -36
- package/src/contexts/index.d.ts.map +0 -1
- package/src/contexts/index.js +0 -8
- package/src/hooks/index.d.ts.map +0 -1
- package/src/hooks/index.js +0 -8
- package/src/hooks/useBackupCodes.d.ts +0 -15
- package/src/hooks/useBackupCodes.d.ts.map +0 -1
- package/src/hooks/useBackupCodes.js +0 -70
- package/src/hooks/useEmailVerification.d.ts +0 -10
- package/src/hooks/useEmailVerification.d.ts.map +0 -1
- package/src/hooks/useEmailVerification.js +0 -36
- package/src/hooks/useExpiringValue.d.ts +0 -14
- package/src/hooks/useExpiringValue.d.ts.map +0 -1
- package/src/hooks/useExpiringValue.js +0 -53
- package/src/hooks/useLocalStorage.d.ts +0 -2
- package/src/hooks/useLocalStorage.d.ts.map +0 -1
- package/src/hooks/useLocalStorage.js +0 -15
- package/src/hooks/useUserSettings.d.ts +0 -46
- package/src/hooks/useUserSettings.d.ts.map +0 -1
- package/src/hooks/useUserSettings.js +0 -152
- package/src/index.d.ts.map +0 -1
- package/src/index.js +0 -12
- package/src/interfaces/IAppConfig.d.ts +0 -6
- package/src/interfaces/IAppConfig.d.ts.map +0 -1
- package/src/interfaces/IAppConfig.js +0 -2
- package/src/interfaces/IMenuConfig.d.ts +0 -11
- package/src/interfaces/IMenuConfig.d.ts.map +0 -1
- package/src/interfaces/IMenuConfig.js +0 -2
- package/src/interfaces/IMenuOption.d.ts +0 -58
- package/src/interfaces/IMenuOption.d.ts.map +0 -1
- package/src/interfaces/IMenuOption.js +0 -2
- package/src/interfaces/index.d.ts +0 -4
- package/src/interfaces/index.d.ts.map +0 -1
- package/src/interfaces/index.js +0 -6
- package/src/services/__mocks__/authService.d.ts +0 -21
- package/src/services/__mocks__/authService.d.ts.map +0 -1
- package/src/services/__mocks__/authService.js +0 -15
- package/src/services/api.d.ts +0 -3
- package/src/services/api.d.ts.map +0 -1
- package/src/services/api.js +0 -14
- package/src/services/authService.d.ts +0 -72
- package/src/services/authService.d.ts.map +0 -1
- package/src/services/authService.js +0 -347
- package/src/services/authenticatedApi.d.ts +0 -3
- package/src/services/authenticatedApi.d.ts.map +0 -1
- package/src/services/authenticatedApi.js +0 -18
- package/src/services/index.d.ts +0 -4
- package/src/services/index.d.ts.map +0 -1
- package/src/services/index.js +0 -6
- package/src/types/MenuType.d.ts +0 -11
- package/src/types/MenuType.d.ts.map +0 -1
- package/src/types/MenuType.js +0 -12
- package/src/types/expirationSeconds.d.ts +0 -3
- package/src/types/expirationSeconds.d.ts.map +0 -1
- package/src/types/expirationSeconds.js +0 -17
- package/src/types/index.d.ts +0 -2
- package/src/types/index.d.ts.map +0 -1
- package/src/types/index.js +0 -4
- package/src/types/translation.d.ts +0 -10
- package/src/types/translation.d.ts.map +0 -1
- package/src/types/translation.js +0 -9
- package/src/wrappers/BackupCodeLoginWrapper.d.ts +0 -8
- package/src/wrappers/BackupCodeLoginWrapper.d.ts.map +0 -1
- package/src/wrappers/BackupCodeLoginWrapper.js +0 -21
- package/src/wrappers/BackupCodesWrapper.d.ts +0 -7
- package/src/wrappers/BackupCodesWrapper.d.ts.map +0 -1
- package/src/wrappers/BackupCodesWrapper.js +0 -17
- package/src/wrappers/ChangePasswordFormWrapper.d.ts +0 -8
- package/src/wrappers/ChangePasswordFormWrapper.d.ts.map +0 -1
- package/src/wrappers/ChangePasswordFormWrapper.js +0 -21
- package/src/wrappers/LoginFormWrapper.d.ts +0 -9
- package/src/wrappers/LoginFormWrapper.d.ts.map +0 -1
- package/src/wrappers/LoginFormWrapper.js +0 -43
- package/src/wrappers/LogoutPageWrapper.d.ts +0 -9
- package/src/wrappers/LogoutPageWrapper.d.ts.map +0 -1
- package/src/wrappers/LogoutPageWrapper.js +0 -21
- package/src/wrappers/RegisterFormWrapper.d.ts +0 -9
- package/src/wrappers/RegisterFormWrapper.d.ts.map +0 -1
- package/src/wrappers/RegisterFormWrapper.js +0 -26
- package/src/wrappers/UserSettingsFormWrapper.d.ts +0 -8
- package/src/wrappers/UserSettingsFormWrapper.d.ts.map +0 -1
- package/src/wrappers/UserSettingsFormWrapper.js +0 -24
- package/src/wrappers/VerifyEmailPageWrapper.d.ts +0 -8
- package/src/wrappers/VerifyEmailPageWrapper.d.ts.map +0 -1
- package/src/wrappers/VerifyEmailPageWrapper.js +0 -20
- package/src/wrappers/index.d.ts.map +0 -1
- 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
|
+
});
|