diff --git a/components/AccountInformation.test.tsx b/components/AccountInformation.test.tsx new file mode 100644 index 00000000..2d4ad0db --- /dev/null +++ b/components/AccountInformation.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {AccountInformation} from './AccountInformation'; + +describe('AccountInformation Component', () => { + const defaultProps = { + email: 'test@example.com', + picture: 'https://example.com/avatar.jpg', + }; + + it('should match snapshot with email and picture', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different email', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different picture URL', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with long email', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/ActivityLogEvent.test.ts b/components/ActivityLogEvent.test.ts index 9fd2f71d..bb050fc6 100644 --- a/components/ActivityLogEvent.test.ts +++ b/components/ActivityLogEvent.test.ts @@ -19,7 +19,7 @@ describe('ActivityLog', () => { describe('getActionText', () => { let activityLog; let mockIl18nfn; - let wellknown = { + const wellknown = { credential_configurations_supported: { mockId: { display: [ @@ -67,6 +67,7 @@ describe('getActionText', () => { activityLog.getActionText(mockIl18nfn, wellknown); expect(mockIl18nfn).toHaveBeenCalledWith('mockType', { idType: 'fake VC', + vcStatus: '', }); expect(mockIl18nfn).toHaveBeenCalledTimes(1); // TODO: assert the returned string @@ -81,3 +82,79 @@ describe('getActionText', () => { expect(mockIl18nfn).toHaveBeenCalledTimes(1); }); }); + +describe('VCActivityLog.getLogFromObject', () => { + it('should create VCActivityLog instance from object', () => { + const mockData = { + id: 'test-id', + type: 'VC_ADDED', + timestamp: 1234567890, + deviceName: 'Test Device', + }; + + const log = VCActivityLog.getLogFromObject(mockData); + + expect(log).toBeInstanceOf(VCActivityLog); + expect(log.id).toBe('test-id'); + expect(log.type).toBe('VC_ADDED'); + expect(log.timestamp).toBe(1234567890); + expect(log.deviceName).toBe('Test Device'); + }); + + it('should create VCActivityLog from empty object', () => { + const log = VCActivityLog.getLogFromObject({}); + + expect(log).toBeInstanceOf(VCActivityLog); + expect(log.timestamp).toBeDefined(); + }); +}); + +describe('VCActivityLog.getActionLabel', () => { + it('should return formatted action label with device name and time', () => { + const mockLog = new VCActivityLog({ + deviceName: 'iPhone 12', + timestamp: Date.now() - 60000, // 1 minute ago + }); + + const label = mockLog.getActionLabel('en'); + + expect(label).toContain('iPhone 12'); + expect(label).toContain('·'); + expect(label).toContain('ago'); + }); + + it('should return only time when device name is empty', () => { + const mockLog = new VCActivityLog({ + deviceName: '', + timestamp: Date.now() - 120000, // 2 minutes ago + }); + + const label = mockLog.getActionLabel('en'); + + expect(label).not.toContain('·'); + expect(label).toContain('ago'); + }); + + it('should filter out empty labels', () => { + const mockLog = new VCActivityLog({ + deviceName: ' ', // whitespace only + timestamp: Date.now(), + }); + + const label = mockLog.getActionLabel('en'); + + expect(label).not.toContain('·'); + expect(label).toBeTruthy(); + }); + + it('should format time with device name in English locale', () => { + const mockLog = new VCActivityLog({ + deviceName: 'Test Device', + timestamp: Date.now() - 300000, // 5 minutes ago + }); + + const labelEn = mockLog.getActionLabel('en'); + expect(labelEn).toBeTruthy(); + expect(labelEn).toContain('Test Device'); + }); +}); diff --git a/components/ActivityLogText.test.tsx b/components/ActivityLogText.test.tsx new file mode 100644 index 00000000..612a41fd --- /dev/null +++ b/components/ActivityLogText.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {ActivityLogText} from './ActivityLogText'; +import {VCItemContainerFlowType} from '../shared/Utils'; +import {VCActivityLog} from './ActivityLogEvent'; +import {VPShareActivityLog} from './VPShareActivityLogEvent'; + +// Mock TextItem +jest.mock('./ui/TextItem', () => ({ + TextItem: jest.fn(() => null), +})); + +// Mock HistoryScreenController +jest.mock('../screens/History/HistoryScreenController', () => ({ + useHistoryTab: jest.fn(() => ({ + getWellKnownIssuerMap: jest.fn(() => ({display: [{name: 'Test Issuer'}]})), + })), +})); + +// Mock ActivityLogEvent +jest.mock('./ActivityLogEvent', () => ({ + VCActivityLog: { + getLogFromObject: jest.fn(obj => ({ + ...obj, + getActionLabel: jest.fn(() => 'Shared'), + getActionText: jest.fn(() => 'Shared with Test Device'), + })), + }, +})); + +// Mock VPShareActivityLogEvent +jest.mock('./VPShareActivityLogEvent', () => ({ + VPShareActivityLog: { + getLogFromObject: jest.fn(obj => ({ + ...obj, + getActionLabel: jest.fn(() => 'Verified'), + getActionText: jest.fn(() => 'Verified by Test Device'), + })), + }, +})); + +describe('ActivityLogText Component', () => { + const mockActivity = { + vcLabel: 'Test VC', + timestamp: new Date().toISOString(), + deviceName: 'Test Device', + vcIdType: 'NationalID', + flow: VCItemContainerFlowType.VC_SHARE, + issuer: 'test-issuer', + } as unknown as VCActivityLog; + + it('should match snapshot with VC activity', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with VP activity', () => { + const vpActivity = { + ...mockActivity, + flow: VCItemContainerFlowType.VP_SHARE, + } as unknown as VPShareActivityLog; + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/BackupAndRestoreBannerNotification.test.tsx b/components/BackupAndRestoreBannerNotification.test.tsx new file mode 100644 index 00000000..559232fa --- /dev/null +++ b/components/BackupAndRestoreBannerNotification.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {BackupAndRestoreBannerNotification} from './BackupAndRestoreBannerNotification'; + +// Mock controllers +jest.mock('../screens/backupAndRestore/BackupController', () => ({ + useBackupScreen: jest.fn(() => ({ + showBackupInProgress: false, + isBackingUpSuccess: false, + isBackingUpFailure: false, + backupErrorReason: '', + DISMISS: jest.fn(), + DISMISS_SHOW_BACKUP_IN_PROGRESS: jest.fn(), + })), +})); + +jest.mock('../screens/Settings/BackupRestoreController', () => ({ + useBackupRestoreScreen: jest.fn(() => ({ + showRestoreInProgress: false, + isBackUpRestoreSuccess: false, + isBackUpRestoreFailure: false, + restoreErrorReason: '', + DISMISS: jest.fn(), + DISMISS_SHOW_RESTORE_IN_PROGRESS: jest.fn(), + })), +})); + +// Mock BannerNotification +jest.mock('./BannerNotification', () => ({ + BannerNotification: jest.fn(() => null), + BannerStatusType: { + IN_PROGRESS: 'inProgress', + SUCCESS: 'success', + ERROR: 'error', + }, +})); + +describe('BackupAndRestoreBannerNotification Component', () => { + it('should match snapshot with no banners', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/BannerNotification.test.tsx b/components/BannerNotification.test.tsx new file mode 100644 index 00000000..4c7c2acf --- /dev/null +++ b/components/BannerNotification.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {BannerNotification, BannerStatusType} from './BannerNotification'; + +describe('BannerNotification Component', () => { + const defaultProps = { + message: 'Test notification message', + onClosePress: jest.fn(), + testId: 'bannerTest', + type: BannerStatusType.SUCCESS, + }; + + it('should match snapshot with success status', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with error status', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with in progress status', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with long message', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different testId', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/BannerNotificationContainer.test.tsx b/components/BannerNotificationContainer.test.tsx new file mode 100644 index 00000000..79724068 --- /dev/null +++ b/components/BannerNotificationContainer.test.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {BannerNotificationContainer} from './BannerNotificationContainer'; + +// Mock all controllers +jest.mock('./BannerNotificationController', () => ({ + UseBannerNotification: jest.fn(() => ({ + isBindingSuccess: false, + verificationStatus: null, + isPasscodeUnlock: false, + isBiometricUnlock: false, + isDownloadingFailed: false, + isDownloadingSuccess: false, + isReverificationSuccess: {status: false}, + isReverificationFailed: {status: false}, + RESET_WALLET_BINDING_SUCCESS: jest.fn(), + RESET_VERIFICATION_STATUS: jest.fn(), + RESET_DOWNLOADING_FAILED: jest.fn(), + RESET_DOWNLOADING_SUCCESS: jest.fn(), + RESET_REVIRIFICATION_SUCCESS: jest.fn(), + RESET_REVERIFICATION_FAILURE: jest.fn(), + DISMISS: jest.fn(), + })), +})); + +jest.mock('../screens/Scan/ScanScreenController', () => ({ + useScanScreen: jest.fn(() => ({ + showQuickShareSuccessBanner: false, + DISMISS_QUICK_SHARE_BANNER: jest.fn(), + })), +})); + +jest.mock('../screens/Settings/SettingScreenController', () => ({ + useSettingsScreen: jest.fn(() => ({ + isKeyOrderSet: null, + RESET_KEY_ORDER_RESPONSE: jest.fn(), + })), +})); + +jest.mock('./BackupAndRestoreBannerNotification', () => ({ + BackupAndRestoreBannerNotification: jest.fn(() => null), +})); + +jest.mock('./BannerNotification', () => ({ + BannerNotification: jest.fn(() => null), + BannerStatusType: { + IN_PROGRESS: 'inProgress', + SUCCESS: 'success', + ERROR: 'error', + }, +})); + +describe('BannerNotificationContainer Component', () => { + it('should match snapshot with no banners visible', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with verification banner enabled', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with verification banner disabled', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/CopilotTooltip.test.tsx b/components/CopilotTooltip.test.tsx new file mode 100644 index 00000000..6ccb3a10 --- /dev/null +++ b/components/CopilotTooltip.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {CopilotTooltip} from './CopilotTooltip'; + +// Mock ui components +jest.mock('./ui', () => ({ + Button: jest.fn(() => null), + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Row: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +// Mock controller +jest.mock('./CopilotTooltipController', () => ({ + UseCopilotTooltip: jest.fn(() => ({ + copilotEvents: { + on: jest.fn(), + }, + SET_TOUR_GUIDE: jest.fn(), + ONBOARDING_DONE: jest.fn(), + INITIAL_DOWNLOAD_DONE: jest.fn(), + CURRENT_STEP: 1, + currentStepTitle: 'Step 1 Title', + currentStepDescription: 'Step 1 Description', + titleTestID: 'stepTitle', + descriptionTestID: 'stepDescription', + stepCount: '1/5', + isFirstStep: true, + isLastStep: false, + isFinalStep: false, + isOnboarding: true, + isInitialDownloading: false, + goToPrev: jest.fn(), + goToNext: jest.fn(), + stop: jest.fn(), + })), +})); + +// Mock settings controller +jest.mock('../screens/Settings/SettingScreenController', () => ({ + useSettingsScreen: jest.fn(() => ({ + BACK: jest.fn(), + })), +})); + +describe('CopilotTooltip Component', () => { + it('should match snapshot with first step', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/CopyButton.test.tsx b/components/CopyButton.test.tsx new file mode 100644 index 00000000..a263a723 --- /dev/null +++ b/components/CopyButton.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {CopyButton} from './CopyButton'; + +// Mock Clipboard +jest.mock('@react-native-clipboard/clipboard', () => ({ + setString: jest.fn(), +})); + +// Mock SvgImage +jest.mock('./ui/svg', () => ({ + SvgImage: { + copyIcon: jest.fn(() => null), + }, +})); + +describe('CopyButton Component', () => { + const defaultProps = { + content: 'Test content to copy', + }; + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with long content', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with special characters', () => { + const specialContent = 'Special: @#$%^&*(){}[]'; + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/DeviceInfoList.test.tsx b/components/DeviceInfoList.test.tsx new file mode 100644 index 00000000..f27db219 --- /dev/null +++ b/components/DeviceInfoList.test.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {DeviceInfoList, DeviceInfo} from './DeviceInfoList'; + +describe('DeviceInfoList Component', () => { + const mockDeviceInfo: DeviceInfo = { + deviceName: 'Samsung Galaxy S21', + name: 'John Doe', + deviceId: 'device123', + }; + + it('should render DeviceInfoList component', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should render with receiver mode', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render with sender mode', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render without of prop', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should handle different device names', () => { + const deviceNames = [ + 'iPhone 14 Pro', + 'Google Pixel 7', + 'OnePlus 11', + 'Samsung Galaxy S23', + ]; + + deviceNames.forEach(deviceName => { + const deviceInfo = {...mockDeviceInfo, deviceName}; + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + }); + + it('should handle different user names', () => { + const names = ['Alice Smith', 'Bob Johnson', 'Charlie Brown']; + + names.forEach(name => { + const deviceInfo = {...mockDeviceInfo, name}; + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + }); + + it('should handle different device IDs', () => { + const deviceIds = ['device001', 'device002', 'device003']; + + deviceIds.forEach(deviceId => { + const deviceInfo = {...mockDeviceInfo, deviceId}; + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + }); + + it('should handle empty device name', () => { + const deviceInfo = {...mockDeviceInfo, deviceName: ''}; + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should handle long device names', () => { + const deviceInfo = { + ...mockDeviceInfo, + deviceName: 'Very Long Device Name With Many Characters', + }; + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); +}); diff --git a/components/DropdownIcon.test.tsx b/components/DropdownIcon.test.tsx new file mode 100644 index 00000000..c08d6560 --- /dev/null +++ b/components/DropdownIcon.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {DropdownIcon} from './DropdownIcon'; + +// Mock Popable +jest.mock('react-native-popable', () => ({ + Popable: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +describe('DropdownIcon Component', () => { + const mockItems = [ + {label: 'Item 1', onPress: jest.fn(), icon: 'account'}, + {label: 'Item 2', onPress: jest.fn(), icon: 'settings'}, + {label: 'Item 3', onPress: jest.fn(), icon: 'delete', idType: 'type1'}, + ]; + + const defaultProps = { + idType: 'type1', + icon: 'dots-vertical', + items: mockItems, + }; + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different icon', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with empty items', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with multiple items', () => { + const manyItems = [ + {label: 'Action 1', onPress: jest.fn(), icon: 'edit'}, + {label: 'Action 2', onPress: jest.fn(), icon: 'share'}, + {label: 'Action 3', onPress: jest.fn(), icon: 'download'}, + {label: 'Action 4', onPress: jest.fn(), icon: 'upload'}, + ]; + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/DualMessageOverlay.test.tsx b/components/DualMessageOverlay.test.tsx new file mode 100644 index 00000000..7d200649 --- /dev/null +++ b/components/DualMessageOverlay.test.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {DualMessageOverlay} from './DualMessageOverlay'; +import {Text} from 'react-native'; + +// Mock react-native-elements +jest.mock('react-native-elements', () => ({ + Overlay: ({children}: {children: React.ReactNode}) => <>{children}, + Button: jest.fn(({title}) => <>{title}), +})); + +// Mock ui components +jest.mock('./ui', () => ({ + Button: jest.fn( + ({title, children}: {title?: string; children?: React.ReactNode}) => ( + <>{title || children} + ), + ), + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Row: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +describe('DualMessageOverlay Component', () => { + const defaultProps = { + isVisible: true, + title: 'Confirm Action', + message: 'Are you sure you want to proceed?', + }; + + it('should match snapshot with title and message', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with both buttons', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with only try again button', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with only ignore button', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with hint text', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom height', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with children', () => { + const {toJSON} = render( + + Custom content here + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/EditableListItem.test.tsx b/components/EditableListItem.test.tsx new file mode 100644 index 00000000..4536bd9e --- /dev/null +++ b/components/EditableListItem.test.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {EditableListItem} from './EditableListItem'; + +// Mock SvgImage +jest.mock('./ui/svg', () => ({ + SvgImage: { + starIcon: jest.fn(() => null), + }, +})); + +// Mock react-native-elements +jest.mock('react-native-elements', () => ({ + Icon: jest.fn(() => null), + ListItem: ({children}: {children: React.ReactNode}) => <>{children}, + Overlay: ({children}: {children: React.ReactNode}) => <>{children}, + Input: jest.fn(() => null), +})); + +// Mock ui components +jest.mock('./ui', () => ({ + Button: jest.fn(() => null), + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Row: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +// Add mock for ListItem.Content and ListItem.Title +const ListItem = require('react-native-elements').ListItem; +ListItem.Content = ({children}: {children: React.ReactNode}) => <>{children}; +ListItem.Title = ({children}: {children: React.ReactNode}) => <>{children}; + +describe('EditableListItem Component', () => { + const mockItems = [ + {label: 'Email', value: 'test@example.com', testID: 'emailItem'}, + {label: 'Phone', value: '1234567890', testID: 'phoneItem'}, + ]; + + const defaultProps = { + testID: 'editableItem', + title: 'Contact Information', + content: 'Edit your details', + items: mockItems, + Icon: 'edit', + onEdit: jest.fn(), + onCancel: jest.fn(), + titleColor: '#000000', + }; + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom title color', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with progress indicator', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with error state', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with success response', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with single item', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/GlobalContextProvider.test.tsx b/components/GlobalContextProvider.test.tsx new file mode 100644 index 00000000..eac28774 --- /dev/null +++ b/components/GlobalContextProvider.test.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {GlobalContextProvider} from './GlobalContextProvider'; +import {Text} from 'react-native'; + +// Mock xstate +jest.mock('@xstate/react', () => ({ + useInterpret: jest.fn(() => ({ + subscribe: jest.fn(), + })), +})); + +// Mock appMachine +jest.mock('../machines/app', () => ({ + appMachine: {}, +})); + +// Mock GlobalContext +jest.mock('../shared/GlobalContext', () => ({ + GlobalContext: { + Provider: ({children}: {children: React.ReactNode}) => <>{children}, + }, +})); + +// Mock commonUtil +jest.mock('../shared/commonUtil', () => ({ + logState: jest.fn(), +})); + +describe('GlobalContextProvider Component', () => { + it('should match snapshot with children', () => { + const {toJSON} = render( + + Test Child + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with multiple children', () => { + const {toJSON} = render( + + Child 1 + Child 2 + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/HelpScreen.test.tsx b/components/HelpScreen.test.tsx new file mode 100644 index 00000000..102db38a --- /dev/null +++ b/components/HelpScreen.test.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {HelpScreen} from './HelpScreen'; +import {Text} from 'react-native'; + +// Mock Modal +jest.mock('./ui/Modal', () => ({ + Modal: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +// Mock BannerNotificationContainer +jest.mock('./BannerNotificationContainer', () => ({ + BannerNotificationContainer: jest.fn(() => null), +})); + +// Mock API +jest.mock('../shared/api', () => ({ + __esModule: true, + default: jest.fn(() => + Promise.resolve({ + aboutInjiUrl: 'https://docs.inji.io', + }), + ), +})); + +describe('HelpScreen Component', () => { + const triggerComponent = Help; + + it('should match snapshot with Inji source', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with BackUp source', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with keyManagement source', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot when disabled', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/KebabPopUp.test.tsx b/components/KebabPopUp.test.tsx new file mode 100644 index 00000000..5153df4d --- /dev/null +++ b/components/KebabPopUp.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {KebabPopUp} from './KebabPopUp'; +import {Text} from 'react-native'; + +// Mock controller +jest.mock('./KebabPopUpController', () => ({ + useKebabPopUp: jest.fn(() => ({ + isScanning: false, + })), +})); + +// Mock kebabMenuUtils +jest.mock('./kebabMenuUtils', () => ({ + getKebabMenuOptions: jest.fn(() => [ + { + testID: 'pinCard', + label: 'Pin Card', + onPress: jest.fn(), + icon: null, + }, + { + testID: 'removeFromWallet', + label: 'Remove', + onPress: jest.fn(), + icon: null, + }, + ]), +})); + +// Mock react-native-elements +jest.mock('react-native-elements', () => ({ + Icon: jest.fn(() => null), + ListItem: ({children}: {children: React.ReactNode}) => <>{children}, + Overlay: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +// Mock FlatList +jest.mock('react-native-gesture-handler', () => ({ + FlatList: ({renderItem, data}: any) => ( + <>{data.map((item: any, index: number) => renderItem({item, index}))} + ), +})); + +describe('KebabPopUp Component', () => { + const mockService = {} as any; + const mockVcMetadata = { + id: 'test-vc', + vcLabel: 'Test VC', + }; + + const defaultProps = { + iconName: 'ellipsis-vertical', + vcMetadata: mockVcMetadata as any, + isVisible: true, + onDismiss: jest.fn(), + service: mockService, + vcHasImage: false, + }; + + it('should match snapshot with default icon', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom icon component', () => { + const CustomIcon = Custom; + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom icon color', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot when not visible', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with VC that has image', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/LanguageSelector.test.tsx b/components/LanguageSelector.test.tsx new file mode 100644 index 00000000..ed165e40 --- /dev/null +++ b/components/LanguageSelector.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {LanguageSelector} from './LanguageSelector'; +import {Text} from 'react-native'; + +// Mock dependencies +jest.mock('react-native-restart', () => ({ + Restart: jest.fn(), +})); + +jest.mock('./ui/Picker', () => ({ + Picker: jest.fn(() => null), +})); + +describe('LanguageSelector Component', () => { + const defaultTrigger = Select Language; + + it('should match snapshot with default trigger', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom trigger component', () => { + const customTrigger = Choose Language; + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/Logo.test.tsx b/components/Logo.test.tsx new file mode 100644 index 00000000..d26728c5 --- /dev/null +++ b/components/Logo.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {Logo} from './Logo'; + +describe('Logo Component', () => { + it('should render Logo component', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should render Logo with width and height props', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should render Logo with string width and height', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); +}); diff --git a/components/Message.test.tsx b/components/Message.test.tsx new file mode 100644 index 00000000..a856bc53 --- /dev/null +++ b/components/Message.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {Message} from './Message'; + +// Mock LinearProgress +jest.mock('react-native-elements', () => ({ + LinearProgress: jest.fn(() => null), +})); + +// Mock Button from ui +jest.mock('./ui', () => ({ + Button: jest.fn(() => null), + Centered: ({children}: {children: React.ReactNode}) => <>{children}, + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +describe('Message Component', () => { + it('should match snapshot with title only', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with message only', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with title and message', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with hint text', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with cancel button', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/MessageOverlay.test.tsx b/components/MessageOverlay.test.tsx new file mode 100644 index 00000000..09e24aaf --- /dev/null +++ b/components/MessageOverlay.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {MessageOverlay, ErrorMessageOverlay} from './MessageOverlay'; +import {Text} from 'react-native'; + +// Mock react-native-elements +jest.mock('react-native-elements', () => ({ + Overlay: ({children}: {children: React.ReactNode}) => <>{children}, + LinearProgress: jest.fn(() => null), +})); + +// Mock ui components +jest.mock('./ui', () => ({ + Button: jest.fn(() => null), + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +describe('MessageOverlay Component', () => { + const defaultProps = { + isVisible: true, + title: 'Test Title', + message: 'Test Message', + }; + + it('should match snapshot with title and message', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with progress indicator', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with numeric progress', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with button', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom children', () => { + const {toJSON} = render( + + Custom Content + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with hint text', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom minHeight', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); + +describe('ErrorMessageOverlay Component', () => { + const errorProps = { + isVisible: true, + error: 'network', + translationPath: 'errors', + onDismiss: jest.fn(), + }; + + it('should match snapshot with error', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with testID', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/Passcode.test.tsx b/components/Passcode.test.tsx new file mode 100644 index 00000000..b73eb9db --- /dev/null +++ b/components/Passcode.test.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {Passcode} from './Passcode'; + +// Mock PasscodeVerify +jest.mock('./PasscodeVerify', () => ({ + PasscodeVerify: jest.fn(() => null), +})); + +// Mock telemetry +jest.mock('../shared/telemetry/TelemetryUtils', () => ({ + getImpressionEventData: jest.fn(), + sendImpressionEvent: jest.fn(), +})); + +describe('Passcode Component', () => { + const defaultProps = { + error: '', + storedPasscode: 'hashed-passcode', + salt: 'salt-value', + onSuccess: jest.fn(), + onError: jest.fn(), + onDismiss: jest.fn(), + }; + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom message', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with error message', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with both message and error', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/PasscodeVerify.test.tsx b/components/PasscodeVerify.test.tsx new file mode 100644 index 00000000..51b56c3b --- /dev/null +++ b/components/PasscodeVerify.test.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {PasscodeVerify} from './PasscodeVerify'; + +// Mock PinInput +jest.mock('./PinInput', () => ({ + PinInput: jest.fn(() => null), +})); + +// Mock commonUtil +jest.mock('../shared/commonUtil', () => ({ + hashData: jest.fn(() => Promise.resolve('hashed-value')), +})); + +// Mock telemetry +jest.mock('../shared/telemetry/TelemetryUtils', () => ({ + getErrorEventData: jest.fn(), + sendErrorEvent: jest.fn(), +})); + +describe('PasscodeVerify Component', () => { + const defaultProps = { + passcode: 'stored-hashed-passcode', + onSuccess: jest.fn(), + onError: jest.fn(), + salt: 'test-salt', + testID: 'passcodeVerify', + }; + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different testID', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot without onError handler', () => { + const {passcode, onSuccess, salt, testID} = defaultProps; + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/PendingIcon.test.tsx b/components/PendingIcon.test.tsx new file mode 100644 index 00000000..5eccf0fc --- /dev/null +++ b/components/PendingIcon.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import PendingIcon from './PendingIcon'; + +describe('PendingIcon', () => { + it('should render PendingIcon component', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should match snapshot', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should render with custom color', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/PinInput.test.tsx b/components/PinInput.test.tsx new file mode 100644 index 00000000..63c38d40 --- /dev/null +++ b/components/PinInput.test.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {PinInput} from './PinInput'; + +// Mock usePinInput +jest.mock('../machines/pinInput', () => ({ + usePinInput: jest.fn(length => ({ + state: { + context: { + inputRefs: Array(length).fill({current: null}), + values: Array(length).fill(''), + }, + }, + send: jest.fn(), + events: { + UPDATE_INPUT: jest.fn((value, index) => ({ + type: 'UPDATE_INPUT', + value, + index, + })), + FOCUS_INPUT: jest.fn(index => ({type: 'FOCUS_INPUT', index})), + KEY_PRESS: jest.fn(key => ({type: 'KEY_PRESS', key})), + }, + })), +})); + +describe('PinInput Component', () => { + it('should match snapshot with 4 digit PIN', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with 6 digit PIN', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with onChange handler', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with onDone and autosubmit', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom testID', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/ProfileIcon.test.tsx b/components/ProfileIcon.test.tsx new file mode 100644 index 00000000..54f93509 --- /dev/null +++ b/components/ProfileIcon.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {ProfileIcon} from './ProfileIcon'; +import {View} from 'react-native'; + +// Mock SvgImage +jest.mock('./ui/svg', () => ({ + SvgImage: { + pinIcon: jest.fn(() => ), + }, +})); + +describe('ProfileIcon Component', () => { + const defaultProps = { + profileIconContainerStyles: {}, + profileIconSize: 40, + }; + + it('should match snapshot without pinned icon', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with pinned icon', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom icon size', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom container styles', () => { + const customStyles = { + backgroundColor: 'blue', + borderRadius: 10, + padding: 5, + }; + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/ProgressingModal.test.tsx b/components/ProgressingModal.test.tsx new file mode 100644 index 00000000..cb9c2c58 --- /dev/null +++ b/components/ProgressingModal.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {ProgressingModal} from './ProgressingModal'; + +// Mock Modal +jest.mock('./ui/Modal', () => ({ + Modal: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +// Mock Spinner +jest.mock('react-native-spinkit', () => 'Spinner'); + +// Mock SvgImage +jest.mock('./ui/svg', () => ({ + SvgImage: { + ProgressIcon: jest.fn(() => null), + }, +})); + +// Mock ui components +jest.mock('./ui', () => ({ + Button: jest.fn(() => null), + Centered: ({children}: {children: React.ReactNode}) => <>{children}, + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +describe('ProgressingModal Component', () => { + const defaultProps = { + isVisible: true, + isHintVisible: false, + title: 'Processing', + }; + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with progress spinner', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with hint visible', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with retry button', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with stay in progress button', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with BLE error visible', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot as requester', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/QrCodeOverlay.test.tsx b/components/QrCodeOverlay.test.tsx new file mode 100644 index 00000000..e05fddb8 --- /dev/null +++ b/components/QrCodeOverlay.test.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {QrCodeOverlay} from './QrCodeOverlay'; +import {NativeModules} from 'react-native'; + +// Mock QRCode +jest.mock('react-native-qrcode-svg', () => 'QRCode'); + +// Mock SvgImage +jest.mock('./ui/svg', () => ({ + SvgImage: { + MagnifierZoom: jest.fn(() => null), + }, +})); + +// Mock sharing utils +jest.mock('../shared/sharing/imageUtils', () => ({ + shareImageToAllSupportedApps: jest.fn(() => Promise.resolve(true)), +})); + +describe('QrCodeOverlay Component', () => { + // Setup mocks for native modules + beforeAll(() => { + // Mock RNSecureKeystoreModule methods + NativeModules.RNSecureKeystoreModule.getData = jest.fn(() => + Promise.resolve(['key', 'mocked-qr-data']), + ); + NativeModules.RNSecureKeystoreModule.storeData = jest.fn(() => + Promise.resolve(), + ); + + // Mock RNPixelpassModule + NativeModules.RNPixelpassModule = { + generateQRData: jest.fn(() => Promise.resolve('mocked-qr-data')), + }; + }); + + // Silence console warnings during tests + beforeAll(() => { + jest.spyOn(console, 'warn').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + const mockVC = { + credential: {id: 'test-credential'}, + generatedOn: new Date().toISOString(), + }; + + const mockMeta = { + id: 'test-vc-id', + vcLabel: 'Test VC', + }; + + const defaultProps = { + verifiableCredential: mockVC as any, + meta: mockMeta as any, + }; + + // NOTE: CodeRabbit suggested making these tests async to wait for QR data loading. + // However, the component requires native module mocks (RNSecureKeystoreModule.getData) + // that are not properly initialized in the test environment, causing the component + // to always return null. These tests currently capture empty snapshots. + // TODO: Fix native module mocking to properly test the async QR data loading behavior. + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with inline QR disabled', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with force visible', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with onClose handler', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/QrScanner.test.tsx b/components/QrScanner.test.tsx new file mode 100644 index 00000000..8b62d5d4 --- /dev/null +++ b/components/QrScanner.test.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {QrScanner} from './QrScanner'; + +// Mock useContext +const mockUseContext = jest.fn(); +jest.spyOn(React, 'useContext').mockImplementation(mockUseContext); + +// Mock GlobalContext +jest.mock('../shared/GlobalContext', () => ({ + GlobalContext: {}, +})); + +// Mock xstate with a mutable mock function +const mockUseSelector = jest.fn(); +jest.mock('@xstate/react', () => ({ + useSelector: jest.fn((...args) => mockUseSelector(...args)), +})); + +// Before each test, set up the context mock +beforeEach(() => { + mockUseContext.mockReturnValue({ + appService: {send: jest.fn()}, + }); + mockUseSelector.mockReturnValue(true); +}); + +// Mock app machine +jest.mock('../machines/app', () => ({ + selectIsActive: jest.fn(), +})); + +// Mock SvgImage +jest.mock('./ui/svg', () => ({ + SvgImage: { + FlipCameraIcon: jest.fn(() => null), + }, +})); + +// Mock ui components +jest.mock('./ui', () => ({ + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Row: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +// Mock expo-camera +jest.mock('expo-camera', () => ({ + CameraView: jest.fn(() => null), + useCameraPermissions: jest.fn(() => [ + null, + jest.fn(() => Promise.resolve({granted: true})), + jest.fn(() => Promise.resolve({status: 'granted'})), + ]), + PermissionStatus: { + UNDETERMINED: 'undetermined', + GRANTED: 'granted', + DENIED: 'denied', + }, + CameraType: { + BACK: 'back', + FRONT: 'front', + }, +})); + +describe('QrScanner Component', () => { + const defaultProps = { + onQrFound: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with title', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom title', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should render with onQrFound callback', () => { + const onQrFound = jest.fn(); + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + expect(onQrFound).not.toHaveBeenCalled(); // Callback not called until QR is scanned + }); + + it('should render when isActive is false', () => { + mockUseSelector.mockReturnValue(false); + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); +}); diff --git a/components/RotatingIcon.test.tsx b/components/RotatingIcon.test.tsx new file mode 100644 index 00000000..0539c671 --- /dev/null +++ b/components/RotatingIcon.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {RotatingIcon} from './RotatingIcon'; + +describe('RotatingIcon Component', () => { + it('should render RotatingIcon component', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render with clockwise rotation', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render with counter-clockwise rotation', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render with custom duration', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render with default duration', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should handle different icon names', () => { + const iconNames = ['sync', 'refresh', 'loading', 'autorenew']; + + iconNames.forEach(name => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + }); + + it('should handle different sizes', () => { + const sizes = [16, 24, 32, 48]; + + sizes.forEach(size => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + }); + + it('should handle different colors', () => { + const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFFFF']; + + colors.forEach(color => { + const {toJSON} = render( + , + ); + expect(toJSON()).toBeTruthy(); + }); + }); +}); diff --git a/components/SectionLayout.test.tsx b/components/SectionLayout.test.tsx new file mode 100644 index 00000000..93c7882b --- /dev/null +++ b/components/SectionLayout.test.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {SectionLayout} from './SectionLayout'; +import {Text, View} from 'react-native'; + +// Mock ui components +jest.mock('./ui', () => ({ + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Row: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +describe('SectionLayout Component', () => { + const defaultProps = { + headerIcon: Icon, + headerText: 'Section Header', + children: Section Content, + testId: 'testSection', + }; + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with custom marginBottom', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different header text', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with complex children', () => { + const {toJSON} = render( + + Line 1 + Line 2 + + Nested content + + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different icon', () => { + const customIcon = 🔍; + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with zero marginBottom', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/TextEditOverlay.test.tsx b/components/TextEditOverlay.test.tsx new file mode 100644 index 00000000..0c16b74e --- /dev/null +++ b/components/TextEditOverlay.test.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import {render, fireEvent} from '@testing-library/react-native'; +import {TextEditOverlay} from './TextEditOverlay'; + +// Mock react-native-elements with a more realistic Input +jest.mock('react-native-elements', () => { + const RN = jest.requireActual('react-native'); + return { + Input: (props: {value: string; onChangeText: (text: string) => void}) => ( + + ), + }; +}); + +// Mock ui components with more realistic Button +jest.mock('./ui', () => { + const RN = jest.requireActual('react-native'); + const React = jest.requireActual('react'); + return { + Button: (props: {title: string; onPress: () => void}) => ( + + {props.title} + + ), + Centered: ({children}: {children: React.ReactNode}) => <>{children}, + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Row: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, + }; +}); + +describe('TextEditOverlay Component', () => { + const defaultProps = { + isVisible: true, + label: 'Edit Name', + value: 'John Doe', + onSave: jest.fn(), + onDismiss: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should match snapshot with default props', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with different label', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with maxLength', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with empty value', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with long value', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should call onSave with value when save button is pressed', () => { + const onSave = jest.fn(); + const {getByTestId} = render( + , + ); + + const saveButton = getByTestId('button-save'); + fireEvent.press(saveButton); + + // onSave should be called with the current value + expect(onSave).toHaveBeenCalledTimes(1); + expect(typeof onSave.mock.calls[0][0]).toBe('string'); + }); + + it('should call onSave with original value when save is pressed without changes', () => { + const onSave = jest.fn(); + const {getByTestId} = render( + , + ); + + const saveButton = getByTestId('button-save'); + fireEvent.press(saveButton); + + expect(onSave).toHaveBeenCalledWith('John Doe'); + }); + + it('should call onDismiss and reset value when cancel button is pressed', () => { + const onDismiss = jest.fn(); + const {getByTestId} = render( + , + ); + + const input = getByTestId('text-input'); + // Simulate text change + input.props.onChangeText('Modified Text'); + + const cancelButton = getByTestId('button-cancel'); + fireEvent.press(cancelButton); + + expect(onDismiss).toHaveBeenCalled(); + }); + + it('should handle text input changes', () => { + const {getByTestId} = render(); + + const input = getByTestId('text-input'); + + // Verify input has onChangeText handler + expect(input.props.onChangeText).toBeDefined(); + expect(typeof input.props.onChangeText).toBe('function'); + }); + + it('should not use isVisible prop', () => { + // Note: isVisible is defined in the interface but not used in the component + // This test documents that the prop exists but has no effect + const {toJSON: jsonVisible} = render( + , + ); + const {toJSON: jsonHidden} = render( + , + ); + + // Both render the same output regardless of isVisible value + expect(JSON.stringify(jsonVisible())).toEqual(JSON.stringify(jsonHidden())); + }); +}); diff --git a/components/TrustModal.test.tsx b/components/TrustModal.test.tsx new file mode 100644 index 00000000..1e44fda9 --- /dev/null +++ b/components/TrustModal.test.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {TrustModal} from './TrustModal'; + +// Mock useTranslation hook +const mockT = jest.fn((key: string) => { + if (key === 'infoPoints' || key === 'verifierInfoPoints') { + return ['Point 1', 'Point 2', 'Point 3']; + } + return key; +}); + +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: () => ({ + t: mockT, + i18n: {changeLanguage: jest.fn()}, + }), +})); + +// Mock ui components +jest.mock('./ui', () => ({ + Button: jest.fn(() => null), + Column: ({children}: {children: React.ReactNode}) => <>{children}, + Row: ({children}: {children: React.ReactNode}) => <>{children}, + Text: ({children}: {children: React.ReactNode}) => <>{children}, +})); + +// Mock react-native components +jest.mock('react-native', () => { + const ReactNative = jest.requireActual('react-native'); + return { + ...ReactNative, + Modal: ({children}: {children: React.ReactNode}) => <>{children}, + View: ({children}: {children: React.ReactNode}) => <>{children}, + ScrollView: ({children}: {children: React.ReactNode}) => <>{children}, + Image: jest.fn(() => null), + }; +}); + +describe('TrustModal Component', () => { + const defaultProps = { + isVisible: true, + logo: 'https://example.com/logo.png', + name: 'Test Issuer', + onConfirm: jest.fn(), + onCancel: jest.fn(), + }; + + it('should match snapshot with issuer flow', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with verifier flow', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot when not visible', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot without logo', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot without name', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot without logo and name', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with long name', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/VCVerification.test.tsx b/components/VCVerification.test.tsx new file mode 100644 index 00000000..2650a64f --- /dev/null +++ b/components/VCVerification.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {VCVerification} from './VCVerification'; +import {VCMetadata} from '../shared/VCMetadata'; +import {Display} from './VC/common/VCUtils'; + +// Mock the Display class +const mockDisplay = { + getTextColor: jest.fn((defaultColor: string) => defaultColor), +} as unknown as Display; + +describe('VCVerification Component', () => { + it('should render for verified and valid credential', () => { + const vcMetadata = new VCMetadata({ + isVerified: true, + isExpired: false, + }); + + const {toJSON} = render( + , + ); + + expect(toJSON()).toBeTruthy(); + }); + + it('should render for verified but expired credential', () => { + const vcMetadata = new VCMetadata({ + isVerified: true, + isExpired: true, + }); + + const {toJSON} = render( + , + ); + + expect(toJSON()).toBeTruthy(); + }); + + it('should render for pending/unverified credential', () => { + const vcMetadata = new VCMetadata({ + isVerified: false, + isExpired: false, + }); + + const {toJSON} = render( + , + ); + + expect(toJSON()).toBeTruthy(); + }); + + it('should render verification status text', () => { + const vcMetadata = new VCMetadata({ + isVerified: true, + isExpired: false, + }); + + const {getByText} = render( + , + ); + + expect(getByText('valid')).toBeTruthy(); + }); + + it('should call getTextColor from display prop', () => { + const vcMetadata = new VCMetadata({ + isVerified: true, + isExpired: false, + }); + + render(); + + expect(mockDisplay.getTextColor).toHaveBeenCalled(); + }); +}); diff --git a/components/VPShareActivityLogEvent.test.ts b/components/VPShareActivityLogEvent.test.ts new file mode 100644 index 00000000..f22edb71 --- /dev/null +++ b/components/VPShareActivityLogEvent.test.ts @@ -0,0 +1,234 @@ +import {VPShareActivityLog} from './VPShareActivityLogEvent'; +import {VCItemContainerFlowType} from '../shared/Utils'; + +describe('VPShareActivityLog', () => { + describe('constructor', () => { + it('should create instance with default values', () => { + const log = new VPShareActivityLog({}); + + expect(log.type).toBe(''); + expect(log.timestamp).toBeDefined(); + expect(log.flow).toBe(VCItemContainerFlowType.VP_SHARE); + expect(log.info).toBe(''); + }); + + it('should create instance with provided values', () => { + const timestamp = Date.now(); + const log = new VPShareActivityLog({ + type: 'SHARED_SUCCESSFULLY', + timestamp, + flow: VCItemContainerFlowType.QR_LOGIN, + info: 'Test info', + }); + + expect(log.type).toBe('SHARED_SUCCESSFULLY'); + expect(log.timestamp).toBe(timestamp); + expect(log.flow).toBe(VCItemContainerFlowType.QR_LOGIN); + expect(log.info).toBe('Test info'); + }); + + it('should handle different activity types', () => { + const types: Array< + | 'SHARED_SUCCESSFULLY' + | 'SHARED_WITH_FACE_VERIFIACTION' + | 'VERIFIER_AUTHENTICATION_FAILED' + | 'INVALID_AUTH_REQUEST' + | 'USER_DECLINED_CONSENT' + > = [ + 'SHARED_SUCCESSFULLY', + 'SHARED_WITH_FACE_VERIFIACTION', + 'VERIFIER_AUTHENTICATION_FAILED', + 'INVALID_AUTH_REQUEST', + 'USER_DECLINED_CONSENT', + ]; + + types.forEach(type => { + const log = new VPShareActivityLog({type}); + expect(log.type).toBe(type); + }); + }); + }); + + describe('getActionText', () => { + it('should return formatted action text', () => { + const log = new VPShareActivityLog({ + type: 'SHARED_SUCCESSFULLY', + info: 'Test info', + }); + + const mockT = jest.fn(key => `Translated: ${key}`); + const result = log.getActionText(mockT); + + expect(mockT).toHaveBeenCalledWith( + 'ActivityLogText:vpSharing:SHARED_SUCCESSFULLY', + {info: 'Test info'}, + ); + expect(result).toContain('Translated:'); + }); + + it('should handle empty type', () => { + const log = new VPShareActivityLog({type: ''}); + const mockT = jest.fn(key => key); + + log.getActionText(mockT); + expect(mockT).toHaveBeenCalled(); + }); + + it('should pass info to translation function', () => { + const log = new VPShareActivityLog({ + type: 'TECHNICAL_ERROR', + info: 'Error details', + }); + + const mockT = jest.fn(); + log.getActionText(mockT); + + expect(mockT).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({info: 'Error details'}), + ); + }); + }); + + describe('getLogFromObject', () => { + it('should create log from object', () => { + const data = { + type: 'SHARED_SUCCESSFULLY', + timestamp: 1234567890, + flow: VCItemContainerFlowType.VP_SHARE, + info: 'Test', + }; + + const log = VPShareActivityLog.getLogFromObject(data); + + expect(log).toBeInstanceOf(VPShareActivityLog); + expect(log.type).toBe('SHARED_SUCCESSFULLY'); + expect(log.timestamp).toBe(1234567890); + }); + + it('should handle empty object', () => { + const log = VPShareActivityLog.getLogFromObject({}); + + expect(log).toBeInstanceOf(VPShareActivityLog); + expect(log.type).toBe(''); + }); + + it('should handle partial object', () => { + const log = VPShareActivityLog.getLogFromObject({ + type: 'USER_DECLINED_CONSENT', + }); + + expect(log).toBeInstanceOf(VPShareActivityLog); + expect(log.type).toBe('USER_DECLINED_CONSENT'); + }); + }); + + describe('getActionLabel', () => { + it('should return formatted action label in English', () => { + const log = new VPShareActivityLog({ + timestamp: Date.now() - 60000, // 1 minute ago + }); + + const label = log.getActionLabel('enUS'); + + expect(label).toBeDefined(); + expect(typeof label).toBe('string'); + expect(label.length).toBeGreaterThan(0); + }); + + it('should handle different languages', () => { + const log = new VPShareActivityLog({ + timestamp: Date.now() - 3600000, // 1 hour ago + }); + + const languages = ['enUS', 'hi', 'kn', 'ta', 'ar']; + + languages.forEach(lang => { + const label = log.getActionLabel(lang); + expect(label).toBeDefined(); + expect(typeof label).toBe('string'); + }); + }); + + it('should show relative time', () => { + const recentTimestamp = Date.now() - 5000; // 5 seconds ago + const log = new VPShareActivityLog({timestamp: recentTimestamp}); + + const label = log.getActionLabel('enUS'); + + expect(label).toBeDefined(); + expect(label.length).toBeGreaterThan(0); + }); + + it('should handle old timestamps', () => { + const oldTimestamp = Date.now() - 86400000; // 1 day ago + const log = new VPShareActivityLog({timestamp: oldTimestamp}); + + const label = log.getActionLabel('enUS'); + + expect(label).toBeDefined(); + expect(label).toContain('ago'); + }); + }); + + describe('Activity Log Types', () => { + it('should handle all success types', () => { + const successTypes: Array< + | 'SHARED_SUCCESSFULLY' + | 'SHARED_WITH_FACE_VERIFIACTION' + | 'SHARED_AFTER_RETRY' + | 'SHARED_WITH_FACE_VERIFICATION_AFTER_RETRY' + > = [ + 'SHARED_SUCCESSFULLY', + 'SHARED_WITH_FACE_VERIFIACTION', + 'SHARED_AFTER_RETRY', + 'SHARED_WITH_FACE_VERIFICATION_AFTER_RETRY', + ]; + + successTypes.forEach(type => { + const log = new VPShareActivityLog({type}); + expect(log.type).toBe(type); + }); + }); + + it('should handle all error types', () => { + const errorTypes: Array< + | 'VERIFIER_AUTHENTICATION_FAILED' + | 'INVALID_AUTH_REQUEST' + | 'RETRY_ATTEMPT_FAILED' + | 'MAX_RETRY_ATTEMPT_FAILED' + | 'FACE_VERIFICATION_FAILED' + | 'TECHNICAL_ERROR' + > = [ + 'VERIFIER_AUTHENTICATION_FAILED', + 'INVALID_AUTH_REQUEST', + 'RETRY_ATTEMPT_FAILED', + 'MAX_RETRY_ATTEMPT_FAILED', + 'FACE_VERIFICATION_FAILED', + 'TECHNICAL_ERROR', + ]; + + errorTypes.forEach(type => { + const log = new VPShareActivityLog({type}); + expect(log.type).toBe(type); + }); + }); + + it('should handle credential-related types', () => { + const credentialTypes: Array< + | 'NO_SELECTED_VC_HAS_IMAGE' + | 'CREDENTIAL_MISMATCH_FROM_KEBAB' + | 'NO_CREDENTIAL_MATCHING_REQUEST' + > = [ + 'NO_SELECTED_VC_HAS_IMAGE', + 'CREDENTIAL_MISMATCH_FROM_KEBAB', + 'NO_CREDENTIAL_MATCHING_REQUEST', + ]; + + credentialTypes.forEach(type => { + const log = new VPShareActivityLog({type}); + expect(log.type).toBe(type); + }); + }); + }); +}); diff --git a/components/VcItemContainerProfileImage.test.tsx b/components/VcItemContainerProfileImage.test.tsx new file mode 100644 index 00000000..da90b178 --- /dev/null +++ b/components/VcItemContainerProfileImage.test.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {VcItemContainerProfileImage} from './VcItemContainerProfileImage'; +import {View} from 'react-native'; + +// Mock SvgImage +jest.mock('./ui/svg', () => ({ + SvgImage: { + pinIcon: jest.fn(() => ), + }, +})); + +// Mock ProfileIcon +jest.mock('./ProfileIcon', () => ({ + ProfileIcon: jest.fn(() => ), +})); + +describe('VcItemContainerProfileImage Component', () => { + const vcDataWithImage = { + face: 'https://example.com/avatar.jpg', + }; + + const vcDataWithoutImage = { + face: null, + }; + + it('should match snapshot with face image', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with face image and pinned', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot without face image', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot without face image and pinned', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should match snapshot with empty string face', () => { + const {toJSON} = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/components/VerifiedIcon.test.tsx b/components/VerifiedIcon.test.tsx new file mode 100644 index 00000000..50fafc67 --- /dev/null +++ b/components/VerifiedIcon.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import VerifiedIcon from './VerifiedIcon'; + +describe('VerifiedIcon', () => { + it('should render VerifiedIcon component', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should match snapshot', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should have proper styling structure', () => { + const {toJSON} = render(); + const tree = toJSON(); + + // Verify component structure exists + expect(tree).toBeTruthy(); + expect(tree.children).toBeTruthy(); + }); + + it('should render with check-circle icon', () => { + const {toJSON} = render(); + const tree = toJSON(); + expect(tree).toBeTruthy(); + }); +}); diff --git a/components/__snapshots__/AccountInformation.test.tsx.snap b/components/__snapshots__/AccountInformation.test.tsx.snap new file mode 100644 index 00000000..35ba47fe --- /dev/null +++ b/components/__snapshots__/AccountInformation.test.tsx.snap @@ -0,0 +1,821 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountInformation Component should match snapshot with different email 1`] = ` + + + + + + + + associatedAccount + + + + + another@test.com + + + + +`; + +exports[`AccountInformation Component should match snapshot with different picture URL 1`] = ` + + + + + + + + associatedAccount + + + + + test@example.com + + + + +`; + +exports[`AccountInformation Component should match snapshot with email and picture 1`] = ` + + + + + + + + associatedAccount + + + + + test@example.com + + + + +`; + +exports[`AccountInformation Component should match snapshot with long email 1`] = ` + + + + + + + + associatedAccount + + + + + very.long.email.address@example-domain.com + + + + +`; diff --git a/components/__snapshots__/ActivityLogText.test.tsx.snap b/components/__snapshots__/ActivityLogText.test.tsx.snap new file mode 100644 index 00000000..7de89d8a --- /dev/null +++ b/components/__snapshots__/ActivityLogText.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActivityLogText Component should match snapshot with VC activity 1`] = `null`; + +exports[`ActivityLogText Component should match snapshot with VP activity 1`] = `null`; diff --git a/components/__snapshots__/BackupAndRestoreBannerNotification.test.tsx.snap b/components/__snapshots__/BackupAndRestoreBannerNotification.test.tsx.snap new file mode 100644 index 00000000..c694cafc --- /dev/null +++ b/components/__snapshots__/BackupAndRestoreBannerNotification.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BackupAndRestoreBannerNotification Component should match snapshot with no banners 1`] = `null`; diff --git a/components/__snapshots__/BannerNotification.test.tsx.snap b/components/__snapshots__/BannerNotification.test.tsx.snap new file mode 100644 index 00000000..9f49314c --- /dev/null +++ b/components/__snapshots__/BannerNotification.test.tsx.snap @@ -0,0 +1,851 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BannerNotification Component should match snapshot with different testId 1`] = ` + + + + + Test notification message + + + + + + + +`; + +exports[`BannerNotification Component should match snapshot with error status 1`] = ` + + + + + Test notification message + + + + + + + +`; + +exports[`BannerNotification Component should match snapshot with in progress status 1`] = ` + + + + + Test notification message + + + + + + + +`; + +exports[`BannerNotification Component should match snapshot with long message 1`] = ` + + + + + This is a very long notification message that should wrap to multiple lines and still be displayed correctly + + + + + + + +`; + +exports[`BannerNotification Component should match snapshot with success status 1`] = ` + + + + + Test notification message + + + + + + + +`; diff --git a/components/__snapshots__/BannerNotificationContainer.test.tsx.snap b/components/__snapshots__/BannerNotificationContainer.test.tsx.snap new file mode 100644 index 00000000..ced6e873 --- /dev/null +++ b/components/__snapshots__/BannerNotificationContainer.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BannerNotificationContainer Component should match snapshot with no banners visible 1`] = `null`; + +exports[`BannerNotificationContainer Component should match snapshot with verification banner disabled 1`] = `null`; + +exports[`BannerNotificationContainer Component should match snapshot with verification banner enabled 1`] = `null`; diff --git a/components/__snapshots__/CopilotTooltip.test.tsx.snap b/components/__snapshots__/CopilotTooltip.test.tsx.snap new file mode 100644 index 00000000..6622b3e0 --- /dev/null +++ b/components/__snapshots__/CopilotTooltip.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopilotTooltip Component should match snapshot with first step 1`] = ` +[ + "Step 1 Title", + "Step 1 Description", + "1/5", +] +`; diff --git a/components/__snapshots__/CopyButton.test.tsx.snap b/components/__snapshots__/CopyButton.test.tsx.snap new file mode 100644 index 00000000..7f320b76 --- /dev/null +++ b/components/__snapshots__/CopyButton.test.tsx.snap @@ -0,0 +1,268 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopyButton Component should match snapshot with default props 1`] = ` + + + + clipboard.copy + + + +`; + +exports[`CopyButton Component should match snapshot with long content 1`] = ` + + + + clipboard.copy + + + +`; + +exports[`CopyButton Component should match snapshot with special characters 1`] = ` + + + + clipboard.copy + + + +`; diff --git a/components/__snapshots__/DropdownIcon.test.tsx.snap b/components/__snapshots__/DropdownIcon.test.tsx.snap new file mode 100644 index 00000000..dd600c8e --- /dev/null +++ b/components/__snapshots__/DropdownIcon.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DropdownIcon Component should match snapshot with default props 1`] = ``; + +exports[`DropdownIcon Component should match snapshot with different icon 1`] = ``; + +exports[`DropdownIcon Component should match snapshot with empty items 1`] = ``; + +exports[`DropdownIcon Component should match snapshot with multiple items 1`] = ``; diff --git a/components/__snapshots__/DualMessageOverlay.test.tsx.snap b/components/__snapshots__/DualMessageOverlay.test.tsx.snap new file mode 100644 index 00000000..5a0b98fe --- /dev/null +++ b/components/__snapshots__/DualMessageOverlay.test.tsx.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DualMessageOverlay Component should match snapshot with both buttons 1`] = ` +[ + "Confirm Action", + "Are you sure you want to proceed?", + + tryAgain + , + + ignore + , +] +`; + +exports[`DualMessageOverlay Component should match snapshot with children 1`] = ` +[ + "Confirm Action", + "Are you sure you want to proceed?", + + Custom content here + , +] +`; + +exports[`DualMessageOverlay Component should match snapshot with custom height 1`] = ` +[ + "Confirm Action", + "Are you sure you want to proceed?", +] +`; + +exports[`DualMessageOverlay Component should match snapshot with hint text 1`] = ` +[ + "Confirm Action", + "Are you sure you want to proceed?", + "Additional information", +] +`; + +exports[`DualMessageOverlay Component should match snapshot with only ignore button 1`] = ` +[ + "Confirm Action", + "Are you sure you want to proceed?", + + ignore + , +] +`; + +exports[`DualMessageOverlay Component should match snapshot with only try again button 1`] = ` +[ + "Confirm Action", + "Are you sure you want to proceed?", + + tryAgain + , +] +`; + +exports[`DualMessageOverlay Component should match snapshot with title and message 1`] = ` +[ + "Confirm Action", + "Are you sure you want to proceed?", +] +`; diff --git a/components/__snapshots__/EditableListItem.test.tsx.snap b/components/__snapshots__/EditableListItem.test.tsx.snap new file mode 100644 index 00000000..02ee9054 --- /dev/null +++ b/components/__snapshots__/EditableListItem.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EditableListItem Component should match snapshot with custom title color 1`] = ` +[ + "Contact Information", + "Edit your details", + "editLabel", + "editLabel", +] +`; + +exports[`EditableListItem Component should match snapshot with default props 1`] = ` +[ + "Contact Information", + "Edit your details", + "editLabel", + "editLabel", +] +`; + +exports[`EditableListItem Component should match snapshot with error state 1`] = ` +[ + "Contact Information", + "Edit your details", + "editLabel", + "Failed to update", + "editLabel", +] +`; + +exports[`EditableListItem Component should match snapshot with progress indicator 1`] = ` +[ + "Contact Information", + "Edit your details", + "editLabel", + "editLabel", +] +`; + +exports[`EditableListItem Component should match snapshot with single item 1`] = ` +[ + "Contact Information", + "Edit your details", + "editLabel", +] +`; + +exports[`EditableListItem Component should match snapshot with success response 1`] = ` +[ + "Contact Information", + "Edit your details", + "editLabel", + "editLabel", +] +`; diff --git a/components/__snapshots__/GlobalContextProvider.test.tsx.snap b/components/__snapshots__/GlobalContextProvider.test.tsx.snap new file mode 100644 index 00000000..1f4a7f00 --- /dev/null +++ b/components/__snapshots__/GlobalContextProvider.test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GlobalContextProvider Component should match snapshot with children 1`] = ` + + Test Child + +`; + +exports[`GlobalContextProvider Component should match snapshot with multiple children 1`] = ` +[ + + Child 1 + , + + Child 2 + , +] +`; diff --git a/components/__snapshots__/HelpScreen.test.tsx.snap b/components/__snapshots__/HelpScreen.test.tsx.snap new file mode 100644 index 00000000..cbe83729 --- /dev/null +++ b/components/__snapshots__/HelpScreen.test.tsx.snap @@ -0,0 +1,6561 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HelpScreen Component should match snapshot when disabled 1`] = ` +[ + + + Help + + , + + + + + answers.inji.one + + + , + "title": "questions.inji.one", + }, + { + "data": + + answers.inji.two + + + here + + + , + "title": "questions.inji.two", + }, + { + "data": + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + , + "title": "questions.inji.three", + }, + { + "data": + + answers.inji.four + + + , + "title": "questions.inji.four", + }, + { + "data": + + answers.inji.five + + + , + "title": "questions.inji.five", + }, + { + "data": + + answers.inji.six + + + , + "title": "questions.inji.six", + }, + { + "data": + + answers.inji.seven + + + here + + + , + "title": "questions.inji.seven", + }, + { + "data": + + answers.inji.eight + + + , + "title": "questions.inji.eight", + }, + { + "data": + + answers.inji.nine + + + here + + + , + "title": "questions.inji.nine", + }, + { + "data": + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + , + "title": "questions.inji.ten", + }, + { + "data": + + answers.inji.eleven + + + , + "title": "questions.inji.eleven", + }, + { + "data": + + answers.inji.twelve + + + , + "title": "questions.inji.twelve", + }, + { + "data": + + answers.inji.sixteen + + + , + "title": "questions.inji.sixteen", + }, + { + "data": + + answers.inji.seventeen + + + , + "title": "questions.inji.seventeen", + }, + { + "data": + + answers.inji.thirteen-a + + + here + + + + answers.inji.thirteen-b + + + , + "title": "questions.inji.thirteen", + }, + { + "data": + + answers.inji.fourteen + + + , + "title": "questions.inji.fourteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.backup.one + + + , + "title": "questions.backup.one", + }, + { + "data": + + answers.backup.two + + + , + "title": "questions.backup.two", + }, + { + "data": + + answers.backup.three + + + , + "title": "questions.backup.three", + }, + { + "data": + + answers.backup.four + + + , + "title": "questions.backup.four", + }, + { + "data": + + answers.backup.five + + + , + "title": "questions.backup.five", + }, + { + "data": + + answers.backup.six + + + , + "title": "questions.backup.six", + }, + { + "data": + + answers.backup.seven + + + , + "title": "questions.backup.seven", + }, + { + "data": + + answers.backup.eight + + + , + "title": "questions.backup.eight", + }, + { + "data": + + answers.KeyManagement.one + + + , + "title": "questions.KeyManagement.one", + }, + { + "data": + + answers.KeyManagement.two + + + , + "title": "questions.KeyManagement.two", + }, + { + "data": + + answers.KeyManagement.three + + + , + "title": "questions.KeyManagement.three", + }, + { + "data": + + answers.KeyManagement.four + + + , + "title": "questions.KeyManagement.four", + }, + { + "data": + + answers.KeyManagement.five + + + , + "title": "questions.KeyManagement.five", + }, + { + "data": + + answers.KeyManagement.six + + + , + "title": "questions.KeyManagement.six", + }, + ] + } + getItem={[Function]} + getItemCount={[Function]} + keyExtractor={[Function]} + onContentSizeChange={[Function]} + onLayout={[Function]} + onMomentumScrollBegin={[Function]} + onMomentumScrollEnd={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + onScrollToIndexFailed={[Function]} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEventThrottle={0.0001} + stickyHeaderIndices={[]} + viewabilityConfigCallbackPairs={[]} + > + + + + + questions.inji.one + + + answers.inji.one + + + + + + + + questions.inji.two + + + answers.inji.two + + + here + + + + + + + + questions.inji.three + + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + + + + + + questions.inji.four + + + answers.inji.four + + + + + + + + questions.inji.five + + + answers.inji.five + + + + + + + + questions.inji.six + + + answers.inji.six + + + + + + + + questions.inji.seven + + + answers.inji.seven + + + here + + + + + + + + questions.inji.eight + + + answers.inji.eight + + + + + + + + questions.inji.nine + + + answers.inji.nine + + + here + + + + + + + + questions.inji.ten + + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + + + + + + + , +] +`; + +exports[`HelpScreen Component should match snapshot with BackUp source 1`] = ` +[ + + + Help + + , + + + + + answers.inji.one + + + , + "title": "questions.inji.one", + }, + { + "data": + + answers.inji.two + + + here + + + , + "title": "questions.inji.two", + }, + { + "data": + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + , + "title": "questions.inji.three", + }, + { + "data": + + answers.inji.four + + + , + "title": "questions.inji.four", + }, + { + "data": + + answers.inji.five + + + , + "title": "questions.inji.five", + }, + { + "data": + + answers.inji.six + + + , + "title": "questions.inji.six", + }, + { + "data": + + answers.inji.seven + + + here + + + , + "title": "questions.inji.seven", + }, + { + "data": + + answers.inji.eight + + + , + "title": "questions.inji.eight", + }, + { + "data": + + answers.inji.nine + + + here + + + , + "title": "questions.inji.nine", + }, + { + "data": + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + , + "title": "questions.inji.ten", + }, + { + "data": + + answers.inji.eleven + + + , + "title": "questions.inji.eleven", + }, + { + "data": + + answers.inji.twelve + + + , + "title": "questions.inji.twelve", + }, + { + "data": + + answers.inji.sixteen + + + , + "title": "questions.inji.sixteen", + }, + { + "data": + + answers.inji.seventeen + + + , + "title": "questions.inji.seventeen", + }, + { + "data": + + answers.inji.thirteen-a + + + here + + + + answers.inji.thirteen-b + + + , + "title": "questions.inji.thirteen", + }, + { + "data": + + answers.inji.fourteen + + + , + "title": "questions.inji.fourteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.backup.one + + + , + "title": "questions.backup.one", + }, + { + "data": + + answers.backup.two + + + , + "title": "questions.backup.two", + }, + { + "data": + + answers.backup.three + + + , + "title": "questions.backup.three", + }, + { + "data": + + answers.backup.four + + + , + "title": "questions.backup.four", + }, + { + "data": + + answers.backup.five + + + , + "title": "questions.backup.five", + }, + { + "data": + + answers.backup.six + + + , + "title": "questions.backup.six", + }, + { + "data": + + answers.backup.seven + + + , + "title": "questions.backup.seven", + }, + { + "data": + + answers.backup.eight + + + , + "title": "questions.backup.eight", + }, + { + "data": + + answers.KeyManagement.one + + + , + "title": "questions.KeyManagement.one", + }, + { + "data": + + answers.KeyManagement.two + + + , + "title": "questions.KeyManagement.two", + }, + { + "data": + + answers.KeyManagement.three + + + , + "title": "questions.KeyManagement.three", + }, + { + "data": + + answers.KeyManagement.four + + + , + "title": "questions.KeyManagement.four", + }, + { + "data": + + answers.KeyManagement.five + + + , + "title": "questions.KeyManagement.five", + }, + { + "data": + + answers.KeyManagement.six + + + , + "title": "questions.KeyManagement.six", + }, + ] + } + getItem={[Function]} + getItemCount={[Function]} + keyExtractor={[Function]} + onContentSizeChange={[Function]} + onLayout={[Function]} + onMomentumScrollBegin={[Function]} + onMomentumScrollEnd={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + onScrollToIndexFailed={[Function]} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEventThrottle={0.0001} + stickyHeaderIndices={[]} + viewabilityConfigCallbackPairs={[]} + > + + + + + questions.inji.one + + + answers.inji.one + + + + + + + + questions.inji.two + + + answers.inji.two + + + here + + + + + + + + questions.inji.three + + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + + + + + + questions.inji.four + + + answers.inji.four + + + + + + + + questions.inji.five + + + answers.inji.five + + + + + + + + questions.inji.six + + + answers.inji.six + + + + + + + + questions.inji.seven + + + answers.inji.seven + + + here + + + + + + + + questions.inji.eight + + + answers.inji.eight + + + + + + + + questions.inji.nine + + + answers.inji.nine + + + here + + + + + + + + questions.inji.ten + + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + + + + + + + , +] +`; + +exports[`HelpScreen Component should match snapshot with Inji source 1`] = ` +[ + + + Help + + , + + + + + answers.inji.one + + + , + "title": "questions.inji.one", + }, + { + "data": + + answers.inji.two + + + here + + + , + "title": "questions.inji.two", + }, + { + "data": + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + , + "title": "questions.inji.three", + }, + { + "data": + + answers.inji.four + + + , + "title": "questions.inji.four", + }, + { + "data": + + answers.inji.five + + + , + "title": "questions.inji.five", + }, + { + "data": + + answers.inji.six + + + , + "title": "questions.inji.six", + }, + { + "data": + + answers.inji.seven + + + here + + + , + "title": "questions.inji.seven", + }, + { + "data": + + answers.inji.eight + + + , + "title": "questions.inji.eight", + }, + { + "data": + + answers.inji.nine + + + here + + + , + "title": "questions.inji.nine", + }, + { + "data": + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + , + "title": "questions.inji.ten", + }, + { + "data": + + answers.inji.eleven + + + , + "title": "questions.inji.eleven", + }, + { + "data": + + answers.inji.twelve + + + , + "title": "questions.inji.twelve", + }, + { + "data": + + answers.inji.sixteen + + + , + "title": "questions.inji.sixteen", + }, + { + "data": + + answers.inji.seventeen + + + , + "title": "questions.inji.seventeen", + }, + { + "data": + + answers.inji.thirteen-a + + + here + + + + answers.inji.thirteen-b + + + , + "title": "questions.inji.thirteen", + }, + { + "data": + + answers.inji.fourteen + + + , + "title": "questions.inji.fourteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.backup.one + + + , + "title": "questions.backup.one", + }, + { + "data": + + answers.backup.two + + + , + "title": "questions.backup.two", + }, + { + "data": + + answers.backup.three + + + , + "title": "questions.backup.three", + }, + { + "data": + + answers.backup.four + + + , + "title": "questions.backup.four", + }, + { + "data": + + answers.backup.five + + + , + "title": "questions.backup.five", + }, + { + "data": + + answers.backup.six + + + , + "title": "questions.backup.six", + }, + { + "data": + + answers.backup.seven + + + , + "title": "questions.backup.seven", + }, + { + "data": + + answers.backup.eight + + + , + "title": "questions.backup.eight", + }, + { + "data": + + answers.KeyManagement.one + + + , + "title": "questions.KeyManagement.one", + }, + { + "data": + + answers.KeyManagement.two + + + , + "title": "questions.KeyManagement.two", + }, + { + "data": + + answers.KeyManagement.three + + + , + "title": "questions.KeyManagement.three", + }, + { + "data": + + answers.KeyManagement.four + + + , + "title": "questions.KeyManagement.four", + }, + { + "data": + + answers.KeyManagement.five + + + , + "title": "questions.KeyManagement.five", + }, + { + "data": + + answers.KeyManagement.six + + + , + "title": "questions.KeyManagement.six", + }, + ] + } + getItem={[Function]} + getItemCount={[Function]} + keyExtractor={[Function]} + onContentSizeChange={[Function]} + onLayout={[Function]} + onMomentumScrollBegin={[Function]} + onMomentumScrollEnd={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + onScrollToIndexFailed={[Function]} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEventThrottle={0.0001} + stickyHeaderIndices={[]} + viewabilityConfigCallbackPairs={[]} + > + + + + + questions.inji.one + + + answers.inji.one + + + + + + + + questions.inji.two + + + answers.inji.two + + + here + + + + + + + + questions.inji.three + + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + + + + + + questions.inji.four + + + answers.inji.four + + + + + + + + questions.inji.five + + + answers.inji.five + + + + + + + + questions.inji.six + + + answers.inji.six + + + + + + + + questions.inji.seven + + + answers.inji.seven + + + here + + + + + + + + questions.inji.eight + + + answers.inji.eight + + + + + + + + questions.inji.nine + + + answers.inji.nine + + + here + + + + + + + + questions.inji.ten + + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + + + + + + + , +] +`; + +exports[`HelpScreen Component should match snapshot with keyManagement source 1`] = ` +[ + + + Help + + , + + + + + answers.inji.one + + + , + "title": "questions.inji.one", + }, + { + "data": + + answers.inji.two + + + here + + + , + "title": "questions.inji.two", + }, + { + "data": + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + , + "title": "questions.inji.three", + }, + { + "data": + + answers.inji.four + + + , + "title": "questions.inji.four", + }, + { + "data": + + answers.inji.five + + + , + "title": "questions.inji.five", + }, + { + "data": + + answers.inji.six + + + , + "title": "questions.inji.six", + }, + { + "data": + + answers.inji.seven + + + here + + + , + "title": "questions.inji.seven", + }, + { + "data": + + answers.inji.eight + + + , + "title": "questions.inji.eight", + }, + { + "data": + + answers.inji.nine + + + here + + + , + "title": "questions.inji.nine", + }, + { + "data": + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + , + "title": "questions.inji.ten", + }, + { + "data": + + answers.inji.eleven + + + , + "title": "questions.inji.eleven", + }, + { + "data": + + answers.inji.twelve + + + , + "title": "questions.inji.twelve", + }, + { + "data": + + answers.inji.sixteen + + + , + "title": "questions.inji.sixteen", + }, + { + "data": + + answers.inji.seventeen + + + , + "title": "questions.inji.seventeen", + }, + { + "data": + + answers.inji.thirteen-a + + + here + + + + answers.inji.thirteen-b + + + , + "title": "questions.inji.thirteen", + }, + { + "data": + + answers.inji.fourteen + + + , + "title": "questions.inji.fourteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.inji.fifteen + + + , + "title": "questions.inji.fifteen", + }, + { + "data": + + answers.backup.one + + + , + "title": "questions.backup.one", + }, + { + "data": + + answers.backup.two + + + , + "title": "questions.backup.two", + }, + { + "data": + + answers.backup.three + + + , + "title": "questions.backup.three", + }, + { + "data": + + answers.backup.four + + + , + "title": "questions.backup.four", + }, + { + "data": + + answers.backup.five + + + , + "title": "questions.backup.five", + }, + { + "data": + + answers.backup.six + + + , + "title": "questions.backup.six", + }, + { + "data": + + answers.backup.seven + + + , + "title": "questions.backup.seven", + }, + { + "data": + + answers.backup.eight + + + , + "title": "questions.backup.eight", + }, + { + "data": + + answers.KeyManagement.one + + + , + "title": "questions.KeyManagement.one", + }, + { + "data": + + answers.KeyManagement.two + + + , + "title": "questions.KeyManagement.two", + }, + { + "data": + + answers.KeyManagement.three + + + , + "title": "questions.KeyManagement.three", + }, + { + "data": + + answers.KeyManagement.four + + + , + "title": "questions.KeyManagement.four", + }, + { + "data": + + answers.KeyManagement.five + + + , + "title": "questions.KeyManagement.five", + }, + { + "data": + + answers.KeyManagement.six + + + , + "title": "questions.KeyManagement.six", + }, + ] + } + getItem={[Function]} + getItemCount={[Function]} + keyExtractor={[Function]} + onContentSizeChange={[Function]} + onLayout={[Function]} + onMomentumScrollBegin={[Function]} + onMomentumScrollEnd={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + onScrollToIndexFailed={[Function]} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEventThrottle={0.0001} + stickyHeaderIndices={[]} + viewabilityConfigCallbackPairs={[]} + > + + + + + questions.inji.one + + + answers.inji.one + + + + + + + + questions.inji.two + + + answers.inji.two + + + here + + + + + + + + questions.inji.three + + + answers.inji.three-a + + + + answers.inji.three-b + + + + answers.inji.three-c + + + + + + + + questions.inji.four + + + answers.inji.four + + + + + + + + questions.inji.five + + + answers.inji.five + + + + + + + + questions.inji.six + + + answers.inji.six + + + + + + + + questions.inji.seven + + + answers.inji.seven + + + here + + + + + + + + questions.inji.eight + + + answers.inji.eight + + + + + + + + questions.inji.nine + + + answers.inji.nine + + + here + + + + + + + + questions.inji.ten + + + answers.inji.ten-a + + + here + + + + answers.inji.ten-b + + + + + + + + + , +] +`; diff --git a/components/__snapshots__/KebabPopUp.test.tsx.snap b/components/__snapshots__/KebabPopUp.test.tsx.snap new file mode 100644 index 00000000..73c26f2d --- /dev/null +++ b/components/__snapshots__/KebabPopUp.test.tsx.snap @@ -0,0 +1,1149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KebabPopUp Component should match snapshot when not visible 1`] = ` + + + + title + + + + + + Pin Card + + + + + + Remove + + + +`; + +exports[`KebabPopUp Component should match snapshot with VC that has image 1`] = ` + + + + title + + + + + + Pin Card + + + + + + Remove + + + +`; + +exports[`KebabPopUp Component should match snapshot with custom icon color 1`] = ` + + + + title + + + + + + Pin Card + + + + + + Remove + + + +`; + +exports[`KebabPopUp Component should match snapshot with custom icon component 1`] = ` + + + Custom + + + + title + + + + + + Pin Card + + + + + + Remove + + + +`; + +exports[`KebabPopUp Component should match snapshot with default icon 1`] = ` + + + + title + + + + + + Pin Card + + + + + + Remove + + + +`; diff --git a/components/__snapshots__/LanguageSelector.test.tsx.snap b/components/__snapshots__/LanguageSelector.test.tsx.snap new file mode 100644 index 00000000..7d6ee666 --- /dev/null +++ b/components/__snapshots__/LanguageSelector.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LanguageSelector Component should match snapshot with custom trigger component 1`] = ``; + +exports[`LanguageSelector Component should match snapshot with default trigger 1`] = ``; diff --git a/components/__snapshots__/Message.test.tsx.snap b/components/__snapshots__/Message.test.tsx.snap new file mode 100644 index 00000000..24898b20 --- /dev/null +++ b/components/__snapshots__/Message.test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Message Component should match snapshot with cancel button 1`] = ` + + Test + +`; + +exports[`Message Component should match snapshot with hint text 1`] = ` + + Test + Hint text + +`; + +exports[`Message Component should match snapshot with message only 1`] = ` + + Test Message + +`; + +exports[`Message Component should match snapshot with title and message 1`] = ` + + Title + Message + +`; + +exports[`Message Component should match snapshot with title only 1`] = ` + + Test Title + +`; diff --git a/components/__snapshots__/MessageOverlay.test.tsx.snap b/components/__snapshots__/MessageOverlay.test.tsx.snap new file mode 100644 index 00000000..9e889d46 --- /dev/null +++ b/components/__snapshots__/MessageOverlay.test.tsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorMessageOverlay Component should match snapshot with error 1`] = ` +[ + "network.title", + "network.message", +] +`; + +exports[`ErrorMessageOverlay Component should match snapshot with testID 1`] = ` +[ + "network.title", + "network.message", +] +`; + +exports[`MessageOverlay Component should match snapshot with button 1`] = ` +[ + "Test Title", + "Test Message", +] +`; + +exports[`MessageOverlay Component should match snapshot with custom children 1`] = ` +[ + "Test Title", + "Test Message", + + Custom Content + , +] +`; + +exports[`MessageOverlay Component should match snapshot with custom minHeight 1`] = ` +[ + "Test Title", + "Test Message", +] +`; + +exports[`MessageOverlay Component should match snapshot with hint text 1`] = ` +[ + "Test Title", + "Test Message", + "This is a hint", +] +`; + +exports[`MessageOverlay Component should match snapshot with numeric progress 1`] = ` +[ + "Test Title", + "Test Message", +] +`; + +exports[`MessageOverlay Component should match snapshot with progress indicator 1`] = ` +[ + "Test Title", + "Test Message", +] +`; + +exports[`MessageOverlay Component should match snapshot with title and message 1`] = ` +[ + "Test Title", + "Test Message", +] +`; diff --git a/components/__snapshots__/Passcode.test.tsx.snap b/components/__snapshots__/Passcode.test.tsx.snap new file mode 100644 index 00000000..c77482f6 --- /dev/null +++ b/components/__snapshots__/Passcode.test.tsx.snap @@ -0,0 +1,629 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Passcode Component should match snapshot with both message and error 1`] = ` + + + + + Enter passcode + + + + + Authentication failed + + + + +`; + +exports[`Passcode Component should match snapshot with custom message 1`] = ` + + + + + Please enter your 6-digit passcode + + + + + + + +`; + +exports[`Passcode Component should match snapshot with default props 1`] = ` + + + + + Enter your passcode + + + + + + + +`; + +exports[`Passcode Component should match snapshot with error message 1`] = ` + + + + + Enter your passcode + + + + + Incorrect passcode. Try again. + + + + +`; diff --git a/components/__snapshots__/PasscodeVerify.test.tsx.snap b/components/__snapshots__/PasscodeVerify.test.tsx.snap new file mode 100644 index 00000000..ae9b747a --- /dev/null +++ b/components/__snapshots__/PasscodeVerify.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PasscodeVerify Component should match snapshot with default props 1`] = `null`; + +exports[`PasscodeVerify Component should match snapshot with different testID 1`] = `null`; + +exports[`PasscodeVerify Component should match snapshot without onError handler 1`] = `null`; diff --git a/components/__snapshots__/PendingIcon.test.tsx.snap b/components/__snapshots__/PendingIcon.test.tsx.snap new file mode 100644 index 00000000..eb7406d5 --- /dev/null +++ b/components/__snapshots__/PendingIcon.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PendingIcon should match snapshot 1`] = ` + + + +`; + +exports[`PendingIcon should render with custom color 1`] = ` + + + +`; diff --git a/components/__snapshots__/PinInput.test.tsx.snap b/components/__snapshots__/PinInput.test.tsx.snap new file mode 100644 index 00000000..55b071f9 --- /dev/null +++ b/components/__snapshots__/PinInput.test.tsx.snap @@ -0,0 +1,719 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PinInput Component should match snapshot with 4 digit PIN 1`] = ` + + + + + + +`; + +exports[`PinInput Component should match snapshot with 6 digit PIN 1`] = ` + + + + + + + + +`; + +exports[`PinInput Component should match snapshot with custom testID 1`] = ` + + + + + + +`; + +exports[`PinInput Component should match snapshot with onChange handler 1`] = ` + + + + + + +`; + +exports[`PinInput Component should match snapshot with onDone and autosubmit 1`] = ` + + + + + + +`; diff --git a/components/__snapshots__/ProfileIcon.test.tsx.snap b/components/__snapshots__/ProfileIcon.test.tsx.snap new file mode 100644 index 00000000..8e212b44 --- /dev/null +++ b/components/__snapshots__/ProfileIcon.test.tsx.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProfileIcon Component should match snapshot with custom container styles 1`] = ` + + + +`; + +exports[`ProfileIcon Component should match snapshot with custom icon size 1`] = ` + + + +`; + +exports[`ProfileIcon Component should match snapshot with pinned icon 1`] = ` + + + + +`; + +exports[`ProfileIcon Component should match snapshot without pinned icon 1`] = ` + + + +`; diff --git a/components/__snapshots__/ProgressingModal.test.tsx.snap b/components/__snapshots__/ProgressingModal.test.tsx.snap new file mode 100644 index 00000000..3b38f533 --- /dev/null +++ b/components/__snapshots__/ProgressingModal.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProgressingModal Component should match snapshot as requester 1`] = `null`; + +exports[`ProgressingModal Component should match snapshot with BLE error visible 1`] = `"Bluetooth error occurred"`; + +exports[`ProgressingModal Component should match snapshot with default props 1`] = `null`; + +exports[`ProgressingModal Component should match snapshot with hint visible 1`] = `"Please wait..."`; + +exports[`ProgressingModal Component should match snapshot with progress spinner 1`] = ` + +`; + +exports[`ProgressingModal Component should match snapshot with retry button 1`] = `"Connection failed"`; + +exports[`ProgressingModal Component should match snapshot with stay in progress button 1`] = `"Taking longer than expected"`; diff --git a/components/__snapshots__/QrCodeOverlay.test.tsx.snap b/components/__snapshots__/QrCodeOverlay.test.tsx.snap new file mode 100644 index 00000000..cf71eff7 --- /dev/null +++ b/components/__snapshots__/QrCodeOverlay.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QrCodeOverlay Component should match snapshot with default props 1`] = `null`; + +exports[`QrCodeOverlay Component should match snapshot with force visible 1`] = `null`; + +exports[`QrCodeOverlay Component should match snapshot with inline QR disabled 1`] = `null`; + +exports[`QrCodeOverlay Component should match snapshot with onClose handler 1`] = `null`; diff --git a/components/__snapshots__/QrScanner.test.tsx.snap b/components/__snapshots__/QrScanner.test.tsx.snap new file mode 100644 index 00000000..f1731d60 --- /dev/null +++ b/components/__snapshots__/QrScanner.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QrScanner Component should match snapshot with custom title 1`] = ``; + +exports[`QrScanner Component should match snapshot with default props 1`] = ``; + +exports[`QrScanner Component should match snapshot with title 1`] = ``; diff --git a/components/__snapshots__/SectionLayout.test.tsx.snap b/components/__snapshots__/SectionLayout.test.tsx.snap new file mode 100644 index 00000000..aca2a2cd --- /dev/null +++ b/components/__snapshots__/SectionLayout.test.tsx.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SectionLayout Component should match snapshot with complex children 1`] = ` + + + Icon + + Section Header + + Line 1 + + + Line 2 + + + + Nested content + + + +`; + +exports[`SectionLayout Component should match snapshot with custom marginBottom 1`] = ` + + + Icon + + Section Header + + Section Content + + +`; + +exports[`SectionLayout Component should match snapshot with default props 1`] = ` + + + Icon + + Section Header + + Section Content + + +`; + +exports[`SectionLayout Component should match snapshot with different header text 1`] = ` + + + Icon + + Custom Section Title + + Section Content + + +`; + +exports[`SectionLayout Component should match snapshot with different icon 1`] = ` + + + 🔍 + + Section Header + + Section Content + + +`; + +exports[`SectionLayout Component should match snapshot with zero marginBottom 1`] = ` + + + Icon + + Section Header + + Section Content + + +`; diff --git a/components/__snapshots__/TextEditOverlay.test.tsx.snap b/components/__snapshots__/TextEditOverlay.test.tsx.snap new file mode 100644 index 00000000..71c9704c --- /dev/null +++ b/components/__snapshots__/TextEditOverlay.test.tsx.snap @@ -0,0 +1,501 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TextEditOverlay Component should match snapshot with default props 1`] = ` + + Edit Name + + + + cancel + + + + + save + + + +`; + +exports[`TextEditOverlay Component should match snapshot with different label 1`] = ` + + Edit Email + + + + cancel + + + + + save + + + +`; + +exports[`TextEditOverlay Component should match snapshot with empty value 1`] = ` + + Edit Name + + + + cancel + + + + + save + + + +`; + +exports[`TextEditOverlay Component should match snapshot with long value 1`] = ` + + Edit Name + + + + cancel + + + + + save + + + +`; + +exports[`TextEditOverlay Component should match snapshot with maxLength 1`] = ` + + Edit Name + + + + cancel + + + + + save + + + +`; diff --git a/components/__snapshots__/TrustModal.test.tsx.snap b/components/__snapshots__/TrustModal.test.tsx.snap new file mode 100644 index 00000000..fdbf5b88 --- /dev/null +++ b/components/__snapshots__/TrustModal.test.tsx.snap @@ -0,0 +1,1287 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TrustModal Component should match snapshot when not visible 1`] = `null`; + +exports[`TrustModal Component should match snapshot with issuer flow 1`] = ` + + + + + + + Test Issuer + + + + + + description + + + + + • + + + Point 1 + + + + + • + + + Point 2 + + + + + • + + + Point 3 + + + + + + + + + +`; + +exports[`TrustModal Component should match snapshot with long name 1`] = ` + + + + + + + Very Long Issuer Name That Should Wrap Properly + + + + + + verifierDescription + + + + + • + + + Point 1 + + + + + • + + + Point 2 + + + + + • + + + Point 3 + + + + + + + + + +`; + +exports[`TrustModal Component should match snapshot with verifier flow 1`] = ` + + + + + + + Test Issuer + + + + + + verifierDescription + + + + + • + + + Point 1 + + + + + • + + + Point 2 + + + + + • + + + Point 3 + + + + + + + + + +`; + +exports[`TrustModal Component should match snapshot without logo 1`] = ` + + + + + + Test Issuer + + + + + + description + + + + + • + + + Point 1 + + + + + • + + + Point 2 + + + + + • + + + Point 3 + + + + + + + + + +`; + +exports[`TrustModal Component should match snapshot without logo and name 1`] = ` + + + + + + + description + + + + + • + + + Point 1 + + + + + • + + + Point 2 + + + + + • + + + Point 3 + + + + + + + + + +`; + +exports[`TrustModal Component should match snapshot without name 1`] = ` + + + + + + + + + + description + + + + + • + + + Point 1 + + + + + • + + + Point 2 + + + + + • + + + Point 3 + + + + + + + + + +`; diff --git a/components/__snapshots__/VcItemContainerProfileImage.test.tsx.snap b/components/__snapshots__/VcItemContainerProfileImage.test.tsx.snap new file mode 100644 index 00000000..7a3d4cb5 --- /dev/null +++ b/components/__snapshots__/VcItemContainerProfileImage.test.tsx.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VcItemContainerProfileImage Component should match snapshot with empty string face 1`] = ` + +`; + +exports[`VcItemContainerProfileImage Component should match snapshot with face image 1`] = ` + + + +`; + +exports[`VcItemContainerProfileImage Component should match snapshot with face image and pinned 1`] = ` + + + + +`; + +exports[`VcItemContainerProfileImage Component should match snapshot without face image 1`] = ` + +`; + +exports[`VcItemContainerProfileImage Component should match snapshot without face image and pinned 1`] = ` + +`; diff --git a/components/__snapshots__/VerifiedIcon.test.tsx.snap b/components/__snapshots__/VerifiedIcon.test.tsx.snap new file mode 100644 index 00000000..3b24a759 --- /dev/null +++ b/components/__snapshots__/VerifiedIcon.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VerifiedIcon should match snapshot 1`] = ` + + + +`; diff --git a/components/ui/Layout.test.tsx b/components/ui/Layout.test.tsx new file mode 100644 index 00000000..cf238962 --- /dev/null +++ b/components/ui/Layout.test.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {Column, Row} from './Layout'; +import {Text} from 'react-native'; + +describe('Layout Components', () => { + describe('Column Component', () => { + it('should render Column component', () => { + const {toJSON} = render( + + Test + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render children in column', () => { + const {getByText} = render( + + Child 1 + Child 2 + , + ); + + expect(getByText('Child 1')).toBeTruthy(); + expect(getByText('Child 2')).toBeTruthy(); + }); + + it('should render with fill prop', () => { + const {getByLabelText} = render( + + Fill Column + , + ); + expect(getByLabelText('fill-column')).toBeTruthy(); + }); + + it('should render with multiple layout props', () => { + const {getByLabelText} = render( + + Complex Column + , + ); + expect(getByLabelText('complex-column')).toBeTruthy(); + }); + }); + + describe('Row Component', () => { + it('should render Row component', () => { + const {toJSON} = render( + + Test + , + ); + expect(toJSON()).toBeTruthy(); + }); + + it('should render children in row', () => { + const {getByText} = render( + + Item 1 + Item 2 + Item 3 + , + ); + + expect(getByText('Item 1')).toBeTruthy(); + expect(getByText('Item 2')).toBeTruthy(); + expect(getByText('Item 3')).toBeTruthy(); + }); + + it('should render with fill prop', () => { + const {getByLabelText} = render( + + Fill Row + , + ); + expect(getByLabelText('fill-row')).toBeTruthy(); + }); + + it('should render with multiple layout props', () => { + const {getByLabelText} = render( + + Complex Row + , + ); + expect(getByLabelText('complex-row')).toBeTruthy(); + }); + + it('should handle nested layouts', () => { + const {getByText} = render( + + + Nested 1 + + + Nested 2 + + , + ); + + expect(getByText('Nested 1')).toBeTruthy(); + expect(getByText('Nested 2')).toBeTruthy(); + }); + }); +}); diff --git a/components/ui/TextItem.test.tsx b/components/ui/TextItem.test.tsx new file mode 100644 index 00000000..a4b86ea0 --- /dev/null +++ b/components/ui/TextItem.test.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {TextItem} from './TextItem'; + +describe('TextItem', () => { + it('should render text without label', () => { + const {getByText} = render(); + expect(getByText('Test Text')).toBeTruthy(); + }); + + it('should render text with label', () => { + const {getByText} = render( + , + ); + expect(getByText('Main Text')).toBeTruthy(); + expect(getByText('Label Text')).toBeTruthy(); + }); + + it('should render only text when label is not provided', () => { + const {getByText, queryByText} = render(); + expect(getByText('Only Text')).toBeTruthy(); + expect(queryByText('Label Text')).toBeNull(); + }); + + it('should render with testID prop', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should render with divider prop', () => { + const {getByText} = render(); + expect(getByText('Test')).toBeTruthy(); + }); + + it('should render without divider when not specified', () => { + const {getByText} = render(); + expect(getByText('Test')).toBeTruthy(); + }); + + it('should render with topDivider prop', () => { + const {getByText} = render(); + expect(getByText('Test')).toBeTruthy(); + }); + + it('should render without topDivider when not specified', () => { + const {getByText} = render(); + expect(getByText('Test')).toBeTruthy(); + }); + + it('should render with both dividers', () => { + const {getByText} = render( + , + ); + expect(getByText('Test')).toBeTruthy(); + }); + + it('should render with custom margin when provided', () => { + const {getByText} = render(); + expect(getByText('Test')).toBeTruthy(); + }); + + it('should render long text correctly', () => { + const longText = 'A'.repeat(200); + const {getByText} = render(); + expect(getByText(longText)).toBeTruthy(); + }); + + it('should handle empty text', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should handle special characters in text', () => { + const specialText = '!@#$%^&*()_+-={}[]|:";\'<>?,./'; + const {getByText} = render(); + expect(getByText(specialText)).toBeTruthy(); + }); + + it('should render with all props combined', () => { + const {getByText} = render( + , + ); + + expect(getByText('Complete Test')).toBeTruthy(); + expect(getByText('All Props')).toBeTruthy(); + }); + + it('should render multiple TextItems', () => { + const {getByText} = render( + <> + + + + , + ); + + expect(getByText('First')).toBeTruthy(); + expect(getByText('Second')).toBeTruthy(); + expect(getByText('Third')).toBeTruthy(); + expect(getByText('One')).toBeTruthy(); + expect(getByText('Two')).toBeTruthy(); + expect(getByText('Three')).toBeTruthy(); + }); +}); diff --git a/components/ui/Timestamp.test.tsx b/components/ui/Timestamp.test.tsx new file mode 100644 index 00000000..79e2abae --- /dev/null +++ b/components/ui/Timestamp.test.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {Timestamp} from './Timestamp'; + +describe('Timestamp', () => { + it('should render formatted date and time', () => { + // March 15, 2024 at 14:30:00 + const timestamp = new Date(2024, 2, 15, 14, 30, 0).getTime(); + const {getByText} = render(); + + const formatted = getByText(/15 March 2024/); + expect(formatted).toBeTruthy(); + expect(formatted.props.children).toContain('PM'); + }); + + it('should format AM time correctly', () => { + // March 15, 2024 at 09:15:00 + const timestamp = new Date(2024, 2, 15, 9, 15, 0).getTime(); + const {getByText} = render(); + + const formatted = getByText(/09:15 AM/); + expect(formatted).toBeTruthy(); + }); + + it('should format PM time correctly', () => { + // June 20, 2024 at 18:45:00 + const timestamp = new Date(2024, 5, 20, 18, 45, 0).getTime(); + const {getByText} = render(); + + const formatted = getByText(/06:45 PM/); + expect(formatted).toBeTruthy(); + }); + + it('should handle midnight correctly', () => { + // January 1, 2024 at 00:00:00 + const timestamp = new Date(2024, 0, 1, 0, 0, 0).getTime(); + const {getByText} = render( + , + ); + + const formatted = getByText(/12:00 AM/); + expect(formatted).toBeTruthy(); + }); + + it('should handle noon correctly', () => { + // December 25, 2023 at 12:00:00 + const timestamp = new Date(2023, 11, 25, 12, 0, 0).getTime(); + const {getByText} = render(); + + const formatted = getByText(/12:00 PM/); + expect(formatted).toBeTruthy(); + }); + + it('should pad single digit minutes with zero', () => { + // April 10, 2024 at 15:05:00 + const timestamp = new Date(2024, 3, 10, 15, 5, 0).getTime(); + const {getByText} = render(); + + const formatted = getByText(/03:05 PM/); + expect(formatted).toBeTruthy(); + }); + + it('should render with testID prop', () => { + const timestamp = new Date(2024, 0, 1, 0, 0, 0).getTime(); + const {toJSON} = render(); + + expect(toJSON()).toBeTruthy(); + }); + + it('should display different months correctly', () => { + const months = [ + {month: 0, name: 'January'}, + {month: 1, name: 'February'}, + {month: 2, name: 'March'}, + {month: 6, name: 'July'}, + {month: 11, name: 'December'}, + ]; + + months.forEach(({month, name}) => { + const timestamp = new Date(2024, month, 15, 12, 0, 0).getTime(); + const {getByText} = render( + , + ); + expect(getByText(new RegExp(name))).toBeTruthy(); + }); + }); +}); diff --git a/components/ui/ToastItem.test.tsx b/components/ui/ToastItem.test.tsx new file mode 100644 index 00000000..3c941e5c --- /dev/null +++ b/components/ui/ToastItem.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import {render} from '@testing-library/react-native'; +import {ToastItem} from './ToastItem'; + +describe('ToastItem', () => { + it('should render toast message', () => { + const {getByText} = render(); + expect(getByText('Success!')).toBeTruthy(); + }); + + it('should render long message', () => { + const longMessage = + 'This is a very long toast message that should still be displayed properly'; + const {getByText} = render(); + expect(getByText(longMessage)).toBeTruthy(); + }); + + it('should render short message', () => { + const {getByText} = render(); + expect(getByText('OK')).toBeTruthy(); + }); + + it('should render empty message', () => { + const {toJSON} = render(); + expect(toJSON()).toBeTruthy(); + }); + + it('should render message with special characters', () => { + const message = 'Error: Operation failed! (Code: 500)'; + const {getByText} = render(); + expect(getByText(message)).toBeTruthy(); + }); + + it('should render message with numbers', () => { + const message = '123 items processed successfully'; + const {getByText} = render(); + expect(getByText(message)).toBeTruthy(); + }); + + it('should render message with emojis', () => { + const message = '✅ Success! 🎉'; + const {getByText} = render(); + expect(getByText(message)).toBeTruthy(); + }); + + it('should render multiline message', () => { + const message = 'Line 1\nLine 2\nLine 3'; + const {getByText} = render(); + expect(getByText(message)).toBeTruthy(); + }); + + it('should render different toast messages', () => { + const messages = [ + 'Operation completed', + 'Error occurred', + 'Warning: Low storage', + 'Info: Update available', + ]; + + messages.forEach(message => { + const {getByText} = render(); + expect(getByText(message)).toBeTruthy(); + }); + }); +}); diff --git a/components/ui/themes/themes.test.ts b/components/ui/themes/themes.test.ts new file mode 100644 index 00000000..792fa544 --- /dev/null +++ b/components/ui/themes/themes.test.ts @@ -0,0 +1,106 @@ +import {DefaultTheme} from './DefaultTheme'; +import {PurpleTheme} from './PurpleTheme'; + +describe('DefaultTheme', () => { + it('should be defined', () => { + expect(DefaultTheme).toBeDefined(); + }); + + it('should have Colors property', () => { + expect(DefaultTheme.Colors).toBeDefined(); + expect(typeof DefaultTheme.Colors).toBe('object'); + }); + + it('should have ProfileIconColor', () => { + expect(DefaultTheme.Colors.ProfileIconColor).toBeDefined(); + }); + + it('should have TextStyles property', () => { + expect(DefaultTheme.TextStyles).toBeDefined(); + expect(typeof DefaultTheme.TextStyles).toBe('object'); + }); + + it('should have ButtonStyles property', () => { + expect(DefaultTheme.ButtonStyles).toBeDefined(); + expect(typeof DefaultTheme.ButtonStyles).toBe('object'); + }); + + it('should have spacing function', () => { + expect(DefaultTheme.spacing).toBeDefined(); + expect(typeof DefaultTheme.spacing).toBe('function'); + }); + + it('should have elevation function', () => { + expect(DefaultTheme.elevation).toBeDefined(); + expect(typeof DefaultTheme.elevation).toBe('function'); + }); +}); + +describe('PurpleTheme', () => { + it('should be defined', () => { + expect(PurpleTheme).toBeDefined(); + }); + + it('should have Colors property', () => { + expect(PurpleTheme.Colors).toBeDefined(); + expect(typeof PurpleTheme.Colors).toBe('object'); + }); + + it('should have ProfileIconColor', () => { + expect(PurpleTheme.Colors.ProfileIconColor).toBeDefined(); + }); + + it('should have different colors than Default', () => { + expect(PurpleTheme.Colors).not.toEqual(DefaultTheme.Colors); + }); + + it('should have TextStyles property', () => { + expect(PurpleTheme.TextStyles).toBeDefined(); + expect(typeof PurpleTheme.TextStyles).toBe('object'); + }); + + it('should have ButtonStyles property', () => { + expect(PurpleTheme.ButtonStyles).toBeDefined(); + expect(typeof PurpleTheme.ButtonStyles).toBe('object'); + }); + + it('should have spacing function', () => { + expect(PurpleTheme.spacing).toBeDefined(); + expect(typeof PurpleTheme.spacing).toBe('function'); + }); + + it('should have elevation function', () => { + expect(PurpleTheme.elevation).toBeDefined(); + expect(typeof PurpleTheme.elevation).toBe('function'); + }); + + it('should have same structure as DefaultTheme', () => { + const defaultKeys = Object.keys(DefaultTheme); + const purpleKeys = Object.keys(PurpleTheme); + expect(purpleKeys.sort()).toEqual(defaultKeys.sort()); + }); +}); + +describe('Theme spacing function', () => { + it('should return spacing styles for Default theme', () => { + const spacing = DefaultTheme.spacing('margin', 'xs'); + expect(spacing).toBeDefined(); + }); + + it('should return spacing styles for Purple theme', () => { + const spacing = PurpleTheme.spacing('padding', 'sm'); + expect(spacing).toBeDefined(); + }); +}); + +describe('Theme elevation function', () => { + it('should return elevation styles for Default theme', () => { + const elevation = DefaultTheme.elevation(2); + expect(elevation).toBeDefined(); + }); + + it('should return elevation styles for Purple theme', () => { + const elevation = PurpleTheme.elevation(3); + expect(elevation).toBeDefined(); + }); +}); diff --git a/jest.config.js b/jest.config.js index 80c10275..84a41346 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // jest.config.js const {defaults: tsjPreset} = require('ts-jest/presets'); module.exports = { @@ -36,8 +37,8 @@ module.exports = { ], transformIgnorePatterns: [ 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-native-vector-icons|jsonld|jsonld-signatures|@digitalbazaar/.*)', - 'node_modules/(?!(@react-native|react-native|react-native-argon2|@react-navigation|react-native-elements|react-native-size-matters|react-native-ratings|expo-constants|base58-universal|@react-native-*|react-native-google-signin|react-native-linear-gradient|expo-camera|base58-universal/*|react-native-*|react-native-vector-icons|jsonld|jsonld-signatures|@digitalbazaar/.*)/).*/', - ], + 'node_modules/(?!(@react-native|react-native|react-native-argon2|@react-navigation|react-native-elements|react-native-size-matters|react-native-ratings|expo-constants|base58-universal|@react-native-*|react-native-google-signin|react-native-linear-gradient|expo-camera|base58-universal/*|react-native-*|react-native-vector-icons|jsonld|jsonld-signatures|@digitalbazaar/.*)/).*/', + ], setupFiles: [ '/__mocks__/svg.mock.js', '/__mocks__/jest-init.js', @@ -52,8 +53,8 @@ module.exports = { '/__mocks__/@noble/mock-secp256k1.js', '/__mocks__/@noble/mock-ed25519.js', '/__mocks__/react-native-base64.js', - '__mocks__/mockCrytoUtil.js', - '__mocks__/text-encoder.js', + '/__mocks__/mockCrytoUtil.js', + '/__mocks__/text-encoder.js', // https://github.com/react-native-google-signin/google-signin?tab=readme-ov-file#jest-module-mock '/node_modules/@react-native-google-signin/google-signin/jest/build/setup.js', ], diff --git a/machines/Issuers/IssuersEvents.test.ts b/machines/Issuers/IssuersEvents.test.ts new file mode 100644 index 00000000..90b920a6 --- /dev/null +++ b/machines/Issuers/IssuersEvents.test.ts @@ -0,0 +1,271 @@ +import {IssuersEvents} from './IssuersEvents'; +import {CredentialTypes} from '../VerifiableCredential/VCMetaMachine/vc'; + +describe('IssuersEvents', () => { + describe('SELECTED_ISSUER', () => { + it('should create event with id', () => { + const result = IssuersEvents.SELECTED_ISSUER('issuer-123'); + expect(result).toEqual({id: 'issuer-123'}); + }); + + it('should handle empty id', () => { + const result = IssuersEvents.SELECTED_ISSUER(''); + expect(result).toEqual({id: ''}); + }); + }); + + describe('DOWNLOAD_ID', () => { + it('should create empty event', () => { + const result = IssuersEvents.DOWNLOAD_ID(); + expect(result).toEqual({}); + }); + }); + + describe('BIOMETRIC_CANCELLED', () => { + it('should create event with requester', () => { + const result = IssuersEvents.BIOMETRIC_CANCELLED('login'); + expect(result).toEqual({requester: 'login'}); + }); + + it('should create event without requester', () => { + const result = IssuersEvents.BIOMETRIC_CANCELLED(); + expect(result).toEqual({requester: undefined}); + }); + }); + + describe('COMPLETED', () => { + it('should create empty event', () => { + const result = IssuersEvents.COMPLETED(); + expect(result).toEqual({}); + }); + }); + + describe('TRY_AGAIN', () => { + it('should create empty event', () => { + const result = IssuersEvents.TRY_AGAIN(); + expect(result).toEqual({}); + }); + }); + + describe('RESET_ERROR', () => { + it('should create empty event', () => { + const result = IssuersEvents.RESET_ERROR(); + expect(result).toEqual({}); + }); + }); + + describe('CHECK_KEY_PAIR', () => { + it('should create empty event', () => { + const result = IssuersEvents.CHECK_KEY_PAIR(); + expect(result).toEqual({}); + }); + }); + + describe('CANCEL', () => { + it('should create empty event', () => { + const result = IssuersEvents.CANCEL(); + expect(result).toEqual({}); + }); + }); + + describe('STORE_RESPONSE', () => { + it('should create event with response', () => { + const response = {data: 'test'}; + const result = IssuersEvents.STORE_RESPONSE(response); + expect(result).toEqual({response: {data: 'test'}}); + }); + + it('should create event without response', () => { + const result = IssuersEvents.STORE_RESPONSE(); + expect(result).toEqual({response: undefined}); + }); + }); + + describe('STORE_ERROR', () => { + it('should create event with error and requester', () => { + const error = new Error('Test error'); + const result = IssuersEvents.STORE_ERROR(error, 'test-requester'); + expect(result.error).toBe(error); + expect(result.requester).toBe('test-requester'); + }); + + it('should create event with error only', () => { + const error = new Error('Test error'); + const result = IssuersEvents.STORE_ERROR(error); + expect(result.error).toBe(error); + expect(result.requester).toBeUndefined(); + }); + }); + + describe('RESET_VERIFY_ERROR', () => { + it('should create empty event', () => { + const result = IssuersEvents.RESET_VERIFY_ERROR(); + expect(result).toEqual({}); + }); + }); + + describe('SELECTED_CREDENTIAL_TYPE', () => { + it('should create event with credential type', () => { + const credType = {} as unknown as CredentialTypes; + const result = IssuersEvents.SELECTED_CREDENTIAL_TYPE(credType); + expect(result).toEqual({credType}); + }); + }); + + describe('SCAN_CREDENTIAL_OFFER_QR_CODE', () => { + it('should create empty event', () => { + const result = IssuersEvents.SCAN_CREDENTIAL_OFFER_QR_CODE(); + expect(result).toEqual({}); + }); + }); + + describe('QR_CODE_SCANNED', () => { + it('should create event with data', () => { + const result = IssuersEvents.QR_CODE_SCANNED('qr-code-data'); + expect(result).toEqual({data: 'qr-code-data'}); + }); + + it('should handle JSON data', () => { + const jsonData = '{"key": "value"}'; + const result = IssuersEvents.QR_CODE_SCANNED(jsonData); + expect(result).toEqual({data: jsonData}); + }); + }); + + describe('AUTH_ENDPOINT_RECEIVED', () => { + it('should create event with auth endpoint', () => { + const result = IssuersEvents.AUTH_ENDPOINT_RECEIVED( + 'https://auth.example.com', + ); + expect(result).toEqual({authEndpoint: 'https://auth.example.com'}); + }); + }); + + describe('PROOF_REQUEST', () => { + it('should create event with all parameters', () => { + const accessToken = 'token-123'; + const cNonce = 'nonce-456'; + const issuerMetadata = {name: 'Test Issuer'}; + const issuer = {id: 'issuer-1'} as unknown as any; + const credentialtypes = {} as unknown as CredentialTypes; + + const result = IssuersEvents.PROOF_REQUEST( + accessToken, + cNonce, + issuerMetadata, + issuer, + credentialtypes, + ); + + expect(result.accessToken).toBe('token-123'); + expect(result.cNonce).toBe('nonce-456'); + expect(result.issuerMetadata).toEqual({name: 'Test Issuer'}); + expect(result.issuer).toEqual({id: 'issuer-1'}); + expect(result.credentialtypes).toBeDefined(); + }); + + it('should handle undefined cNonce', () => { + const result = IssuersEvents.PROOF_REQUEST( + 'token', + undefined, + {}, + {} as unknown as any, + {} as unknown as CredentialTypes, + ); + + expect(result.cNonce).toBeUndefined(); + }); + }); + + describe('TX_CODE_REQUEST', () => { + it('should create empty event', () => { + const result = IssuersEvents.TX_CODE_REQUEST(); + expect(result).toEqual({}); + }); + }); + + describe('TX_CODE_RECEIVED', () => { + it('should create event with txCode', () => { + const result = IssuersEvents.TX_CODE_RECEIVED('TX123456'); + expect(result).toEqual({txCode: 'TX123456'}); + }); + + it('should handle empty txCode', () => { + const result = IssuersEvents.TX_CODE_RECEIVED(''); + expect(result).toEqual({txCode: ''}); + }); + }); + + describe('ON_CONSENT_GIVEN', () => { + it('should create empty event', () => { + const result = IssuersEvents.ON_CONSENT_GIVEN(); + expect(result).toEqual({}); + }); + }); + + describe('TRUST_ISSUER_CONSENT_REQUEST', () => { + it('should create event with issuerMetadata', () => { + const issuerMetadata = { + name: 'Trusted Issuer', + url: 'https://issuer.example.com', + }; + const result = IssuersEvents.TRUST_ISSUER_CONSENT_REQUEST(issuerMetadata); + expect(result).toEqual({issuerMetadata}); + }); + + it('should handle empty issuerMetadata', () => { + const result = IssuersEvents.TRUST_ISSUER_CONSENT_REQUEST({}); + expect(result).toEqual({issuerMetadata: {}}); + }); + }); + + describe('TOKEN_REQUEST', () => { + it('should create event with tokenRequest', () => { + const tokenRequest = { + grant_type: 'authorization_code', + code: 'auth-code-123', + }; + const result = IssuersEvents.TOKEN_REQUEST(tokenRequest); + expect(result).toEqual({tokenRequest}); + }); + + it('should handle empty tokenRequest', () => { + const result = IssuersEvents.TOKEN_REQUEST({}); + expect(result).toEqual({tokenRequest: {}}); + }); + }); + + describe('IssuersEvents object structure', () => { + it('should have all expected event creators', () => { + expect(IssuersEvents.SELECTED_ISSUER).toBeDefined(); + expect(IssuersEvents.DOWNLOAD_ID).toBeDefined(); + expect(IssuersEvents.BIOMETRIC_CANCELLED).toBeDefined(); + expect(IssuersEvents.COMPLETED).toBeDefined(); + expect(IssuersEvents.TRY_AGAIN).toBeDefined(); + expect(IssuersEvents.RESET_ERROR).toBeDefined(); + expect(IssuersEvents.CHECK_KEY_PAIR).toBeDefined(); + expect(IssuersEvents.CANCEL).toBeDefined(); + expect(IssuersEvents.STORE_RESPONSE).toBeDefined(); + expect(IssuersEvents.STORE_ERROR).toBeDefined(); + expect(IssuersEvents.RESET_VERIFY_ERROR).toBeDefined(); + expect(IssuersEvents.SELECTED_CREDENTIAL_TYPE).toBeDefined(); + expect(IssuersEvents.SCAN_CREDENTIAL_OFFER_QR_CODE).toBeDefined(); + expect(IssuersEvents.QR_CODE_SCANNED).toBeDefined(); + expect(IssuersEvents.AUTH_ENDPOINT_RECEIVED).toBeDefined(); + expect(IssuersEvents.PROOF_REQUEST).toBeDefined(); + expect(IssuersEvents.TX_CODE_REQUEST).toBeDefined(); + expect(IssuersEvents.TX_CODE_RECEIVED).toBeDefined(); + expect(IssuersEvents.ON_CONSENT_GIVEN).toBeDefined(); + expect(IssuersEvents.TRUST_ISSUER_CONSENT_REQUEST).toBeDefined(); + expect(IssuersEvents.TOKEN_REQUEST).toBeDefined(); + }); + + it('should have all event creators be functions', () => { + expect(typeof IssuersEvents.SELECTED_ISSUER).toBe('function'); + expect(typeof IssuersEvents.DOWNLOAD_ID).toBe('function'); + expect(typeof IssuersEvents.BIOMETRIC_CANCELLED).toBe('function'); + expect(typeof IssuersEvents.COMPLETED).toBe('function'); + expect(typeof IssuersEvents.TRY_AGAIN).toBe('function'); + }); + }); +}); diff --git a/machines/Issuers/IssuersModel.test.ts b/machines/Issuers/IssuersModel.test.ts new file mode 100644 index 00000000..a2e6cb3e --- /dev/null +++ b/machines/Issuers/IssuersModel.test.ts @@ -0,0 +1,301 @@ +import {IssuersModel} from './IssuersModel'; + +describe('IssuersModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(IssuersModel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(IssuersModel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(IssuersModel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = IssuersModel.initialContext; + + it('should have issuers as empty array', () => { + expect(initialContext.issuers).toEqual([]); + expect(Array.isArray(initialContext.issuers)).toBe(true); + }); + + it('should have selectedIssuerId as empty string', () => { + expect(initialContext.selectedIssuerId).toBe(''); + }); + + it('should have qrData as empty string', () => { + expect(initialContext.qrData).toBe(''); + }); + + it('should have selectedIssuer as empty object', () => { + expect(initialContext.selectedIssuer).toEqual({}); + }); + + it('should have selectedIssuerWellknownResponse as empty object', () => { + expect(initialContext.selectedIssuerWellknownResponse).toEqual({}); + }); + + it('should have tokenResponse as empty object', () => { + expect(initialContext.tokenResponse).toEqual({}); + }); + + it('should have errorMessage as empty string', () => { + expect(initialContext.errorMessage).toBe(''); + }); + + it('should have loadingReason as displayIssuers', () => { + expect(initialContext.loadingReason).toBe('displayIssuers'); + }); + + it('should have verifiableCredential as null', () => { + expect(initialContext.verifiableCredential).toBeNull(); + }); + + it('should have selectedCredentialType as empty object', () => { + expect(initialContext.selectedCredentialType).toEqual({}); + }); + + it('should have supportedCredentialTypes as empty array', () => { + expect(initialContext.supportedCredentialTypes).toEqual([]); + expect(Array.isArray(initialContext.supportedCredentialTypes)).toBe(true); + }); + + it('should have credentialWrapper as empty object', () => { + expect(initialContext.credentialWrapper).toEqual({}); + }); + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + }); + + it('should have verificationErrorMessage as empty string', () => { + expect(initialContext.verificationErrorMessage).toBe(''); + }); + + it('should have publicKey as empty string', () => { + expect(initialContext.publicKey).toBe(''); + }); + + it('should have privateKey as empty string', () => { + expect(initialContext.privateKey).toBe(''); + }); + + it('should have vcMetadata as empty object', () => { + expect(initialContext.vcMetadata).toEqual({}); + }); + + it('should have keyType as RS256', () => { + expect(initialContext.keyType).toBe('RS256'); + }); + + it('should have wellknownKeyTypes as empty array', () => { + expect(initialContext.wellknownKeyTypes).toEqual([]); + expect(Array.isArray(initialContext.wellknownKeyTypes)).toBe(true); + }); + + it('should have authEndpointToOpen as false', () => { + expect(initialContext.authEndpointToOpen).toBe(false); + }); + + it('should have isTransactionCodeRequested as false', () => { + expect(initialContext.isTransactionCodeRequested).toBe(false); + }); + + it('should have authEndpoint as empty string', () => { + expect(initialContext.authEndpoint).toBe(''); + }); + + it('should have accessToken as empty string', () => { + expect(initialContext.accessToken).toBe(''); + }); + + it('should have txCode as empty string', () => { + expect(initialContext.txCode).toBe(''); + }); + + it('should have cNonce as empty string', () => { + expect(initialContext.cNonce).toBe(''); + }); + + it('should have isConsentRequested as false', () => { + expect(initialContext.isConsentRequested).toBe(false); + }); + + it('should have issuerLogo as empty string', () => { + expect(initialContext.issuerLogo).toBe(''); + }); + + it('should have issuerName as empty string', () => { + expect(initialContext.issuerName).toBe(''); + }); + + it('should have txCodeInputMode as empty string', () => { + expect(initialContext.txCodeInputMode).toBe(''); + }); + + it('should have txCodeDescription as empty string', () => { + expect(initialContext.txCodeDescription).toBe(''); + }); + + it('should have txCodeLength as null', () => { + expect(initialContext.txCodeLength).toBeNull(); + }); + + it('should have isCredentialOfferFlow as false', () => { + expect(initialContext.isCredentialOfferFlow).toBe(false); + }); + + it('should have tokenRequestObject as empty object', () => { + expect(initialContext.tokenRequestObject).toEqual({}); + }); + + it('should have credentialConfigurationId as empty string', () => { + expect(initialContext.credentialConfigurationId).toBe(''); + }); + + it('should have all 35 required properties', () => { + const properties = Object.keys(initialContext); + expect(properties).toHaveLength(35); + }); + }); + + describe('String properties', () => { + const context = IssuersModel.initialContext; + + it('all empty string properties should be empty', () => { + const emptyStrings = [ + context.selectedIssuerId, + context.qrData, + context.errorMessage, + context.verificationErrorMessage, + context.publicKey, + context.privateKey, + context.authEndpoint, + context.accessToken, + context.txCode, + context.cNonce, + context.issuerLogo, + context.issuerName, + context.txCodeInputMode, + context.txCodeDescription, + context.credentialConfigurationId, + ]; + + emptyStrings.forEach(str => { + expect(str).toBe(''); + expect(typeof str).toBe('string'); + }); + }); + + it('loadingReason should be displayIssuers', () => { + expect(context.loadingReason).toBe('displayIssuers'); + expect(typeof context.loadingReason).toBe('string'); + }); + + it('keyType should be RS256', () => { + expect(context.keyType).toBe('RS256'); + expect(typeof context.keyType).toBe('string'); + }); + }); + + describe('Array properties', () => { + const context = IssuersModel.initialContext; + + it('all array properties should be empty arrays', () => { + const arrays = [ + context.issuers, + context.supportedCredentialTypes, + context.wellknownKeyTypes, + ]; + + arrays.forEach(arr => { + expect(Array.isArray(arr)).toBe(true); + expect(arr).toHaveLength(0); + }); + }); + }); + + describe('Object properties', () => { + const context = IssuersModel.initialContext; + + it('all object properties should be empty objects or as specified', () => { + const emptyObjects = [ + context.selectedIssuer, + context.selectedIssuerWellknownResponse, + context.tokenResponse, + context.selectedCredentialType, + context.credentialWrapper, + context.serviceRefs, + context.vcMetadata, + context.tokenRequestObject, + ]; + + emptyObjects.forEach(obj => { + expect(typeof obj).toBe('object'); + expect(Object.keys(obj)).toHaveLength(0); + }); + }); + }); + + describe('Boolean properties', () => { + const context = IssuersModel.initialContext; + + it('all boolean properties should be false initially', () => { + const booleans = [ + context.authEndpointToOpen, + context.isTransactionCodeRequested, + context.isConsentRequested, + context.isCredentialOfferFlow, + ]; + + booleans.forEach(bool => { + expect(bool).toBe(false); + expect(typeof bool).toBe('boolean'); + }); + }); + }); + + describe('Null properties', () => { + const context = IssuersModel.initialContext; + + it('verifiableCredential should be null', () => { + expect(context.verifiableCredential).toBeNull(); + }); + + it('txCodeLength should be null', () => { + expect(context.txCodeLength).toBeNull(); + }); + }); + + describe('Model events', () => { + it('should have events object', () => { + expect(IssuersModel.events).toBeDefined(); + expect(typeof IssuersModel.events).toBe('object'); + }); + + it('should have event creators', () => { + const eventKeys = Object.keys(IssuersModel.events); + expect(eventKeys.length).toBeGreaterThan(0); + }); + }); + + describe('Property types', () => { + const context = IssuersModel.initialContext; + + it('should have correct types for all properties', () => { + expect(typeof context.selectedIssuerId).toBe('string'); + expect(typeof context.qrData).toBe('string'); + expect(typeof context.errorMessage).toBe('string'); + expect(typeof context.loadingReason).toBe('string'); + expect(typeof context.keyType).toBe('string'); + expect(typeof context.authEndpointToOpen).toBe('boolean'); + expect(typeof context.isTransactionCodeRequested).toBe('boolean'); + expect(Array.isArray(context.issuers)).toBe(true); + expect(typeof context.selectedIssuer).toBe('object'); + }); + }); +}); diff --git a/machines/Issuers/IssuersSelectors.test.ts b/machines/Issuers/IssuersSelectors.test.ts new file mode 100644 index 00000000..c86a0b03 --- /dev/null +++ b/machines/Issuers/IssuersSelectors.test.ts @@ -0,0 +1,372 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectIssuers, + selectSelectedIssuer, + selectAuthWebViewStatus, + selectAuthEndPoint, + selectErrorMessageType, + selectLoadingReason, + selectIsDownloadCredentials, + selectIsTxCodeRequested, + selectIsConsentRequested, + selectIssuerLogo, + selectIssuerName, + selectTxCodeDisplayDetails, + selectIsNonGenericError, + selectIsDone, + selectIsIdle, + selectStoring, + selectIsError, + selectVerificationErrorMessage, + selectSelectingCredentialType, + selectSupportedCredentialTypes, + selectIsQrScanning, +} from './IssuersSelectors'; + +describe('IssuersSelectors', () => { + const mockState: any = { + context: { + issuers: [{issuer_id: '1'}, {issuer_id: '2'}], + selectedIssuer: {issuer_id: '1', credential_issuer: 'test.example.com'}, + authEndpointToOpen: 'https://auth.example.com', + authEndpoint: 'https://auth.example.com/authorize', + errorMessage: 'Test error', + loadingReason: 'Loading data', + isTransactionCodeRequested: true, + isConsentRequested: false, + issuerLogo: 'https://example.com/logo.png', + issuerName: 'Test Issuer', + txCodeInputMode: 'numeric', + txCodeDescription: 'Enter transaction code', + txCodeLength: 6, + }, + matches: jest.fn( + (stateName: string) => stateName === 'downloadCredentials', + ), + }; + + describe('selectIssuers', () => { + it('should return issuers from context', () => { + const result = selectIssuers(mockState); + expect(result).toEqual(mockState.context.issuers); + }); + + it('should return array of issuers', () => { + const result = selectIssuers(mockState); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + }); + }); + + describe('selectSelectedIssuer', () => { + it('should return selected issuer from context', () => { + const result = selectSelectedIssuer(mockState); + expect(result).toEqual(mockState.context.selectedIssuer); + }); + + it('should return issuer with issuer_id property', () => { + const result = selectSelectedIssuer(mockState); + expect(result.issuer_id).toBe('1'); + expect(result.credential_issuer).toBe('test.example.com'); + }); + }); + + describe('selectAuthWebViewStatus', () => { + it('should return auth endpoint to open', () => { + const result = selectAuthWebViewStatus(mockState); + expect(result).toBe('https://auth.example.com'); + }); + }); + + describe('selectAuthEndPoint', () => { + it('should return auth endpoint', () => { + const result = selectAuthEndPoint(mockState); + expect(result).toBe('https://auth.example.com/authorize'); + }); + }); + + describe('selectErrorMessageType', () => { + it('should return error message from context', () => { + const result = selectErrorMessageType(mockState); + expect(result).toBe('Test error'); + }); + }); + + describe('selectLoadingReason', () => { + it('should return loading reason from context', () => { + const result = selectLoadingReason(mockState); + expect(result).toBe('Loading data'); + }); + }); + + describe('selectIsDownloadCredentials', () => { + it('should return true when in downloadCredentials state', () => { + const result = selectIsDownloadCredentials(mockState); + expect(result).toBe(true); + }); + + it('should call matches with correct state name', () => { + selectIsDownloadCredentials(mockState); + expect(mockState.matches).toHaveBeenCalledWith('downloadCredentials'); + }); + }); + + describe('selectIsTxCodeRequested', () => { + it('should return transaction code requested status', () => { + const result = selectIsTxCodeRequested(mockState); + expect(result).toBe(true); + }); + + it('should return false when not requested', () => { + const stateWithFalse: any = { + ...mockState, + context: {...mockState.context, isTransactionCodeRequested: false}, + }; + const result = selectIsTxCodeRequested(stateWithFalse); + expect(result).toBe(false); + }); + }); + + describe('selectIsConsentRequested', () => { + it('should return consent requested status', () => { + const result = selectIsConsentRequested(mockState); + expect(result).toBe(false); + }); + + it('should return true when consent requested', () => { + const stateWithConsent: any = { + ...mockState, + context: {...mockState.context, isConsentRequested: true}, + }; + const result = selectIsConsentRequested(stateWithConsent); + expect(result).toBe(true); + }); + }); + + describe('selectIssuerLogo', () => { + it('should return issuer logo URL', () => { + const result = selectIssuerLogo(mockState); + expect(result).toBe('https://example.com/logo.png'); + }); + }); + + describe('selectIssuerName', () => { + it('should return issuer name', () => { + const result = selectIssuerName(mockState); + expect(result).toBe('Test Issuer'); + }); + }); + + describe('selectTxCodeDisplayDetails', () => { + it('should return transaction code display details', () => { + const result = selectTxCodeDisplayDetails(mockState); + expect(result).toEqual({ + inputMode: 'numeric', + description: 'Enter transaction code', + length: 6, + }); + }); + + it('should have all required properties', () => { + const result = selectTxCodeDisplayDetails(mockState); + expect(result).toHaveProperty('inputMode'); + expect(result).toHaveProperty('description'); + expect(result).toHaveProperty('length'); + }); + + it('should return correct input mode', () => { + const result = selectTxCodeDisplayDetails(mockState); + expect(result.inputMode).toBe('numeric'); + }); + + it('should return correct description', () => { + const result = selectTxCodeDisplayDetails(mockState); + expect(result.description).toBe('Enter transaction code'); + }); + + it('should return correct length', () => { + const result = selectTxCodeDisplayDetails(mockState); + expect(result.length).toBe(6); + }); + }); + + describe('Selectors with empty/null values', () => { + const emptyState: any = { + context: { + issuers: [], + selectedIssuer: null, + authEndpointToOpen: '', + authEndpoint: '', + errorMessage: '', + loadingReason: '', + isTransactionCodeRequested: false, + isConsentRequested: false, + issuerLogo: '', + issuerName: '', + txCodeInputMode: '', + txCodeDescription: '', + txCodeLength: 0, + }, + matches: jest.fn(() => false), + }; + + it('should handle empty issuers array', () => { + const result = selectIssuers(emptyState); + expect(result).toEqual([]); + }); + + it('should handle null selected issuer', () => { + const result = selectSelectedIssuer(emptyState); + expect(result).toBeNull(); + }); + + it('should handle empty strings', () => { + expect(selectAuthWebViewStatus(emptyState)).toBe(''); + expect(selectAuthEndPoint(emptyState)).toBe(''); + expect(selectErrorMessageType(emptyState)).toBe(''); + expect(selectLoadingReason(emptyState)).toBe(''); + expect(selectIssuerLogo(emptyState)).toBe(''); + expect(selectIssuerName(emptyState)).toBe(''); + }); + + it('should handle false boolean values', () => { + expect(selectIsTxCodeRequested(emptyState)).toBe(false); + expect(selectIsConsentRequested(emptyState)).toBe(false); + expect(selectIsDownloadCredentials(emptyState)).toBe(false); + }); + + it('should handle zero length', () => { + const result = selectTxCodeDisplayDetails(emptyState); + expect(result.length).toBe(0); + }); + }); + + describe('selectIsNonGenericError', () => { + it('should return true when error message is not generic and not empty', () => { + const stateWithError: any = { + context: {errorMessage: 'NETWORK_ERROR'}, + }; + const result = selectIsNonGenericError(stateWithError); + expect(result).toBe(true); + }); + + it('should return false when error message is GENERIC', () => { + const stateWithGenericError: any = { + context: {errorMessage: 'generic'}, + }; + const result = selectIsNonGenericError(stateWithGenericError); + expect(result).toBe(false); + }); + + it('should return false when error message is empty', () => { + const stateWithNoError: any = { + context: {errorMessage: ''}, + }; + const result = selectIsNonGenericError(stateWithNoError); + expect(result).toBe(false); + }); + }); + + describe('selectIsDone', () => { + it('should return true when state matches done', () => { + const doneState: any = { + matches: jest.fn((name: string) => name === 'done'), + }; + const result = selectIsDone(doneState); + expect(result).toBe(true); + expect(doneState.matches).toHaveBeenCalledWith('done'); + }); + + it('should return false when state does not match done', () => { + const notDoneState: any = { + matches: jest.fn(() => false), + }; + const result = selectIsDone(notDoneState); + expect(result).toBe(false); + }); + }); + + describe('selectIsIdle', () => { + it('should return true when state matches idle', () => { + const idleState: any = { + matches: jest.fn((name: string) => name === 'idle'), + }; + const result = selectIsIdle(idleState); + expect(result).toBe(true); + expect(idleState.matches).toHaveBeenCalledWith('idle'); + }); + }); + + describe('selectStoring', () => { + it('should return true when state matches storing', () => { + const storingState: any = { + matches: jest.fn((name: string) => name === 'storing'), + }; + const result = selectStoring(storingState); + expect(result).toBe(true); + expect(storingState.matches).toHaveBeenCalledWith('storing'); + }); + }); + + describe('selectIsError', () => { + it('should return true when state matches error', () => { + const errorState: any = { + matches: jest.fn((name: string) => name === 'error'), + }; + const result = selectIsError(errorState); + expect(result).toBe(true); + expect(errorState.matches).toHaveBeenCalledWith('error'); + }); + }); + + describe('selectVerificationErrorMessage', () => { + it('should return verification error message', () => { + const stateWithVerificationError: any = { + context: {verificationErrorMessage: 'Signature verification failed'}, + }; + const result = selectVerificationErrorMessage(stateWithVerificationError); + expect(result).toBe('Signature verification failed'); + }); + }); + + describe('selectSelectingCredentialType', () => { + it('should return true when selecting credential type', () => { + const selectingState: any = { + matches: jest.fn((name: string) => name === 'selectingCredentialType'), + }; + const result = selectSelectingCredentialType(selectingState); + expect(result).toBe(true); + expect(selectingState.matches).toHaveBeenCalledWith( + 'selectingCredentialType', + ); + }); + }); + + describe('selectSupportedCredentialTypes', () => { + it('should return supported credential types', () => { + const stateWithCredTypes: any = { + context: { + supportedCredentialTypes: [ + {id: 'type1', name: 'Type 1'}, + {id: 'type2', name: 'Type 2'}, + ], + }, + }; + const result = selectSupportedCredentialTypes(stateWithCredTypes); + expect(result).toHaveLength(2); + expect(result[0].id).toBe('type1'); + expect(result[1].id).toBe('type2'); + }); + }); + + describe('selectIsQrScanning', () => { + it('should return true when waiting for QR scan', () => { + const scanningState: any = { + matches: jest.fn((name: string) => name === 'waitingForQrScan'), + }; + const result = selectIsQrScanning(scanningState); + expect(result).toBe(true); + expect(scanningState.matches).toHaveBeenCalledWith('waitingForQrScan'); + }); + }); +}); diff --git a/machines/QrLogin/QrLoginModel.test.ts b/machines/QrLogin/QrLoginModel.test.ts new file mode 100644 index 00000000..5c186c3f --- /dev/null +++ b/machines/QrLogin/QrLoginModel.test.ts @@ -0,0 +1,332 @@ +import {QrLoginmodel} from './QrLoginModel'; +import {VCShareFlowType} from '../../shared/Utils'; + +describe('QrLoginModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(QrLoginmodel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(QrLoginmodel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(QrLoginmodel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = QrLoginmodel.initialContext; + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + expect(typeof initialContext.serviceRefs).toBe('object'); + }); + + it('should have selectedVc as empty object', () => { + expect(initialContext.selectedVc).toEqual({}); + expect(typeof initialContext.selectedVc).toBe('object'); + }); + + it('should have linkCode as empty string', () => { + expect(initialContext.linkCode).toBe(''); + expect(typeof initialContext.linkCode).toBe('string'); + }); + + it('should have flowType as SIMPLE_SHARE', () => { + expect(initialContext.flowType).toBe(VCShareFlowType.SIMPLE_SHARE); + }); + + it('should have myVcs as empty array', () => { + expect(initialContext.myVcs).toEqual([]); + expect(Array.isArray(initialContext.myVcs)).toBe(true); + expect(initialContext.myVcs).toHaveLength(0); + }); + + it('should have thumbprint as empty string', () => { + expect(initialContext.thumbprint).toBe(''); + expect(typeof initialContext.thumbprint).toBe('string'); + }); + + it('should have linkTransactionResponse as empty object', () => { + expect(initialContext.linkTransactionResponse).toEqual({}); + expect(typeof initialContext.linkTransactionResponse).toBe('object'); + }); + + it('should have authFactors as empty array', () => { + expect(initialContext.authFactors).toEqual([]); + expect(Array.isArray(initialContext.authFactors)).toBe(true); + }); + + it('should have authorizeScopes as null', () => { + expect(initialContext.authorizeScopes).toBeNull(); + }); + + it('should have clientName as empty object', () => { + expect(initialContext.clientName).toEqual({}); + expect(typeof initialContext.clientName).toBe('object'); + }); + + it('should have configs as empty object', () => { + expect(initialContext.configs).toEqual({}); + expect(typeof initialContext.configs).toBe('object'); + }); + + it('should have essentialClaims as empty array', () => { + expect(initialContext.essentialClaims).toEqual([]); + expect(Array.isArray(initialContext.essentialClaims)).toBe(true); + }); + + it('should have linkTransactionId as empty string', () => { + expect(initialContext.linkTransactionId).toBe(''); + expect(typeof initialContext.linkTransactionId).toBe('string'); + }); + + it('should have logoUrl as empty string', () => { + expect(initialContext.logoUrl).toBe(''); + expect(typeof initialContext.logoUrl).toBe('string'); + }); + + it('should have voluntaryClaims as empty array', () => { + expect(initialContext.voluntaryClaims).toEqual([]); + expect(Array.isArray(initialContext.voluntaryClaims)).toBe(true); + }); + + it('should have selectedVoluntaryClaims as empty array', () => { + expect(initialContext.selectedVoluntaryClaims).toEqual([]); + expect(Array.isArray(initialContext.selectedVoluntaryClaims)).toBe(true); + }); + + it('should have errorMessage as empty string', () => { + expect(initialContext.errorMessage).toBe(''); + expect(typeof initialContext.errorMessage).toBe('string'); + }); + + it('should have domainName as empty string', () => { + expect(initialContext.domainName).toBe(''); + expect(typeof initialContext.domainName).toBe('string'); + }); + + it('should have consentClaims with name and picture', () => { + expect(initialContext.consentClaims).toEqual(['name', 'picture']); + expect(Array.isArray(initialContext.consentClaims)).toBe(true); + expect(initialContext.consentClaims).toHaveLength(2); + expect(initialContext.consentClaims).toContain('name'); + expect(initialContext.consentClaims).toContain('picture'); + }); + + it('should have isSharing as empty object', () => { + expect(initialContext.isSharing).toEqual({}); + expect(typeof initialContext.isSharing).toBe('object'); + }); + + it('should have linkedTransactionId as empty string', () => { + expect(initialContext.linkedTransactionId).toBe(''); + expect(typeof initialContext.linkedTransactionId).toBe('string'); + }); + + it('should have showFaceAuthConsent as true', () => { + expect(initialContext.showFaceAuthConsent).toBe(true); + expect(typeof initialContext.showFaceAuthConsent).toBe('boolean'); + }); + + it('should have isQrLoginViaDeepLink as false', () => { + expect(initialContext.isQrLoginViaDeepLink).toBe(false); + expect(typeof initialContext.isQrLoginViaDeepLink).toBe('boolean'); + }); + + it('should have all required properties', () => { + const requiredProps = [ + 'serviceRefs', + 'selectedVc', + 'linkCode', + 'flowType', + 'myVcs', + 'thumbprint', + 'linkTransactionResponse', + 'authFactors', + 'authorizeScopes', + 'clientName', + 'configs', + 'essentialClaims', + 'linkTransactionId', + 'logoUrl', + 'voluntaryClaims', + 'selectedVoluntaryClaims', + 'errorMessage', + 'domainName', + 'consentClaims', + 'isSharing', + 'linkedTransactionId', + 'showFaceAuthConsent', + 'isQrLoginViaDeepLink', + ]; + + requiredProps.forEach(prop => { + expect(initialContext).toHaveProperty(prop); + }); + }); + + it('should have exactly 23 properties in initial context', () => { + const propertyCount = Object.keys(initialContext).length; + expect(propertyCount).toBe(23); + }); + }); + + describe('Model events', () => { + it('should have events object defined', () => { + expect(QrLoginmodel.events).toBeDefined(); + expect(typeof QrLoginmodel.events).toBe('object'); + }); + + it('should have SELECT_VC event', () => { + expect(QrLoginmodel.events.SELECT_VC).toBeDefined(); + expect(typeof QrLoginmodel.events.SELECT_VC).toBe('function'); + }); + + it('should have SCANNING_DONE event', () => { + expect(QrLoginmodel.events.SCANNING_DONE).toBeDefined(); + expect(typeof QrLoginmodel.events.SCANNING_DONE).toBe('function'); + }); + + it('should have STORE_RESPONSE event', () => { + expect(QrLoginmodel.events.STORE_RESPONSE).toBeDefined(); + expect(typeof QrLoginmodel.events.STORE_RESPONSE).toBe('function'); + }); + + it('should have STORE_ERROR event', () => { + expect(QrLoginmodel.events.STORE_ERROR).toBeDefined(); + expect(typeof QrLoginmodel.events.STORE_ERROR).toBe('function'); + }); + + it('should have TOGGLE_CONSENT_CLAIM event', () => { + expect(QrLoginmodel.events.TOGGLE_CONSENT_CLAIM).toBeDefined(); + expect(typeof QrLoginmodel.events.TOGGLE_CONSENT_CLAIM).toBe('function'); + }); + + it('should have DISMISS event', () => { + expect(QrLoginmodel.events.DISMISS).toBeDefined(); + expect(typeof QrLoginmodel.events.DISMISS).toBe('function'); + }); + + it('should have CONFIRM event', () => { + expect(QrLoginmodel.events.CONFIRM).toBeDefined(); + expect(typeof QrLoginmodel.events.CONFIRM).toBe('function'); + }); + + it('should have GET event', () => { + expect(QrLoginmodel.events.GET).toBeDefined(); + expect(typeof QrLoginmodel.events.GET).toBe('function'); + }); + + it('should have VERIFY event', () => { + expect(QrLoginmodel.events.VERIFY).toBeDefined(); + expect(typeof QrLoginmodel.events.VERIFY).toBe('function'); + }); + + it('should have CANCEL event', () => { + expect(QrLoginmodel.events.CANCEL).toBeDefined(); + expect(typeof QrLoginmodel.events.CANCEL).toBe('function'); + }); + + it('should have FACE_VALID event', () => { + expect(QrLoginmodel.events.FACE_VALID).toBeDefined(); + expect(typeof QrLoginmodel.events.FACE_VALID).toBe('function'); + }); + + it('should have FACE_INVALID event', () => { + expect(QrLoginmodel.events.FACE_INVALID).toBeDefined(); + expect(typeof QrLoginmodel.events.FACE_INVALID).toBe('function'); + }); + + it('should have RETRY_VERIFICATION event', () => { + expect(QrLoginmodel.events.RETRY_VERIFICATION).toBeDefined(); + expect(typeof QrLoginmodel.events.RETRY_VERIFICATION).toBe('function'); + }); + + it('should have FACE_VERIFICATION_CONSENT event', () => { + expect(QrLoginmodel.events.FACE_VERIFICATION_CONSENT).toBeDefined(); + expect(typeof QrLoginmodel.events.FACE_VERIFICATION_CONSENT).toBe( + 'function', + ); + }); + }); + + describe('String properties', () => { + const context = QrLoginmodel.initialContext; + + it('all string properties should be empty strings initially', () => { + const stringProps = [ + context.linkCode, + context.thumbprint, + context.linkTransactionId, + context.logoUrl, + context.errorMessage, + context.domainName, + context.linkedTransactionId, + ]; + + stringProps.forEach(prop => { + expect(prop).toBe(''); + expect(typeof prop).toBe('string'); + }); + }); + }); + + describe('Array properties', () => { + const context = QrLoginmodel.initialContext; + + it('empty array properties should have length 0', () => { + const emptyArrays = [ + context.myVcs, + context.authFactors, + context.essentialClaims, + context.voluntaryClaims, + context.selectedVoluntaryClaims, + ]; + + emptyArrays.forEach(arr => { + expect(Array.isArray(arr)).toBe(true); + expect(arr).toHaveLength(0); + }); + }); + + it('consentClaims should have length 2', () => { + expect(context.consentClaims).toHaveLength(2); + expect(Array.isArray(context.consentClaims)).toBe(true); + }); + }); + + describe('Object properties', () => { + const context = QrLoginmodel.initialContext; + + it('empty object properties should have no keys', () => { + const emptyObjects = [ + context.serviceRefs, + context.selectedVc, + context.linkTransactionResponse, + context.clientName, + context.configs, + context.isSharing, + ]; + + emptyObjects.forEach(obj => { + expect(typeof obj).toBe('object'); + expect(Object.keys(obj)).toHaveLength(0); + }); + }); + }); + + describe('Boolean properties', () => { + const context = QrLoginmodel.initialContext; + + it('showFaceAuthConsent should be true', () => { + expect(context.showFaceAuthConsent).toBe(true); + }); + + it('isQrLoginViaDeepLink should be false', () => { + expect(context.isQrLoginViaDeepLink).toBe(false); + }); + }); +}); diff --git a/machines/QrLogin/QrLoginSelectors.test.ts b/machines/QrLogin/QrLoginSelectors.test.ts new file mode 100644 index 00000000..8f82cc8f --- /dev/null +++ b/machines/QrLogin/QrLoginSelectors.test.ts @@ -0,0 +1,512 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectMyVcs, + selectIsWaitingForData, + selectDomainName, + selectIsLinkTransaction, + selectIsloadMyVcs, + selectIsShowingVcList, + selectIsisVerifyingIdentity, + selectIsInvalidIdentity, + selectIsShowError, + selectIsRequestConsent, + selectIsSendingAuthenticate, + selectIsSendingConsent, + selectIsVerifyingSuccesful, + selectCredential, + selectVerifiableCredentialData, + selectLinkTransactionResponse, + selectEssentialClaims, + selectVoluntaryClaims, + selectLogoUrl, + selectClientName, + selectErrorMessage, + selectIsSharing, + selectIsQrLoginViaDeepLink, + selectIsFaceVerificationConsent, +} from './QrLoginSelectors'; + +describe('QrLoginSelectors', () => { + const mockVc = { + vcMetadata: {id: 'vc1', displayName: 'Test VC'}, + credential: {}, + }; + + const mockState: any = { + context: { + myVcs: [mockVc], + domainName: 'example.com', + selectedVc: mockVc, + senderInfo: {name: 'Test Sender'}, + linkTransactionResponse: {status: 'success'}, + verifiableCredentialData: {type: 'VerifiableCredential'}, + connectionParams: {uri: 'https://connect.example.com'}, + }, + matches: jest.fn((stateName: string) => stateName === 'waitingForData'), + }; + + describe('selectMyVcs', () => { + it('should return my VCs from context', () => { + const result = selectMyVcs(mockState); + expect(result).toEqual([mockVc]); + }); + + it('should return array of VCs', () => { + const result = selectMyVcs(mockState); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + }); + }); + + describe('selectIsWaitingForData', () => { + it('should return true when in waitingForData state', () => { + const result = selectIsWaitingForData(mockState); + expect(result).toBe(true); + }); + + it('should call matches with correct state', () => { + selectIsWaitingForData(mockState); + expect(mockState.matches).toHaveBeenCalledWith('waitingForData'); + }); + + it('should return false when not in waitingForData state', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + const result = selectIsWaitingForData(state); + expect(result).toBe(false); + }); + }); + + describe('selectDomainName', () => { + it('should return domain name from context', () => { + const result = selectDomainName(mockState); + expect(result).toBe('example.com'); + }); + }); + + describe('selectIsLinkTransaction', () => { + it('should return true when in linkTransaction state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'linkTransaction'), + }; + const result = selectIsLinkTransaction(state); + expect(result).toBe(true); + }); + + it('should call matches with linkTransaction', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsLinkTransaction(state); + expect(state.matches).toHaveBeenCalledWith('linkTransaction'); + }); + }); + + describe('selectIsloadMyVcs', () => { + it('should return true when in loadMyVcs state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'loadMyVcs'), + }; + const result = selectIsloadMyVcs(state); + expect(result).toBe(true); + }); + + it('should call matches with loadMyVcs', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsloadMyVcs(state); + expect(state.matches).toHaveBeenCalledWith('loadMyVcs'); + }); + }); + + describe('selectIsShowingVcList', () => { + it('should return true when in showvcList state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'showvcList'), + }; + const result = selectIsShowingVcList(state); + expect(result).toBe(true); + }); + + it('should call matches with showvcList', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsShowingVcList(state); + expect(state.matches).toHaveBeenCalledWith('showvcList'); + }); + }); + + describe('selectIsisVerifyingIdentity', () => { + it('should return true when in faceAuth state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'faceAuth'), + }; + const result = selectIsisVerifyingIdentity(state); + expect(result).toBe(true); + }); + + it('should call matches with faceAuth', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsisVerifyingIdentity(state); + expect(state.matches).toHaveBeenCalledWith('faceAuth'); + }); + }); + + describe('selectIsInvalidIdentity', () => { + it('should return true when in invalidIdentity state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'invalidIdentity'), + }; + const result = selectIsInvalidIdentity(state); + expect(result).toBe(true); + }); + + it('should call matches with invalidIdentity', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsInvalidIdentity(state); + expect(state.matches).toHaveBeenCalledWith('invalidIdentity'); + }); + }); + + describe('selectIsShowError', () => { + it('should return true when in ShowError state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'ShowError'), + }; + const result = selectIsShowError(state); + expect(result).toBe(true); + }); + + it('should call matches with ShowError', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsShowError(state); + expect(state.matches).toHaveBeenCalledWith('ShowError'); + }); + }); + + describe('selectIsRequestConsent', () => { + it('should return true when in requestConsent state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'requestConsent'), + }; + const result = selectIsRequestConsent(state); + expect(result).toBe(true); + }); + + it('should call matches with requestConsent', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsRequestConsent(state); + expect(state.matches).toHaveBeenCalledWith('requestConsent'); + }); + }); + + describe('selectIsSendingAuthenticate', () => { + it('should return true when in sendingAuthenticate state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'sendingAuthenticate'), + }; + const result = selectIsSendingAuthenticate(state); + expect(result).toBe(true); + }); + + it('should call matches with sendingAuthenticate', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsSendingAuthenticate(state); + expect(state.matches).toHaveBeenCalledWith('sendingAuthenticate'); + }); + }); + + describe('selectIsSendingConsent', () => { + it('should return true when in sendingConsent state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'sendingConsent'), + }; + const result = selectIsSendingConsent(state); + expect(result).toBe(true); + }); + + it('should call matches with sendingConsent', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsSendingConsent(state); + expect(state.matches).toHaveBeenCalledWith('sendingConsent'); + }); + }); + + describe('selectIsVerifyingSuccesful', () => { + it('should return true when in success state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'success'), + }; + const result = selectIsVerifyingSuccesful(state); + expect(result).toBe(true); + }); + + it('should call matches with success', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsVerifyingSuccesful(state); + expect(state.matches).toHaveBeenCalledWith('success'); + }); + }); + + describe('selectCredential', () => { + it('should return credential from selectedVc', () => { + const result = selectCredential(mockState); + expect(result).toBeDefined(); + }); + + it('should handle null selectedVc', () => { + const state: any = { + ...mockState, + context: {...mockState.context, selectedVc: null}, + }; + const result = selectCredential(state); + expect(result).toBeUndefined(); + }); + }); + + describe('selectVerifiableCredentialData', () => { + it('should return verifiable credential data', () => { + const result = selectVerifiableCredentialData(mockState); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + }); + + it('should include vcMetadata, issuer, and issuerLogo', () => { + const result = selectVerifiableCredentialData(mockState); + expect(result[0]).toHaveProperty('vcMetadata'); + expect(result[0]).toHaveProperty('issuer'); + expect(result[0]).toHaveProperty('issuerLogo'); + }); + }); + + describe('selectLinkTransactionResponse', () => { + it('should return link transaction response from context', () => { + const result = selectLinkTransactionResponse(mockState); + expect(result).toEqual({status: 'success'}); + }); + }); + + describe('selectEssentialClaims', () => { + const stateWithClaims: any = { + ...mockState, + context: {...mockState.context, essentialClaims: ['name', 'age']}, + }; + + it('should return essential claims from context', () => { + const result = selectEssentialClaims(stateWithClaims); + expect(result).toEqual(['name', 'age']); + }); + }); + + describe('selectVoluntaryClaims', () => { + const stateWithClaims: any = { + ...mockState, + context: {...mockState.context, voluntaryClaims: ['email']}, + }; + + it('should return voluntary claims from context', () => { + const result = selectVoluntaryClaims(stateWithClaims); + expect(result).toEqual(['email']); + }); + }); + + describe('selectLogoUrl', () => { + const stateWithLogo: any = { + ...mockState, + context: {...mockState.context, logoUrl: 'https://example.com/logo.png'}, + }; + + it('should return logo URL from context', () => { + const result = selectLogoUrl(stateWithLogo); + expect(result).toBe('https://example.com/logo.png'); + }); + }); + + describe('selectClientName', () => { + const stateWithClient: any = { + ...mockState, + context: {...mockState.context, clientName: 'Test Client'}, + }; + + it('should return client name from context', () => { + const result = selectClientName(stateWithClient); + expect(result).toBe('Test Client'); + }); + }); + + describe('selectErrorMessage', () => { + const stateWithError: any = { + ...mockState, + context: {...mockState.context, errorMessage: 'Test error'}, + }; + + it('should return error message from context', () => { + const result = selectErrorMessage(stateWithError); + expect(result).toBe('Test error'); + }); + }); + + describe('selectIsSharing', () => { + const stateWithSharing: any = { + ...mockState, + context: {...mockState.context, isSharing: true}, + }; + + it('should return sharing status from context', () => { + const result = selectIsSharing(stateWithSharing); + expect(result).toBe(true); + }); + + it('should return false when not sharing', () => { + const state: any = { + ...mockState, + context: {...mockState.context, isSharing: false}, + }; + const result = selectIsSharing(state); + expect(result).toBe(false); + }); + }); + + describe('selectIsQrLoginViaDeepLink', () => { + const stateWithDeepLink: any = { + ...mockState, + context: {...mockState.context, isQrLoginViaDeepLink: true}, + }; + + it('should return deep link status from context', () => { + const result = selectIsQrLoginViaDeepLink(stateWithDeepLink); + expect(result).toBe(true); + }); + + it('should return false when not via deep link', () => { + const state: any = { + ...mockState, + context: {...mockState.context, isQrLoginViaDeepLink: false}, + }; + const result = selectIsQrLoginViaDeepLink(state); + expect(result).toBe(false); + }); + }); + + describe('selectIsFaceVerificationConsent', () => { + it('should return true when in faceVerificationConsent state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'faceVerificationConsent'), + }; + const result = selectIsFaceVerificationConsent(state); + expect(result).toBe(true); + }); + + it('should call matches with faceVerificationConsent', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsFaceVerificationConsent(state); + expect(state.matches).toHaveBeenCalledWith('faceVerificationConsent'); + }); + }); + + describe('Selectors with empty/null values', () => { + const emptyState: any = { + context: { + myVcs: [], + domainName: '', + selectedVc: null, + linkTransactionResponse: null, + essentialClaims: [], + voluntaryClaims: [], + logoUrl: '', + clientName: '', + errorMessage: '', + isSharing: false, + isQrLoginViaDeepLink: false, + }, + matches: jest.fn(() => false), + }; + + it('should handle empty VCs array', () => { + const result = selectMyVcs(emptyState); + expect(result).toEqual([]); + }); + + it('should handle empty domain name', () => { + const result = selectDomainName(emptyState); + expect(result).toBe(''); + }); + + it('should handle empty strings', () => { + expect(selectLogoUrl(emptyState)).toBe(''); + expect(selectClientName(emptyState)).toBe(''); + expect(selectErrorMessage(emptyState)).toBe(''); + }); + + it('should handle null responses', () => { + expect(selectLinkTransactionResponse(emptyState)).toBeNull(); + }); + + it('should handle empty arrays', () => { + expect(selectEssentialClaims(emptyState)).toEqual([]); + expect(selectVoluntaryClaims(emptyState)).toEqual([]); + }); + + it('should handle false boolean values', () => { + expect(selectIsSharing(emptyState)).toBe(false); + expect(selectIsQrLoginViaDeepLink(emptyState)).toBe(false); + }); + + it('should return false for all state checks', () => { + expect(selectIsWaitingForData(emptyState)).toBe(false); + expect(selectIsLinkTransaction(emptyState)).toBe(false); + expect(selectIsloadMyVcs(emptyState)).toBe(false); + expect(selectIsShowingVcList(emptyState)).toBe(false); + expect(selectIsisVerifyingIdentity(emptyState)).toBe(false); + expect(selectIsInvalidIdentity(emptyState)).toBe(false); + expect(selectIsShowError(emptyState)).toBe(false); + expect(selectIsRequestConsent(emptyState)).toBe(false); + expect(selectIsSendingAuthenticate(emptyState)).toBe(false); + expect(selectIsSendingConsent(emptyState)).toBe(false); + expect(selectIsVerifyingSuccesful(emptyState)).toBe(false); + expect(selectIsFaceVerificationConsent(emptyState)).toBe(false); + }); + }); +}); diff --git a/machines/VerifiableCredential/VCItemMachine/VCItemModel.test.ts b/machines/VerifiableCredential/VCItemMachine/VCItemModel.test.ts new file mode 100644 index 00000000..037e18db --- /dev/null +++ b/machines/VerifiableCredential/VCItemMachine/VCItemModel.test.ts @@ -0,0 +1,290 @@ +import {VCItemModel} from './VCItemModel'; + +describe('VCItemModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(VCItemModel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(VCItemModel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(VCItemModel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = VCItemModel.initialContext; + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + expect(typeof initialContext.serviceRefs).toBe('object'); + }); + + it('should have vcMetadata as empty object', () => { + expect(initialContext.vcMetadata).toEqual({}); + expect(typeof initialContext.vcMetadata).toBe('object'); + }); + + it('should have generatedOn as Date instance', () => { + expect(initialContext.generatedOn).toBeInstanceOf(Date); + }); + + it('should have credential as null', () => { + expect(initialContext.credential).toBeNull(); + }); + + it('should have verifiableCredential as null', () => { + expect(initialContext.verifiableCredential).toBeNull(); + }); + + it('should have hashedId as empty string', () => { + expect(initialContext.hashedId).toBe(''); + expect(typeof initialContext.hashedId).toBe('string'); + }); + + it('should have publicKey as empty string', () => { + expect(initialContext.publicKey).toBe(''); + expect(typeof initialContext.publicKey).toBe('string'); + }); + + it('should have privateKey as empty string', () => { + expect(initialContext.privateKey).toBe(''); + expect(typeof initialContext.privateKey).toBe('string'); + }); + + it('should have OTP as empty string', () => { + expect(initialContext.OTP).toBe(''); + expect(typeof initialContext.OTP).toBe('string'); + }); + + it('should have error as empty string', () => { + expect(initialContext.error).toBe(''); + expect(typeof initialContext.error).toBe('string'); + }); + + it('should have bindingTransactionId as empty string', () => { + expect(initialContext.bindingTransactionId).toBe(''); + expect(typeof initialContext.bindingTransactionId).toBe('string'); + }); + + it('should have requestId as empty string', () => { + expect(initialContext.requestId).toBe(''); + expect(typeof initialContext.requestId).toBe('string'); + }); + + it('should have downloadCounter as 0', () => { + expect(initialContext.downloadCounter).toBe(0); + expect(typeof initialContext.downloadCounter).toBe('number'); + }); + + it('should have maxDownloadCount as null', () => { + expect(initialContext.maxDownloadCount).toBeNull(); + }); + + it('should have downloadInterval as null', () => { + expect(initialContext.downloadInterval).toBeNull(); + }); + + it('should have walletBindingResponse as null', () => { + expect(initialContext.walletBindingResponse).toBeNull(); + }); + + it('should have isMachineInKebabPopupState as false', () => { + expect(initialContext.isMachineInKebabPopupState).toBe(false); + expect(typeof initialContext.isMachineInKebabPopupState).toBe('boolean'); + }); + + it('should have communicationDetails as null', () => { + expect(initialContext.communicationDetails).toBeNull(); + }); + + it('should have verificationStatus as null', () => { + expect(initialContext.verificationStatus).toBeNull(); + }); + + it('should have showVerificationStatusBanner as false', () => { + expect(initialContext.showVerificationStatusBanner).toBe(false); + expect(typeof initialContext.showVerificationStatusBanner).toBe( + 'boolean', + ); + }); + + it('should have wellknownResponse as empty object', () => { + expect(initialContext.wellknownResponse).toEqual({}); + expect(typeof initialContext.wellknownResponse).toBe('object'); + }); + + it('should have all 24 required properties', () => { + const properties = Object.keys(initialContext); + expect(properties).toHaveLength(24); + }); + }); + + describe('String properties', () => { + const context = VCItemModel.initialContext; + + it('all empty string properties should be empty', () => { + const emptyStrings = [ + context.hashedId, + context.publicKey, + context.privateKey, + context.OTP, + context.error, + context.bindingTransactionId, + context.requestId, + ]; + + emptyStrings.forEach(str => { + expect(str).toBe(''); + expect(typeof str).toBe('string'); + }); + }); + }); + + describe('Object properties', () => { + const context = VCItemModel.initialContext; + + it('empty object properties should be empty objects', () => { + const emptyObjects = [ + context.serviceRefs, + context.vcMetadata, + context.wellknownResponse, + ]; + + emptyObjects.forEach(obj => { + expect(typeof obj).toBe('object'); + expect(Object.keys(obj)).toHaveLength(0); + }); + }); + }); + + describe('Boolean properties', () => { + const context = VCItemModel.initialContext; + + it('isMachineInKebabPopupState should be false', () => { + expect(context.isMachineInKebabPopupState).toBe(false); + expect(typeof context.isMachineInKebabPopupState).toBe('boolean'); + }); + + it('showVerificationStatusBanner should be false', () => { + expect(context.showVerificationStatusBanner).toBe(false); + expect(typeof context.showVerificationStatusBanner).toBe('boolean'); + }); + + it('all boolean properties should be false initially', () => { + const booleans = [ + context.isMachineInKebabPopupState, + context.showVerificationStatusBanner, + ]; + + booleans.forEach(bool => { + expect(bool).toBe(false); + expect(typeof bool).toBe('boolean'); + }); + }); + }); + + describe('Null properties', () => { + const context = VCItemModel.initialContext; + + it('credential should be null', () => { + expect(context.credential).toBeNull(); + }); + + it('verifiableCredential should be null', () => { + expect(context.verifiableCredential).toBeNull(); + }); + + it('maxDownloadCount should be null', () => { + expect(context.maxDownloadCount).toBeNull(); + }); + + it('downloadInterval should be null', () => { + expect(context.downloadInterval).toBeNull(); + }); + + it('walletBindingResponse should be null', () => { + expect(context.walletBindingResponse).toBeNull(); + }); + + it('communicationDetails should be null', () => { + expect(context.communicationDetails).toBeNull(); + }); + + it('verificationStatus should be null', () => { + expect(context.verificationStatus).toBeNull(); + }); + + it('all null properties should be null initially', () => { + const nullProps = [ + context.credential, + context.verifiableCredential, + context.maxDownloadCount, + context.downloadInterval, + context.walletBindingResponse, + context.communicationDetails, + context.verificationStatus, + ]; + + nullProps.forEach(prop => { + expect(prop).toBeNull(); + }); + }); + }); + + describe('Number properties', () => { + const context = VCItemModel.initialContext; + + it('downloadCounter should be 0', () => { + expect(context.downloadCounter).toBe(0); + expect(typeof context.downloadCounter).toBe('number'); + }); + }); + + describe('Date properties', () => { + const context = VCItemModel.initialContext; + + it('generatedOn should be a Date instance', () => { + expect(context.generatedOn).toBeInstanceOf(Date); + }); + + it('generatedOn should be a valid date', () => { + expect(context.generatedOn.getTime()).not.toBeNaN(); + }); + }); + + describe('Model events', () => { + it('should have events object', () => { + expect(VCItemModel.events).toBeDefined(); + expect(typeof VCItemModel.events).toBe('object'); + }); + + it('should have event creators', () => { + const eventKeys = Object.keys(VCItemModel.events); + expect(eventKeys.length).toBeGreaterThan(0); + }); + }); + + describe('Property types validation', () => { + const context = VCItemModel.initialContext; + + it('should have correct types for all properties', () => { + expect(typeof context.hashedId).toBe('string'); + expect(typeof context.publicKey).toBe('string'); + expect(typeof context.privateKey).toBe('string'); + expect(typeof context.OTP).toBe('string'); + expect(typeof context.error).toBe('string'); + expect(typeof context.bindingTransactionId).toBe('string'); + expect(typeof context.requestId).toBe('string'); + expect(typeof context.downloadCounter).toBe('number'); + expect(typeof context.isMachineInKebabPopupState).toBe('boolean'); + expect(typeof context.showVerificationStatusBanner).toBe('boolean'); + expect(typeof context.serviceRefs).toBe('object'); + expect(typeof context.vcMetadata).toBe('object'); + expect(context.generatedOn).toBeInstanceOf(Date); + }); + }); +}); diff --git a/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.test.ts b/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.test.ts new file mode 100644 index 00000000..ff9609d8 --- /dev/null +++ b/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.test.ts @@ -0,0 +1,451 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectVerificationStatus, + selectIsVerificationInProgress, + selectIsVerificationCompleted, + selectShowVerificationStatusBanner, + selectVerifiableCredential, + getVerifiableCredential, + selectCredential, + selectVerifiableCredentialData, + selectKebabPopUp, + selectContext, + selectGeneratedOn, + selectWalletBindingSuccess, + selectWalletBindingResponse, + selectIsCommunicationDetails, + selectWalletBindingError, + selectBindingAuthFailedError, + selectAcceptingBindingOtp, + selectWalletBindingInProgress, + selectBindingWarning, + selectRemoveWalletWarning, + selectIsPinned, + selectOtpError, + selectShowActivities, + selectShowWalletBindingError, + selectVc, +} from './VCItemSelectors'; + +describe('VCItemSelectors', () => { + describe('selectVerificationStatus', () => { + it('should return verification status from context', () => { + const mockState: any = { + context: { + verificationStatus: 'verified', + }, + }; + expect(selectVerificationStatus(mockState)).toBe('verified'); + }); + }); + + describe('selectIsVerificationInProgress', () => { + it('should return true when in verifyingCredential state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'verifyState.verifyingCredential', + ), + }; + expect(selectIsVerificationInProgress(mockState)).toBe(true); + }); + + it('should return false when not in verifyingCredential state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsVerificationInProgress(mockState)).toBe(false); + }); + }); + + describe('selectIsVerificationCompleted', () => { + it('should return true when in verificationCompleted state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'verifyState.verificationCompleted', + ), + }; + expect(selectIsVerificationCompleted(mockState)).toBe(true); + }); + + it('should return false when not in verificationCompleted state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsVerificationCompleted(mockState)).toBe(false); + }); + }); + + describe('selectShowVerificationStatusBanner', () => { + it('should return showVerificationStatusBanner from context', () => { + const mockState: any = { + context: { + showVerificationStatusBanner: true, + }, + }; + expect(selectShowVerificationStatusBanner(mockState)).toBe(true); + }); + }); + + describe('selectVerifiableCredential', () => { + it('should return verifiableCredential from context', () => { + const mockVC = {credential: {id: 'test-123'}}; + const mockState: any = { + context: { + verifiableCredential: mockVC, + }, + }; + expect(selectVerifiableCredential(mockState)).toBe(mockVC); + }); + }); + + describe('getVerifiableCredential', () => { + it('should return credential property if it exists', () => { + const mockCredential = {id: 'cred-123'}; + const mockVC: any = { + credential: mockCredential, + }; + expect(getVerifiableCredential(mockVC)).toBe(mockCredential); + }); + + it('should return the whole object if credential property does not exist', () => { + const mockCredential: any = {id: 'cred-456'}; + expect(getVerifiableCredential(mockCredential)).toBe(mockCredential); + }); + }); + + describe('selectCredential', () => { + it('should return verifiableCredential from context', () => { + const mockVC = {credential: {id: 'test-789'}}; + const mockState: any = { + context: { + verifiableCredential: mockVC, + }, + }; + expect(selectCredential(mockState)).toBe(mockVC); + }); + }); + + describe('selectVerifiableCredentialData', () => { + it('should return formatted verifiable credential data', () => { + const mockState: any = { + context: { + vcMetadata: { + id: 'vc-001', + issuer: 'Test Issuer', + format: 'ldp_vc', + }, + verifiableCredential: { + credential: { + credentialSubject: { + name: 'John Doe', + }, + }, + issuerLogo: 'https://example.com/logo.png', + wellKnown: 'https://example.com/.well-known', + credentialConfigurationId: 'config-123', + }, + format: 'ldp_vc', + credential: null, + }, + }; + + const result = selectVerifiableCredentialData(mockState); + expect(result.issuer).toBe('Test Issuer'); + expect(result.issuerLogo).toBe('https://example.com/logo.png'); + expect(result.wellKnown).toBe('https://example.com/.well-known'); + expect(result.credentialConfigurationId).toBe('config-123'); + }); + }); + + describe('selectKebabPopUp', () => { + it('should return isMachineInKebabPopupState from context', () => { + const mockState: any = { + context: { + isMachineInKebabPopupState: true, + }, + }; + expect(selectKebabPopUp(mockState)).toBe(true); + }); + }); + + describe('selectContext', () => { + it('should return entire context', () => { + const mockContext = { + verificationStatus: 'verified', + generatedOn: '2023-01-01', + }; + const mockState: any = { + context: mockContext, + }; + expect(selectContext(mockState)).toBe(mockContext); + }); + }); + + describe('selectGeneratedOn', () => { + it('should return generatedOn from context', () => { + const mockState: any = { + context: { + generatedOn: '2023-12-25', + }, + }; + expect(selectGeneratedOn(mockState)).toBe('2023-12-25'); + }); + }); + + describe('selectWalletBindingSuccess', () => { + it('should return walletBindingResponse from context', () => { + const mockResponse = {walletBindingId: 'binding-123'}; + const mockState: any = { + context: { + walletBindingResponse: mockResponse, + }, + }; + expect(selectWalletBindingSuccess(mockState)).toBe(mockResponse); + }); + }); + + describe('selectWalletBindingResponse', () => { + it('should return walletBindingResponse from context', () => { + const mockResponse = {walletBindingId: 'binding-456'}; + const mockState: any = { + context: { + walletBindingResponse: mockResponse, + }, + }; + expect(selectWalletBindingResponse(mockState)).toBe(mockResponse); + }); + }); + + describe('selectIsCommunicationDetails', () => { + it('should return communicationDetails from context', () => { + const mockDetails = {email: 'test@example.com', phone: '1234567890'}; + const mockState: any = { + context: { + communicationDetails: mockDetails, + }, + }; + expect(selectIsCommunicationDetails(mockState)).toBe(mockDetails); + }); + }); + + describe('selectWalletBindingError', () => { + it('should return error from context', () => { + const mockError = new Error('Binding failed'); + const mockState: any = { + context: { + error: mockError, + }, + }; + expect(selectWalletBindingError(mockState)).toBe(mockError); + }); + }); + + describe('selectBindingAuthFailedError', () => { + it('should return error from context', () => { + const mockError = new Error('Auth failed'); + const mockState: any = { + context: { + error: mockError, + }, + }; + expect(selectBindingAuthFailedError(mockState)).toBe(mockError); + }); + }); + + describe('selectAcceptingBindingOtp', () => { + it('should return true when in acceptingBindingOTP state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.walletBinding.acceptingBindingOTP', + ), + }; + expect(selectAcceptingBindingOtp(mockState)).toBe(true); + }); + + it('should return false when not in acceptingBindingOTP state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectAcceptingBindingOtp(mockState)).toBe(false); + }); + }); + + describe('selectWalletBindingInProgress', () => { + it('should return true when in requestingBindingOTP state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.walletBinding.requestingBindingOTP', + ), + }; + expect(selectWalletBindingInProgress(mockState)).toBe(true); + }); + + it('should return true when in addingWalletBindingId state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.walletBinding.addingWalletBindingId', + ), + }; + expect(selectWalletBindingInProgress(mockState)).toBe(true); + }); + + it('should return true when in addKeyPair state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.walletBinding.addKeyPair', + ), + }; + expect(selectWalletBindingInProgress(mockState)).toBe(true); + }); + + it('should return true when in updatingPrivateKey state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.walletBinding.updatingPrivateKey', + ), + }; + expect(selectWalletBindingInProgress(mockState)).toBe(true); + }); + + it('should return false when not in any wallet binding progress state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectWalletBindingInProgress(mockState)).toBe(false); + }); + }); + + describe('selectBindingWarning', () => { + it('should return true when in showBindingWarning state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.walletBinding.showBindingWarning', + ), + }; + expect(selectBindingWarning(mockState)).toBe(true); + }); + + it('should return false when not in showBindingWarning state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectBindingWarning(mockState)).toBe(false); + }); + }); + + describe('selectRemoveWalletWarning', () => { + it('should return true when in removeWallet state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.kebabPopUp.removeWallet', + ), + }; + expect(selectRemoveWalletWarning(mockState)).toBe(true); + }); + + it('should return false when not in removeWallet state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectRemoveWalletWarning(mockState)).toBe(false); + }); + }); + + describe('selectIsPinned', () => { + it('should return isPinned from vcMetadata', () => { + const mockState: any = { + context: { + vcMetadata: { + isPinned: true, + }, + }, + }; + expect(selectIsPinned(mockState)).toBe(true); + }); + + it('should return false when isPinned is false', () => { + const mockState: any = { + context: { + vcMetadata: { + isPinned: false, + }, + }, + }; + expect(selectIsPinned(mockState)).toBe(false); + }); + }); + + describe('selectOtpError', () => { + it('should return error from context', () => { + const mockError = new Error('OTP invalid'); + const mockState: any = { + context: { + error: mockError, + }, + }; + expect(selectOtpError(mockState)).toBe(mockError); + }); + }); + + describe('selectShowActivities', () => { + it('should return true when in showActivities state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'vcUtilitiesState.kebabPopUp.showActivities', + ), + }; + expect(selectShowActivities(mockState)).toBe(true); + }); + + it('should return false when not in showActivities state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectShowActivities(mockState)).toBe(false); + }); + }); + + describe('selectShowWalletBindingError', () => { + it('should return true when in showingWalletBindingError state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === + 'vcUtilitiesState.walletBinding.showingWalletBindingError', + ), + }; + expect(selectShowWalletBindingError(mockState)).toBe(true); + }); + + it('should return false when not in showingWalletBindingError state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectShowWalletBindingError(mockState)).toBe(false); + }); + }); + + describe('selectVc', () => { + it('should return context without serviceRefs', () => { + const mockState: any = { + context: { + verificationStatus: 'verified', + generatedOn: '2023-01-01', + serviceRefs: {ref1: 'service1', ref2: 'service2'}, + }, + }; + const result: any = selectVc(mockState); + expect(result.verificationStatus).toBe('verified'); + expect(result.generatedOn).toBe('2023-01-01'); + expect(result.serviceRefs).toBeUndefined(); + }); + }); +}); diff --git a/machines/VerifiableCredential/VCMetaMachine/VCMetaEvents.test.ts b/machines/VerifiableCredential/VCMetaMachine/VCMetaEvents.test.ts new file mode 100644 index 00000000..db0ad458 --- /dev/null +++ b/machines/VerifiableCredential/VCMetaMachine/VCMetaEvents.test.ts @@ -0,0 +1,288 @@ +import {VcMetaEvents} from './VCMetaEvents'; +import {VCMetadata} from '../../../shared/VCMetadata'; +import {VC} from './vc'; + +describe('VcMetaEvents', () => { + describe('VIEW_VC', () => { + it('should create event with vc', () => { + const vc = {id: 'vc-123'} as unknown as VC; + const result = VcMetaEvents.VIEW_VC(vc); + expect(result).toEqual({vc}); + }); + }); + + describe('GET_VC_ITEM', () => { + it('should create event with vcMetadata', () => { + const vcMetadata = new VCMetadata(); + const result = VcMetaEvents.GET_VC_ITEM(vcMetadata); + expect(result).toEqual({vcMetadata}); + }); + }); + + describe('STORE_RESPONSE', () => { + it('should create event with response', () => { + const response = {data: 'test'}; + const result = VcMetaEvents.STORE_RESPONSE(response); + expect(result).toEqual({response: {data: 'test'}}); + }); + + it('should handle undefined response', () => { + const result = VcMetaEvents.STORE_RESPONSE(undefined); + expect(result).toEqual({response: undefined}); + }); + }); + + describe('STORE_ERROR', () => { + it('should create event with error', () => { + const error = new Error('Test error'); + const result = VcMetaEvents.STORE_ERROR(error); + expect(result).toEqual({error}); + }); + }); + + describe('VC_ADDED', () => { + it('should create event with vcMetadata', () => { + const vcMetadata = new VCMetadata(); + const result = VcMetaEvents.VC_ADDED(vcMetadata); + expect(result).toEqual({vcMetadata}); + }); + }); + + describe('REMOVE_VC_FROM_CONTEXT', () => { + it('should create event with vcMetadata', () => { + const vcMetadata = new VCMetadata(); + const result = VcMetaEvents.REMOVE_VC_FROM_CONTEXT(vcMetadata); + expect(result).toEqual({vcMetadata}); + }); + }); + + describe('VC_METADATA_UPDATED', () => { + it('should create event with vcMetadata', () => { + const vcMetadata = new VCMetadata(); + const result = VcMetaEvents.VC_METADATA_UPDATED(vcMetadata); + expect(result).toEqual({vcMetadata}); + }); + }); + + describe('VC_DOWNLOADED', () => { + it('should create event with vc and vcMetadata', () => { + const vc = {id: 'vc-123'} as unknown as VC; + const vcMetadata = new VCMetadata(); + const result = VcMetaEvents.VC_DOWNLOADED(vc, vcMetadata); + expect(result.vc).toBe(vc); + expect(result.vcMetadata).toBe(vcMetadata); + }); + + it('should handle undefined vcMetadata', () => { + const vc = {id: 'vc-123'} as unknown as VC; + const result = VcMetaEvents.VC_DOWNLOADED(vc); + expect(result.vc).toBe(vc); + expect(result.vcMetadata).toBeUndefined(); + }); + }); + + describe('REFRESH_MY_VCS', () => { + it('should create empty event', () => { + const result = VcMetaEvents.REFRESH_MY_VCS(); + expect(result).toEqual({}); + }); + }); + + describe('REFRESH_MY_VCS_TWO', () => { + it('should create event with vc', () => { + const vc = {id: 'vc-123'} as unknown as VC; + const result = VcMetaEvents.REFRESH_MY_VCS_TWO(vc); + expect(result).toEqual({vc}); + }); + }); + + describe('REFRESH_RECEIVED_VCS', () => { + it('should create empty event', () => { + const result = VcMetaEvents.REFRESH_RECEIVED_VCS(); + expect(result).toEqual({}); + }); + }); + + describe('WALLET_BINDING_SUCCESS', () => { + it('should create event with vcKey and vc', () => { + const vcKey = 'key-123'; + const vc = {id: 'vc-123'} as unknown as VC; + const result = VcMetaEvents.WALLET_BINDING_SUCCESS(vcKey, vc); + expect(result).toEqual({vcKey: 'key-123', vc}); + }); + }); + + describe('RESET_WALLET_BINDING_SUCCESS', () => { + it('should create empty event', () => { + const result = VcMetaEvents.RESET_WALLET_BINDING_SUCCESS(); + expect(result).toEqual({}); + }); + }); + + describe('ADD_VC_TO_IN_PROGRESS_DOWNLOADS', () => { + it('should create event with requestId', () => { + const result = VcMetaEvents.ADD_VC_TO_IN_PROGRESS_DOWNLOADS('req-123'); + expect(result).toEqual({requestId: 'req-123'}); + }); + }); + + describe('REMOVE_VC_FROM_IN_PROGRESS_DOWNLOADS', () => { + it('should create event with vcMetadata', () => { + const vcMetadata = new VCMetadata(); + const result = + VcMetaEvents.REMOVE_VC_FROM_IN_PROGRESS_DOWNLOADS(vcMetadata); + expect(result).toEqual({vcMetadata}); + }); + }); + + describe('RESET_IN_PROGRESS_VCS_DOWNLOADED', () => { + it('should create empty event', () => { + const result = VcMetaEvents.RESET_IN_PROGRESS_VCS_DOWNLOADED(); + expect(result).toEqual({}); + }); + }); + + describe('REMOVE_TAMPERED_VCS', () => { + it('should create empty event', () => { + const result = VcMetaEvents.REMOVE_TAMPERED_VCS(); + expect(result).toEqual({}); + }); + }); + + describe('DOWNLOAD_LIMIT_EXPIRED', () => { + it('should create event with vcMetadata', () => { + const vcMetadata = new VCMetadata(); + const result = VcMetaEvents.DOWNLOAD_LIMIT_EXPIRED(vcMetadata); + expect(result).toEqual({vcMetadata}); + }); + }); + + describe('DELETE_VC', () => { + it('should create empty event', () => { + const result = VcMetaEvents.DELETE_VC(); + expect(result).toEqual({}); + }); + }); + + describe('VERIFY_VC_FAILED', () => { + it('should create event with errorMessage and vcMetadata', () => { + const vcMetadata = new VCMetadata(); + const result = VcMetaEvents.VERIFY_VC_FAILED( + 'Verification failed', + vcMetadata, + ); + expect(result.errorMessage).toBe('Verification failed'); + expect(result.vcMetadata).toBe(vcMetadata); + }); + + it('should handle undefined vcMetadata', () => { + const result = VcMetaEvents.VERIFY_VC_FAILED('Error occurred'); + expect(result.errorMessage).toBe('Error occurred'); + expect(result.vcMetadata).toBeUndefined(); + }); + }); + + describe('RESET_VERIFY_ERROR', () => { + it('should create empty event', () => { + const result = VcMetaEvents.RESET_VERIFY_ERROR(); + expect(result).toEqual({}); + }); + }); + + describe('REFRESH_VCS_METADATA', () => { + it('should create empty event', () => { + const result = VcMetaEvents.REFRESH_VCS_METADATA(); + expect(result).toEqual({}); + }); + }); + + describe('SHOW_TAMPERED_POPUP', () => { + it('should create empty event', () => { + const result = VcMetaEvents.SHOW_TAMPERED_POPUP(); + expect(result).toEqual({}); + }); + }); + + describe('SET_VERIFICATION_STATUS', () => { + it('should create event with verificationStatus', () => { + const status = {verified: true}; + const result = VcMetaEvents.SET_VERIFICATION_STATUS(status); + expect(result).toEqual({verificationStatus: status}); + }); + }); + + describe('RESET_VERIFICATION_STATUS', () => { + it('should create event with verificationStatus', () => { + const status = {message: 'Reset'} as any; + const result = VcMetaEvents.RESET_VERIFICATION_STATUS(status); + expect(result).toEqual({verificationStatus: status}); + }); + + it('should handle null verificationStatus', () => { + const result = VcMetaEvents.RESET_VERIFICATION_STATUS(null); + expect(result).toEqual({verificationStatus: null}); + }); + }); + + describe('VC_DOWNLOADING_FAILED', () => { + it('should create empty event', () => { + const result = VcMetaEvents.VC_DOWNLOADING_FAILED(); + expect(result).toEqual({}); + }); + }); + + describe('RESET_DOWNLOADING_FAILED', () => { + it('should create empty event', () => { + const result = VcMetaEvents.RESET_DOWNLOADING_FAILED(); + expect(result).toEqual({}); + }); + }); + + describe('RESET_DOWNLOADING_SUCCESS', () => { + it('should create empty event', () => { + const result = VcMetaEvents.RESET_DOWNLOADING_SUCCESS(); + expect(result).toEqual({}); + }); + }); + + describe('VcMetaEvents object structure', () => { + it('should have all expected event creators', () => { + expect(VcMetaEvents.VIEW_VC).toBeDefined(); + expect(VcMetaEvents.GET_VC_ITEM).toBeDefined(); + expect(VcMetaEvents.STORE_RESPONSE).toBeDefined(); + expect(VcMetaEvents.STORE_ERROR).toBeDefined(); + expect(VcMetaEvents.VC_ADDED).toBeDefined(); + expect(VcMetaEvents.REMOVE_VC_FROM_CONTEXT).toBeDefined(); + expect(VcMetaEvents.VC_METADATA_UPDATED).toBeDefined(); + expect(VcMetaEvents.VC_DOWNLOADED).toBeDefined(); + expect(VcMetaEvents.REFRESH_MY_VCS).toBeDefined(); + expect(VcMetaEvents.REFRESH_MY_VCS_TWO).toBeDefined(); + expect(VcMetaEvents.REFRESH_RECEIVED_VCS).toBeDefined(); + expect(VcMetaEvents.WALLET_BINDING_SUCCESS).toBeDefined(); + expect(VcMetaEvents.RESET_WALLET_BINDING_SUCCESS).toBeDefined(); + expect(VcMetaEvents.ADD_VC_TO_IN_PROGRESS_DOWNLOADS).toBeDefined(); + expect(VcMetaEvents.REMOVE_VC_FROM_IN_PROGRESS_DOWNLOADS).toBeDefined(); + expect(VcMetaEvents.RESET_IN_PROGRESS_VCS_DOWNLOADED).toBeDefined(); + expect(VcMetaEvents.REMOVE_TAMPERED_VCS).toBeDefined(); + expect(VcMetaEvents.DOWNLOAD_LIMIT_EXPIRED).toBeDefined(); + expect(VcMetaEvents.DELETE_VC).toBeDefined(); + expect(VcMetaEvents.VERIFY_VC_FAILED).toBeDefined(); + expect(VcMetaEvents.RESET_VERIFY_ERROR).toBeDefined(); + expect(VcMetaEvents.REFRESH_VCS_METADATA).toBeDefined(); + expect(VcMetaEvents.SHOW_TAMPERED_POPUP).toBeDefined(); + expect(VcMetaEvents.SET_VERIFICATION_STATUS).toBeDefined(); + expect(VcMetaEvents.RESET_VERIFICATION_STATUS).toBeDefined(); + expect(VcMetaEvents.VC_DOWNLOADING_FAILED).toBeDefined(); + expect(VcMetaEvents.RESET_DOWNLOADING_FAILED).toBeDefined(); + expect(VcMetaEvents.RESET_DOWNLOADING_SUCCESS).toBeDefined(); + }); + + it('should have all event creators be functions', () => { + expect(typeof VcMetaEvents.VIEW_VC).toBe('function'); + expect(typeof VcMetaEvents.GET_VC_ITEM).toBe('function'); + expect(typeof VcMetaEvents.STORE_RESPONSE).toBe('function'); + expect(typeof VcMetaEvents.STORE_ERROR).toBe('function'); + expect(typeof VcMetaEvents.VC_ADDED).toBe('function'); + }); + }); +}); diff --git a/machines/VerifiableCredential/VCMetaMachine/VCMetaModel.test.ts b/machines/VerifiableCredential/VCMetaMachine/VCMetaModel.test.ts new file mode 100644 index 00000000..1f1e941e --- /dev/null +++ b/machines/VerifiableCredential/VCMetaMachine/VCMetaModel.test.ts @@ -0,0 +1,219 @@ +import {VCMetamodel} from './VCMetaModel'; + +describe('VCMetaModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(VCMetamodel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(VCMetamodel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(VCMetamodel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = VCMetamodel.initialContext; + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + expect(typeof initialContext.serviceRefs).toBe('object'); + }); + + it('should have myVcsMetadata as empty array', () => { + expect(initialContext.myVcsMetadata).toEqual([]); + expect(Array.isArray(initialContext.myVcsMetadata)).toBe(true); + expect(initialContext.myVcsMetadata).toHaveLength(0); + }); + + it('should have receivedVcsMetadata as empty array', () => { + expect(initialContext.receivedVcsMetadata).toEqual([]); + expect(Array.isArray(initialContext.receivedVcsMetadata)).toBe(true); + expect(initialContext.receivedVcsMetadata).toHaveLength(0); + }); + + it('should have myVcs as empty object', () => { + expect(initialContext.myVcs).toEqual({}); + expect(typeof initialContext.myVcs).toBe('object'); + expect(Object.keys(initialContext.myVcs)).toHaveLength(0); + }); + + it('should have receivedVcs as empty object', () => { + expect(initialContext.receivedVcs).toEqual({}); + expect(typeof initialContext.receivedVcs).toBe('object'); + expect(Object.keys(initialContext.receivedVcs)).toHaveLength(0); + }); + + it('should have inProgressVcDownloads as empty Set', () => { + expect(initialContext.inProgressVcDownloads).toBeInstanceOf(Set); + expect(initialContext.inProgressVcDownloads.size).toBe(0); + }); + + it('should have areAllVcsDownloaded as false', () => { + expect(initialContext.areAllVcsDownloaded).toBe(false); + expect(typeof initialContext.areAllVcsDownloaded).toBe('boolean'); + }); + + it('should have walletBindingSuccess as false', () => { + expect(initialContext.walletBindingSuccess).toBe(false); + expect(typeof initialContext.walletBindingSuccess).toBe('boolean'); + }); + + it('should have tamperedVcs as empty array', () => { + expect(initialContext.tamperedVcs).toEqual([]); + expect(Array.isArray(initialContext.tamperedVcs)).toBe(true); + expect(initialContext.tamperedVcs).toHaveLength(0); + }); + + it('should have downloadingFailedVcs as empty array', () => { + expect(initialContext.downloadingFailedVcs).toEqual([]); + expect(Array.isArray(initialContext.downloadingFailedVcs)).toBe(true); + expect(initialContext.downloadingFailedVcs).toHaveLength(0); + }); + + it('should have verificationErrorMessage as empty string', () => { + expect(initialContext.verificationErrorMessage).toBe(''); + expect(typeof initialContext.verificationErrorMessage).toBe('string'); + }); + + it('should have verificationStatus as null', () => { + expect(initialContext.verificationStatus).toBeNull(); + }); + + it('should have DownloadingCredentialsFailed as false', () => { + expect(initialContext.DownloadingCredentialsFailed).toBe(false); + expect(typeof initialContext.DownloadingCredentialsFailed).toBe( + 'boolean', + ); + }); + + it('should have DownloadingCredentialsSuccess as false', () => { + expect(initialContext.DownloadingCredentialsSuccess).toBe(false); + expect(typeof initialContext.DownloadingCredentialsSuccess).toBe( + 'boolean', + ); + }); + + it('should have all required properties', () => { + expect(initialContext).toHaveProperty('serviceRefs'); + expect(initialContext).toHaveProperty('myVcsMetadata'); + expect(initialContext).toHaveProperty('receivedVcsMetadata'); + expect(initialContext).toHaveProperty('myVcs'); + expect(initialContext).toHaveProperty('receivedVcs'); + expect(initialContext).toHaveProperty('inProgressVcDownloads'); + expect(initialContext).toHaveProperty('areAllVcsDownloaded'); + expect(initialContext).toHaveProperty('walletBindingSuccess'); + expect(initialContext).toHaveProperty('tamperedVcs'); + expect(initialContext).toHaveProperty('downloadingFailedVcs'); + expect(initialContext).toHaveProperty('verificationErrorMessage'); + expect(initialContext).toHaveProperty('verificationStatus'); + expect(initialContext).toHaveProperty('DownloadingCredentialsFailed'); + expect(initialContext).toHaveProperty('DownloadingCredentialsSuccess'); + }); + + it('should have exactly 16 properties in initial context', () => { + const propertyCount = Object.keys(initialContext).length; + expect(propertyCount).toBe(16); + }); + }); + + describe('Model events', () => { + it('should have events object defined', () => { + expect(VCMetamodel.events).toBeDefined(); + expect(typeof VCMetamodel.events).toBe('object'); + }); + + it('should have non-empty events', () => { + const eventKeys = Object.keys(VCMetamodel.events); + expect(eventKeys.length).toBeGreaterThan(0); + }); + }); + + describe('Type validation', () => { + const context = VCMetamodel.initialContext; + + it('myVcsMetadata should accept VCMetadata array', () => { + expect(() => { + const metadata: typeof context.myVcsMetadata = []; + expect(Array.isArray(metadata)).toBe(true); + }).not.toThrow(); + }); + + it('myVcs should accept Record', () => { + expect(() => { + const vcs: typeof context.myVcs = {}; + expect(typeof vcs).toBe('object'); + }).not.toThrow(); + }); + + it('inProgressVcDownloads should be a Set', () => { + expect(context.inProgressVcDownloads).toBeInstanceOf(Set); + expect(context.inProgressVcDownloads.constructor.name).toBe('Set'); + }); + }); + + describe('Boolean flags', () => { + const context = VCMetamodel.initialContext; + + it('all boolean flags should be false initially', () => { + const booleanFlags = [ + context.areAllVcsDownloaded, + context.walletBindingSuccess, + context.DownloadingCredentialsFailed, + context.DownloadingCredentialsSuccess, + ]; + + booleanFlags.forEach(flag => { + expect(flag).toBe(false); + }); + }); + }); + + describe('Array properties', () => { + const context = VCMetamodel.initialContext; + + it('all array properties should be empty initially', () => { + const arrays = [ + context.myVcsMetadata, + context.receivedVcsMetadata, + context.tamperedVcs, + context.downloadingFailedVcs, + ]; + + arrays.forEach(arr => { + expect(Array.isArray(arr)).toBe(true); + expect(arr).toHaveLength(0); + }); + }); + }); + + describe('Object properties', () => { + const context = VCMetamodel.initialContext; + + it('all object properties should be empty initially', () => { + const objects = [context.serviceRefs, context.myVcs, context.receivedVcs]; + + objects.forEach(obj => { + expect(typeof obj).toBe('object'); + expect(Object.keys(obj)).toHaveLength(0); + }); + }); + }); + + describe('Null/undefined properties', () => { + const context = VCMetamodel.initialContext; + + it('verificationStatus should be null', () => { + expect(context.verificationStatus).toBeNull(); + expect(context.verificationStatus).not.toBeUndefined(); + }); + + it('verificationErrorMessage should be empty string, not null', () => { + expect(context.verificationErrorMessage).not.toBeNull(); + expect(context.verificationErrorMessage).toBe(''); + }); + }); +}); diff --git a/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.test.ts b/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.test.ts new file mode 100644 index 00000000..51bc2f41 --- /dev/null +++ b/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.test.ts @@ -0,0 +1,510 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectVerificationStatus, + selectMyVcsMetadata, + selectShareableVcsMetadata, + selectShareableVcs, + selectReceivedVcsMetadata, + selectIsRefreshingMyVcs, + selectIsRefreshingReceivedVcs, + selectAreAllVcsDownloaded, + selectBindedVcsMetadata, + selectInProgressVcDownloads, + selectWalletBindingSuccess, + selectIsTampered, + selectDownloadingFailedVcs, + selectMyVcs, + selectVerificationErrorMessage, + selectIsDownloadingFailed, + selectIsDownloadingSuccess, +} from './VCMetaSelectors'; +import {VCMetadata} from '../../../shared/VCMetadata'; + +describe('VCMetaSelectors', () => { + const mockVcMetadata1 = new VCMetadata({ + id: 'vc1', + idType: 'NationalID', + issuer: 'Test Issuer 1', + }); + + const mockVcMetadata2 = new VCMetadata({ + id: 'vc2', + idType: 'Passport', + issuer: 'Test Issuer 2', + }); + + const mockVcMetadata3 = new VCMetadata({ + id: 'vc3', + idType: 'DriversLicense', + issuer: 'Test Issuer 3', + }); + + const mockVc1 = { + verifiableCredential: {credential: {id: 'cred1'}}, + walletBindingResponse: null, + }; + + const mockVc2 = { + verifiableCredential: {credential: {id: 'cred2'}}, + walletBindingResponse: {walletBindingId: 'binding123'}, + }; + + const mockVc3 = { + verifiableCredential: null, + walletBindingResponse: null, + }; + + const mockState: any = { + context: { + verificationStatus: 'verified', + myVcsMetadata: [mockVcMetadata1, mockVcMetadata2, mockVcMetadata3], + receivedVcsMetadata: [mockVcMetadata1], + myVcs: { + [mockVcMetadata1.getVcKey()]: mockVc1, + [mockVcMetadata2.getVcKey()]: mockVc2, + [mockVcMetadata3.getVcKey()]: mockVc3, + }, + areAllVcsDownloaded: true, + inProgressVcDownloads: [], + walletBindingSuccess: false, + downloadingFailedVcs: [], + }, + matches: jest.fn((stateName: string) => stateName === 'ready.myVcs'), + }; + + describe('selectVerificationStatus', () => { + it('should return verification status from context', () => { + const result = selectVerificationStatus(mockState); + expect(result).toBe('verified'); + }); + + it('should handle different status values', () => { + const statuses = ['verified', 'pending', 'failed', 'invalid']; + statuses.forEach(status => { + const state: any = { + ...mockState, + context: {...mockState.context, verificationStatus: status}, + }; + expect(selectVerificationStatus(state)).toBe(status); + }); + }); + }); + + describe('selectMyVcsMetadata', () => { + it('should return all VCs metadata from context', () => { + const result = selectMyVcsMetadata(mockState); + expect(result).toHaveLength(3); + expect(result).toEqual([ + mockVcMetadata1, + mockVcMetadata2, + mockVcMetadata3, + ]); + }); + + it('should return empty array when no VCs', () => { + const state: any = { + ...mockState, + context: {...mockState.context, myVcsMetadata: []}, + }; + const result = selectMyVcsMetadata(state); + expect(result).toEqual([]); + }); + }); + + describe('selectShareableVcsMetadata', () => { + it('should filter VCs that have verifiableCredential', () => { + const result = selectShareableVcsMetadata(mockState); + expect(result).toHaveLength(2); + expect(result).toContain(mockVcMetadata1); + expect(result).toContain(mockVcMetadata2); + expect(result).not.toContain(mockVcMetadata3); + }); + + it('should return empty array when no shareable VCs', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + [mockVcMetadata1.getVcKey()]: {verifiableCredential: null}, + [mockVcMetadata2.getVcKey()]: {verifiableCredential: null}, + }, + }, + }; + const result = selectShareableVcsMetadata(state); + expect(result).toEqual([]); + }); + }); + + describe('selectShareableVcs', () => { + it('should filter VCs that have verifiableCredential', () => { + const result = selectShareableVcs(mockState); + expect(result).toHaveLength(2); + expect(result).toContainEqual(mockVc1); + expect(result).toContainEqual(mockVc2); + expect(result).not.toContainEqual(mockVc3); + }); + + it('should return empty array when no VCs have verifiableCredential', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + vc1: {verifiableCredential: null}, + vc2: {verifiableCredential: null}, + }, + }, + }; + const result = selectShareableVcs(state); + expect(result).toEqual([]); + }); + }); + + describe('selectReceivedVcsMetadata', () => { + it('should return received VCs metadata', () => { + const result = selectReceivedVcsMetadata(mockState); + expect(result).toHaveLength(1); + expect(result).toContain(mockVcMetadata1); + }); + + it('should return empty array when no received VCs', () => { + const state: any = { + ...mockState, + context: {...mockState.context, receivedVcsMetadata: []}, + }; + const result = selectReceivedVcsMetadata(state); + expect(result).toEqual([]); + }); + }); + + describe('selectIsRefreshingMyVcs', () => { + it('should return true when in ready.myVcs state', () => { + const result = selectIsRefreshingMyVcs(mockState); + expect(result).toBe(true); + }); + + it('should call matches with ready.myVcs', () => { + selectIsRefreshingMyVcs(mockState); + expect(mockState.matches).toHaveBeenCalledWith('ready.myVcs'); + }); + + it('should return false when not in ready.myVcs state', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + const result = selectIsRefreshingMyVcs(state); + expect(result).toBe(false); + }); + }); + + describe('selectIsRefreshingReceivedVcs', () => { + it('should return true when in ready.receivedVcs state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'ready.receivedVcs'), + }; + const result = selectIsRefreshingReceivedVcs(state); + expect(result).toBe(true); + }); + + it('should call matches with ready.receivedVcs', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsRefreshingReceivedVcs(state); + expect(state.matches).toHaveBeenCalledWith('ready.receivedVcs'); + }); + }); + + describe('selectAreAllVcsDownloaded', () => { + it('should return true when all VCs are downloaded', () => { + const result = selectAreAllVcsDownloaded(mockState); + expect(result).toBe(true); + }); + + it('should return false when not all VCs are downloaded', () => { + const state: any = { + ...mockState, + context: {...mockState.context, areAllVcsDownloaded: false}, + }; + const result = selectAreAllVcsDownloaded(state); + expect(result).toBe(false); + }); + }); + + describe('selectBindedVcsMetadata', () => { + it('should return VCs with wallet binding', () => { + const result = selectBindedVcsMetadata(mockState); + expect(result).toHaveLength(1); + expect(result).toContain(mockVcMetadata2); + expect(result).not.toContain(mockVcMetadata1); + expect(result).not.toContain(mockVcMetadata3); + }); + + it('should filter out VCs with null walletBindingResponse', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + [mockVcMetadata1.getVcKey()]: {walletBindingResponse: null}, + [mockVcMetadata2.getVcKey()]: {walletBindingResponse: {}}, + }, + }, + }; + const result = selectBindedVcsMetadata(state); + expect(result).toEqual([]); + }); + + it('should filter out VCs with empty walletBindingId', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + [mockVcMetadata1.getVcKey()]: { + walletBindingResponse: {walletBindingId: ''}, + }, + [mockVcMetadata2.getVcKey()]: { + walletBindingResponse: {walletBindingId: null}, + }, + }, + }, + }; + const result = selectBindedVcsMetadata(state); + expect(result).toEqual([]); + }); + + it('should return empty array when no binded VCs', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + [mockVcMetadata1.getVcKey()]: {walletBindingResponse: null}, + }, + }, + }; + const result = selectBindedVcsMetadata(state); + expect(result).toEqual([]); + }); + }); + + describe('selectInProgressVcDownloads', () => { + it('should return in-progress VC downloads', () => { + const downloads = ['vc1', 'vc2']; + const state: any = { + ...mockState, + context: {...mockState.context, inProgressVcDownloads: downloads}, + }; + const result = selectInProgressVcDownloads(state); + expect(result).toEqual(downloads); + }); + + it('should return empty array when no downloads in progress', () => { + const result = selectInProgressVcDownloads(mockState); + expect(result).toEqual([]); + }); + }); + + describe('selectWalletBindingSuccess', () => { + it('should return wallet binding success status', () => { + const result = selectWalletBindingSuccess(mockState); + expect(result).toBe(false); + }); + + it('should return true when wallet binding is successful', () => { + const state: any = { + ...mockState, + context: {...mockState.context, walletBindingSuccess: true}, + }; + const result = selectWalletBindingSuccess(state); + expect(result).toBe(true); + }); + }); + + describe('selectIsTampered', () => { + it('should return true when in ready.tamperedVCs state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'ready.tamperedVCs'), + }; + const result = selectIsTampered(state); + expect(result).toBe(true); + }); + + it('should call matches with ready.tamperedVCs', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsTampered(state); + expect(state.matches).toHaveBeenCalledWith('ready.tamperedVCs'); + }); + + it('should return false when not in tampered state', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + const result = selectIsTampered(state); + expect(result).toBe(false); + }); + }); + + describe('selectDownloadingFailedVcs', () => { + it('should return downloading failed VCs', () => { + const failedVcs = ['vc1', 'vc2']; + const state: any = { + ...mockState, + context: {...mockState.context, downloadingFailedVcs: failedVcs}, + }; + const result = selectDownloadingFailedVcs(state); + expect(result).toEqual(failedVcs); + }); + + it('should return empty array when no failed VCs', () => { + const result = selectDownloadingFailedVcs(mockState); + expect(result).toEqual([]); + }); + }); + + describe('selectMyVcs', () => { + it('should return all my VCs object', () => { + const result = selectMyVcs(mockState); + expect(result).toEqual(mockState.context.myVcs); + }); + + it('should return object with VC keys', () => { + const result = selectMyVcs(mockState); + expect(result).toHaveProperty(mockVcMetadata1.getVcKey()); + expect(result).toHaveProperty(mockVcMetadata2.getVcKey()); + }); + + it('should return empty object when no VCs', () => { + const state: any = { + ...mockState, + context: {...mockState.context, myVcs: {}}, + }; + const result = selectMyVcs(state); + expect(result).toEqual({}); + }); + }); + + describe('Edge cases and boundary conditions', () => { + it('should handle undefined values in walletBindingResponse', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + [mockVcMetadata1.getVcKey()]: { + walletBindingResponse: undefined, + }, + }, + }, + }; + const result = selectBindedVcsMetadata(state); + expect(result).toEqual([]); + }); + + it('should handle empty string in walletBindingId', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + [mockVcMetadata1.getVcKey()]: { + walletBindingResponse: {walletBindingId: ''}, + }, + }, + }, + }; + const result = selectBindedVcsMetadata(state); + expect(result).toEqual([]); + }); + + it('should handle null verifiableCredential in filtering', () => { + const state: any = { + ...mockState, + context: { + ...mockState.context, + myVcs: { + [mockVcMetadata1.getVcKey()]: {verifiableCredential: null}, + }, + }, + }; + const result = selectShareableVcsMetadata(state); + expect(result).toEqual([]); + }); + }); + + describe('selectVerificationErrorMessage', () => { + it('should return verification error message from context', () => { + const state: any = { + context: { + verificationErrorMessage: 'Invalid signature', + }, + }; + const result = selectVerificationErrorMessage(state); + expect(result).toBe('Invalid signature'); + }); + + it('should return empty string when no error', () => { + const state: any = { + context: { + verificationErrorMessage: '', + }, + }; + const result = selectVerificationErrorMessage(state); + expect(result).toBe(''); + }); + }); + + describe('selectIsDownloadingFailed', () => { + it('should return DownloadingCredentialsFailed status', () => { + const state: any = { + context: { + DownloadingCredentialsFailed: true, + }, + }; + const result = selectIsDownloadingFailed(state); + expect(result).toBe(true); + }); + + it('should return false when downloading not failed', () => { + const state: any = { + context: { + DownloadingCredentialsFailed: false, + }, + }; + const result = selectIsDownloadingFailed(state); + expect(result).toBe(false); + }); + }); + + describe('selectIsDownloadingSuccess', () => { + it('should return DownloadingCredentialsSuccess status', () => { + const state: any = { + context: { + DownloadingCredentialsSuccess: true, + }, + }; + const result = selectIsDownloadingSuccess(state); + expect(result).toBe(true); + }); + + it('should return false when downloading not successful', () => { + const state: any = { + context: { + DownloadingCredentialsSuccess: false, + }, + }; + const result = selectIsDownloadingSuccess(state); + expect(result).toBe(false); + }); + }); +}); diff --git a/machines/backupAndRestore/backup/backupModel.test.ts b/machines/backupAndRestore/backup/backupModel.test.ts new file mode 100644 index 00000000..5c55e115 --- /dev/null +++ b/machines/backupAndRestore/backup/backupModel.test.ts @@ -0,0 +1,151 @@ +import {backupModel} from './backupModel'; + +describe('backupModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(backupModel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(backupModel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(backupModel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = backupModel.initialContext; + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + expect(typeof initialContext.serviceRefs).toBe('object'); + }); + + it('should have dataFromStorage as empty object', () => { + expect(initialContext.dataFromStorage).toEqual({}); + expect(typeof initialContext.dataFromStorage).toBe('object'); + }); + + it('should have fileName as empty string', () => { + expect(initialContext.fileName).toBe(''); + expect(typeof initialContext.fileName).toBe('string'); + }); + + it('should have lastBackupDetails as null', () => { + expect(initialContext.lastBackupDetails).toBeNull(); + }); + + it('should have errorReason as empty string', () => { + expect(initialContext.errorReason).toBe(''); + expect(typeof initialContext.errorReason).toBe('string'); + }); + + it('should have isAutoBackUp as true', () => { + expect(initialContext.isAutoBackUp).toBe(true); + expect(typeof initialContext.isAutoBackUp).toBe('boolean'); + }); + + it('should have isLoadingBackupDetails as true', () => { + expect(initialContext.isLoadingBackupDetails).toBe(true); + expect(typeof initialContext.isLoadingBackupDetails).toBe('boolean'); + }); + + it('should have showBackupInProgress as false', () => { + expect(initialContext.showBackupInProgress).toBe(false); + expect(typeof initialContext.showBackupInProgress).toBe('boolean'); + }); + + it('should have all 8 required properties', () => { + const properties = Object.keys(initialContext); + expect(properties).toHaveLength(8); + }); + }); + + describe('String properties', () => { + const context = backupModel.initialContext; + + it('all empty string properties should be empty', () => { + const emptyStrings = [context.fileName, context.errorReason]; + + emptyStrings.forEach(str => { + expect(str).toBe(''); + expect(typeof str).toBe('string'); + }); + }); + }); + + describe('Object properties', () => { + const context = backupModel.initialContext; + + it('all empty object properties should be empty objects', () => { + const emptyObjects = [context.serviceRefs, context.dataFromStorage]; + + emptyObjects.forEach(obj => { + expect(typeof obj).toBe('object'); + expect(Object.keys(obj)).toHaveLength(0); + }); + }); + }); + + describe('Boolean properties', () => { + const context = backupModel.initialContext; + + it('isAutoBackUp should be true', () => { + expect(context.isAutoBackUp).toBe(true); + expect(typeof context.isAutoBackUp).toBe('boolean'); + }); + + it('isLoadingBackupDetails should be true', () => { + expect(context.isLoadingBackupDetails).toBe(true); + expect(typeof context.isLoadingBackupDetails).toBe('boolean'); + }); + + it('showBackupInProgress should be false', () => { + expect(context.showBackupInProgress).toBe(false); + expect(typeof context.showBackupInProgress).toBe('boolean'); + }); + + it('should have correct initial values for boolean properties', () => { + expect(context.isAutoBackUp).toBe(true); + expect(context.isLoadingBackupDetails).toBe(true); + expect(context.showBackupInProgress).toBe(false); + }); + }); + + describe('Null properties', () => { + const context = backupModel.initialContext; + + it('lastBackupDetails should be null', () => { + expect(context.lastBackupDetails).toBeNull(); + }); + }); + + describe('Model events', () => { + it('should have events object', () => { + expect(backupModel.events).toBeDefined(); + expect(typeof backupModel.events).toBe('object'); + }); + + it('should have event creators', () => { + const eventKeys = Object.keys(backupModel.events); + expect(eventKeys.length).toBeGreaterThan(0); + }); + }); + + describe('Property types validation', () => { + const context = backupModel.initialContext; + + it('should have correct types for all properties', () => { + expect(typeof context.serviceRefs).toBe('object'); + expect(typeof context.dataFromStorage).toBe('object'); + expect(typeof context.fileName).toBe('string'); + expect(context.lastBackupDetails).toBeNull(); + expect(typeof context.errorReason).toBe('string'); + expect(typeof context.isAutoBackUp).toBe('boolean'); + expect(typeof context.isLoadingBackupDetails).toBe('boolean'); + expect(typeof context.showBackupInProgress).toBe('boolean'); + }); + }); +}); diff --git a/machines/backupAndRestore/backup/backupSelector.test.ts b/machines/backupAndRestore/backup/backupSelector.test.ts new file mode 100644 index 00000000..6a81cdb6 --- /dev/null +++ b/machines/backupAndRestore/backup/backupSelector.test.ts @@ -0,0 +1,214 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectIsBackupInprogress, + selectIsLoadingBackupDetails, + selectIsBackingUpSuccess, + selectIsBackingUpFailure, + selectIsNetworkError, + lastBackupDetails, + selectBackupErrorReason, + selectShowBackupInProgress, +} from './backupSelector'; + +describe('backupSelector', () => { + describe('selectIsBackupInprogress', () => { + it('should return true when in checkDataAvailabilityForBackup state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => + state === 'backingUp.checkDataAvailabilityForBackup', + ), + }; + expect(selectIsBackupInprogress(mockState)).toBe(true); + }); + + it('should return true when in checkStorageAvailability state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'backingUp.checkStorageAvailability', + ), + }; + expect(selectIsBackupInprogress(mockState)).toBe(true); + }); + + it('should return true when in fetchDataFromDB state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'backingUp.fetchDataFromDB', + ), + }; + expect(selectIsBackupInprogress(mockState)).toBe(true); + }); + + it('should return true when in writeDataToFile state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'backingUp.writeDataToFile', + ), + }; + expect(selectIsBackupInprogress(mockState)).toBe(true); + }); + + it('should return true when in zipBackupFile state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'backingUp.zipBackupFile', + ), + }; + expect(selectIsBackupInprogress(mockState)).toBe(true); + }); + + it('should return true when in uploadBackupFile state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'backingUp.uploadBackupFile', + ), + }; + expect(selectIsBackupInprogress(mockState)).toBe(true); + }); + + it('should return false when not in any backup progress state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsBackupInprogress(mockState)).toBe(false); + }); + }); + + describe('selectIsLoadingBackupDetails', () => { + it('should return isLoadingBackupDetails from context', () => { + const mockState: any = { + context: { + isLoadingBackupDetails: true, + }, + }; + expect(selectIsLoadingBackupDetails(mockState)).toBe(true); + }); + + it('should return false when isLoadingBackupDetails is false', () => { + const mockState: any = { + context: { + isLoadingBackupDetails: false, + }, + }; + expect(selectIsLoadingBackupDetails(mockState)).toBe(false); + }); + }); + + describe('selectIsBackingUpSuccess', () => { + it('should return true when in backingUp.success state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'backingUp.success'), + }; + expect(selectIsBackingUpSuccess(mockState)).toBe(true); + }); + + it('should return false when not in success state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsBackingUpSuccess(mockState)).toBe(false); + }); + }); + + describe('selectIsBackingUpFailure', () => { + it('should return true when in backingUp.failure state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'backingUp.failure'), + }; + expect(selectIsBackingUpFailure(mockState)).toBe(true); + }); + + it('should return false when not in failure state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsBackingUpFailure(mockState)).toBe(false); + }); + }); + + describe('selectIsNetworkError', () => { + it('should return true when in fetchLastBackupDetails.noInternet state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'fetchLastBackupDetails.noInternet', + ), + }; + expect(selectIsNetworkError(mockState)).toBe(true); + }); + + it('should return false when not in network error state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsNetworkError(mockState)).toBe(false); + }); + }); + + describe('lastBackupDetails', () => { + it('should return lastBackupDetails from context', () => { + const mockDetails = { + timestamp: '2024-01-01', + size: '10MB', + fileName: 'backup_123.zip', + }; + const mockState: any = { + context: { + lastBackupDetails: mockDetails, + }, + }; + expect(lastBackupDetails(mockState)).toBe(mockDetails); + }); + + it('should return undefined when lastBackupDetails is not set', () => { + const mockState: any = { + context: { + lastBackupDetails: undefined, + }, + }; + expect(lastBackupDetails(mockState)).toBeUndefined(); + }); + }); + + describe('selectBackupErrorReason', () => { + it('should return errorReason from context', () => { + const mockState: any = { + context: { + errorReason: 'Insufficient storage space', + }, + }; + expect(selectBackupErrorReason(mockState)).toBe( + 'Insufficient storage space', + ); + }); + + it('should return null when no error', () => { + const mockState: any = { + context: { + errorReason: null, + }, + }; + expect(selectBackupErrorReason(mockState)).toBeNull(); + }); + }); + + describe('selectShowBackupInProgress', () => { + it('should return showBackupInProgress from context', () => { + const mockState: any = { + context: { + showBackupInProgress: true, + }, + }; + expect(selectShowBackupInProgress(mockState)).toBe(true); + }); + + it('should return false when showBackupInProgress is false', () => { + const mockState: any = { + context: { + showBackupInProgress: false, + }, + }; + expect(selectShowBackupInProgress(mockState)).toBe(false); + }); + }); +}); diff --git a/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupModel.test.ts b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupModel.test.ts new file mode 100644 index 00000000..7f6fa0b7 --- /dev/null +++ b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupModel.test.ts @@ -0,0 +1,138 @@ +import {backupAndRestoreSetupModel} from './backupAndRestoreSetupModel'; + +describe('backupAndRestoreSetupModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(backupAndRestoreSetupModel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(backupAndRestoreSetupModel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(backupAndRestoreSetupModel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = backupAndRestoreSetupModel.initialContext; + + it('should have isLoading as false', () => { + expect(initialContext.isLoading).toBe(false); + expect(typeof initialContext.isLoading).toBe('boolean'); + }); + + it('should have profileInfo as undefined', () => { + expect(initialContext.profileInfo).toBeUndefined(); + }); + + it('should have errorMessage as empty string', () => { + expect(initialContext.errorMessage).toBe(''); + expect(typeof initialContext.errorMessage).toBe('string'); + }); + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + expect(typeof initialContext.serviceRefs).toBe('object'); + }); + + it('should have shouldTriggerAutoBackup as false', () => { + expect(initialContext.shouldTriggerAutoBackup).toBe(false); + expect(typeof initialContext.shouldTriggerAutoBackup).toBe('boolean'); + }); + + it('should have isCloudSignedIn as false', () => { + expect(initialContext.isCloudSignedIn).toBe(false); + expect(typeof initialContext.isCloudSignedIn).toBe('boolean'); + }); + + it('should have all 6 required properties', () => { + const properties = Object.keys(initialContext); + expect(properties).toHaveLength(6); + }); + }); + + describe('String properties', () => { + const context = backupAndRestoreSetupModel.initialContext; + + it('errorMessage should be empty', () => { + expect(context.errorMessage).toBe(''); + expect(typeof context.errorMessage).toBe('string'); + }); + }); + + describe('Object properties', () => { + const context = backupAndRestoreSetupModel.initialContext; + + it('serviceRefs should be empty object', () => { + expect(typeof context.serviceRefs).toBe('object'); + expect(Object.keys(context.serviceRefs)).toHaveLength(0); + }); + }); + + describe('Boolean properties', () => { + const context = backupAndRestoreSetupModel.initialContext; + + it('isLoading should be false', () => { + expect(context.isLoading).toBe(false); + expect(typeof context.isLoading).toBe('boolean'); + }); + + it('shouldTriggerAutoBackup should be false', () => { + expect(context.shouldTriggerAutoBackup).toBe(false); + expect(typeof context.shouldTriggerAutoBackup).toBe('boolean'); + }); + + it('isCloudSignedIn should be false', () => { + expect(context.isCloudSignedIn).toBe(false); + expect(typeof context.isCloudSignedIn).toBe('boolean'); + }); + + it('should have correct initial values for boolean properties', () => { + const falseProps = [ + context.isLoading, + context.shouldTriggerAutoBackup, + context.isCloudSignedIn, + ]; + + falseProps.forEach(prop => { + expect(prop).toBe(false); + expect(typeof prop).toBe('boolean'); + }); + }); + }); + + describe('Undefined properties', () => { + const context = backupAndRestoreSetupModel.initialContext; + + it('profileInfo should be undefined', () => { + expect(context.profileInfo).toBeUndefined(); + }); + }); + + describe('Model events', () => { + it('should have events object', () => { + expect(backupAndRestoreSetupModel.events).toBeDefined(); + expect(typeof backupAndRestoreSetupModel.events).toBe('object'); + }); + + it('should have event creators', () => { + const eventKeys = Object.keys(backupAndRestoreSetupModel.events); + expect(eventKeys.length).toBeGreaterThan(0); + }); + }); + + describe('Property types validation', () => { + const context = backupAndRestoreSetupModel.initialContext; + + it('should have correct types for all properties', () => { + expect(typeof context.isLoading).toBe('boolean'); + expect(context.profileInfo).toBeUndefined(); + expect(typeof context.errorMessage).toBe('string'); + expect(typeof context.serviceRefs).toBe('object'); + expect(typeof context.shouldTriggerAutoBackup).toBe('boolean'); + expect(typeof context.isCloudSignedIn).toBe('boolean'); + }); + }); +}); diff --git a/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupSelectors.test.ts b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupSelectors.test.ts new file mode 100644 index 00000000..b559e0a4 --- /dev/null +++ b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupSelectors.test.ts @@ -0,0 +1,289 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectIsLoading, + selectProfileInfo, + selectIsNetworkError, + selectShouldTriggerAutoBackup, + selectShowAccountSelectionConfirmation, + selectIsSigningIn, + selectIsSigningInSuccessful, + selectIsSigningFailure, + selectIsCloudSignedInFailed, +} from './backupAndRestoreSetupSelectors'; + +describe('backupAndRestoreSetupSelectors', () => { + const mockProfileInfo = { + email: 'test@example.com', + name: 'Test User', + id: 'user123', + }; + + const mockState: any = { + context: { + isLoading: false, + profileInfo: mockProfileInfo, + shouldTriggerAutoBackup: true, + }, + matches: jest.fn(() => false), + }; + + describe('selectIsLoading', () => { + it('should return loading status from context', () => { + const result = selectIsLoading(mockState); + expect(result).toBe(false); + }); + + it('should return true when loading', () => { + const state: any = { + ...mockState, + context: {...mockState.context, isLoading: true}, + }; + const result = selectIsLoading(state); + expect(result).toBe(true); + }); + }); + + describe('selectProfileInfo', () => { + it('should return profile info from context', () => { + const result = selectProfileInfo(mockState); + expect(result).toEqual(mockProfileInfo); + }); + + it('should return profile with all properties', () => { + const result = selectProfileInfo(mockState); + expect(result).toHaveProperty('email'); + expect(result).toHaveProperty('name'); + expect(result).toHaveProperty('id'); + }); + + it('should handle null profile info', () => { + const state: any = { + ...mockState, + context: {...mockState.context, profileInfo: null}, + }; + const result = selectProfileInfo(state); + expect(result).toBeNull(); + }); + }); + + describe('selectIsNetworkError', () => { + it('should return true when in init.noInternet state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'init.noInternet'), + }; + const result = selectIsNetworkError(state); + expect(result).toBe(true); + }); + + it('should return true when in checkSignIn.noInternet state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'checkSignIn.noInternet'), + }; + const result = selectIsNetworkError(state); + expect(result).toBe(true); + }); + + it('should return true when in signIn.noInternet state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'signIn.noInternet'), + }; + const result = selectIsNetworkError(state); + expect(result).toBe(true); + }); + + it('should return false when not in any noInternet state', () => { + const result = selectIsNetworkError(mockState); + expect(result).toBe(false); + }); + + it('should call matches with all three network error states', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsNetworkError(state); + expect(state.matches).toHaveBeenCalledWith('init.noInternet'); + expect(state.matches).toHaveBeenCalledWith('checkSignIn.noInternet'); + expect(state.matches).toHaveBeenCalledWith('signIn.noInternet'); + }); + }); + + describe('selectShouldTriggerAutoBackup', () => { + it('should return auto backup trigger flag', () => { + const result = selectShouldTriggerAutoBackup(mockState); + expect(result).toBe(true); + }); + + it('should return false when auto backup should not trigger', () => { + const state: any = { + ...mockState, + context: {...mockState.context, shouldTriggerAutoBackup: false}, + }; + const result = selectShouldTriggerAutoBackup(state); + expect(result).toBe(false); + }); + }); + + describe('selectShowAccountSelectionConfirmation', () => { + it('should return true when in selectCloudAccount state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'selectCloudAccount'), + }; + const result = selectShowAccountSelectionConfirmation(state); + expect(result).toBe(true); + }); + + it('should call matches with selectCloudAccount', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectShowAccountSelectionConfirmation(state); + expect(state.matches).toHaveBeenCalledWith('selectCloudAccount'); + }); + + it('should return false when not in selectCloudAccount state', () => { + const result = selectShowAccountSelectionConfirmation(mockState); + expect(result).toBe(false); + }); + }); + + describe('selectIsSigningIn', () => { + it('should return true when in signIn state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'signIn'), + }; + const result = selectIsSigningIn(state); + expect(result).toBe(true); + }); + + it('should call matches with signIn', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsSigningIn(state); + expect(state.matches).toHaveBeenCalledWith('signIn'); + }); + + it('should return false when not signing in', () => { + const result = selectIsSigningIn(mockState); + expect(result).toBe(false); + }); + }); + + describe('selectIsSigningInSuccessful', () => { + it('should return true when in backupAndRestore state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'backupAndRestore'), + }; + const result = selectIsSigningInSuccessful(state); + expect(result).toBe(true); + }); + + it('should call matches with backupAndRestore', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsSigningInSuccessful(state); + expect(state.matches).toHaveBeenCalledWith('backupAndRestore'); + }); + + it('should return false when sign in not successful', () => { + const result = selectIsSigningInSuccessful(mockState); + expect(result).toBe(false); + }); + }); + + describe('selectIsSigningFailure', () => { + it('should return true when in signIn.error state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'signIn.error'), + }; + const result = selectIsSigningFailure(state); + expect(result).toBe(true); + }); + + it('should return true when in checkSignIn.error state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'checkSignIn.error'), + }; + const result = selectIsSigningFailure(state); + expect(result).toBe(true); + }); + + it('should return false when not in error state', () => { + const result = selectIsSigningFailure(mockState); + expect(result).toBe(false); + }); + + it('should call matches with both error states', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsSigningFailure(state); + expect(state.matches).toHaveBeenCalledWith('signIn.error'); + expect(state.matches).toHaveBeenCalledWith('checkSignIn.error'); + }); + }); + + describe('selectIsCloudSignedInFailed', () => { + it('should return true when in checkSignIn.error state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'checkSignIn.error'), + }; + const result = selectIsCloudSignedInFailed(state); + expect(result).toBe(true); + }); + + it('should call matches with checkSignIn.error', () => { + const state: any = { + ...mockState, + matches: jest.fn(() => false), + }; + selectIsCloudSignedInFailed(state); + expect(state.matches).toHaveBeenCalledWith('checkSignIn.error'); + }); + + it('should return false when cloud sign in did not fail', () => { + const result = selectIsCloudSignedInFailed(mockState); + expect(result).toBe(false); + }); + }); + + describe('Edge cases', () => { + it('should handle empty profile info', () => { + const state: any = { + ...mockState, + context: {...mockState.context, profileInfo: {}}, + }; + const result = selectProfileInfo(state); + expect(result).toEqual({}); + }); + + it('should handle undefined values', () => { + const state: any = { + ...mockState, + context: { + isLoading: undefined, + profileInfo: undefined, + shouldTriggerAutoBackup: undefined, + }, + }; + expect(selectIsLoading(state)).toBeUndefined(); + expect(selectProfileInfo(state)).toBeUndefined(); + expect(selectShouldTriggerAutoBackup(state)).toBeUndefined(); + }); + }); +}); diff --git a/machines/backupAndRestore/restore/restoreModel.test.ts b/machines/backupAndRestore/restore/restoreModel.test.ts new file mode 100644 index 00000000..caee3e39 --- /dev/null +++ b/machines/backupAndRestore/restore/restoreModel.test.ts @@ -0,0 +1,114 @@ +import {restoreModel} from './restoreModel'; + +describe('restoreModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(restoreModel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(restoreModel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(restoreModel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = restoreModel.initialContext; + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + expect(typeof initialContext.serviceRefs).toBe('object'); + }); + + it('should have fileName as empty string', () => { + expect(initialContext.fileName).toBe(''); + expect(typeof initialContext.fileName).toBe('string'); + }); + + it('should have dataFromBackupFile as empty object', () => { + expect(initialContext.dataFromBackupFile).toEqual({}); + expect(typeof initialContext.dataFromBackupFile).toBe('object'); + }); + + it('should have errorReason as empty string', () => { + expect(initialContext.errorReason).toBe(''); + expect(typeof initialContext.errorReason).toBe('string'); + }); + + it('should have showRestoreInProgress as false', () => { + expect(initialContext.showRestoreInProgress).toBe(false); + expect(typeof initialContext.showRestoreInProgress).toBe('boolean'); + }); + + it('should have all 5 required properties', () => { + const properties = Object.keys(initialContext); + expect(properties).toHaveLength(5); + }); + }); + + describe('String properties', () => { + const context = restoreModel.initialContext; + + it('all empty string properties should be empty', () => { + const emptyStrings = [context.fileName, context.errorReason]; + + emptyStrings.forEach(str => { + expect(str).toBe(''); + expect(typeof str).toBe('string'); + }); + }); + }); + + describe('Object properties', () => { + const context = restoreModel.initialContext; + + it('all empty object properties should be empty objects', () => { + const emptyObjects = [context.serviceRefs, context.dataFromBackupFile]; + + emptyObjects.forEach(obj => { + expect(typeof obj).toBe('object'); + expect(Object.keys(obj)).toHaveLength(0); + }); + }); + }); + + describe('Boolean properties', () => { + const context = restoreModel.initialContext; + + it('showRestoreInProgress should be false', () => { + expect(context.showRestoreInProgress).toBe(false); + expect(typeof context.showRestoreInProgress).toBe('boolean'); + }); + + it('should have correct initial values for boolean properties', () => { + expect(context.showRestoreInProgress).toBe(false); + }); + }); + + describe('Model events', () => { + it('should have events object', () => { + expect(restoreModel.events).toBeDefined(); + expect(typeof restoreModel.events).toBe('object'); + }); + + it('should have event creators', () => { + const eventKeys = Object.keys(restoreModel.events); + expect(eventKeys.length).toBeGreaterThan(0); + }); + }); + + describe('Property types validation', () => { + const context = restoreModel.initialContext; + + it('should have correct types for all properties', () => { + expect(typeof context.serviceRefs).toBe('object'); + expect(typeof context.fileName).toBe('string'); + expect(typeof context.dataFromBackupFile).toBe('object'); + expect(typeof context.errorReason).toBe('string'); + expect(typeof context.showRestoreInProgress).toBe('boolean'); + }); + }); +}); diff --git a/machines/backupAndRestore/restore/restoreSelector.test.ts b/machines/backupAndRestore/restore/restoreSelector.test.ts new file mode 100644 index 00000000..ebe6dc88 --- /dev/null +++ b/machines/backupAndRestore/restore/restoreSelector.test.ts @@ -0,0 +1,125 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectErrorReason, + selectIsBackUpRestoring, + selectIsBackUpRestoreSuccess, + selectIsBackUpRestoreFailure, + selectShowRestoreInProgress, +} from './restoreSelector'; + +describe('restoreSelector', () => { + describe('selectErrorReason', () => { + it('should return errorReason from context', () => { + const mockState: any = { + context: { + errorReason: 'Failed to restore backup', + }, + }; + expect(selectErrorReason(mockState)).toBe('Failed to restore backup'); + }); + + it('should return null when no error', () => { + const mockState: any = { + context: { + errorReason: null, + }, + }; + expect(selectErrorReason(mockState)).toBeNull(); + }); + }); + + describe('selectIsBackUpRestoring', () => { + it('should return true when in restoreBackup state but not success or failure', () => { + const mockState: any = { + matches: jest.fn((state: string) => { + if (state === 'restoreBackup') return true; + if (state === 'restoreBackup.success') return false; + if (state === 'restoreBackup.failure') return false; + return false; + }), + }; + expect(selectIsBackUpRestoring(mockState)).toBe(true); + }); + + it('should return false when in restoreBackup.success state', () => { + const mockState: any = { + matches: jest.fn((state: string) => { + if (state === 'restoreBackup') return true; + if (state === 'restoreBackup.success') return true; + return false; + }), + }; + expect(selectIsBackUpRestoring(mockState)).toBe(false); + }); + + it('should return false when in restoreBackup.failure state', () => { + const mockState: any = { + matches: jest.fn((state: string) => { + if (state === 'restoreBackup') return true; + if (state === 'restoreBackup.failure') return true; + return false; + }), + }; + expect(selectIsBackUpRestoring(mockState)).toBe(false); + }); + + it('should return false when not in restoreBackup state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsBackUpRestoring(mockState)).toBe(false); + }); + }); + + describe('selectIsBackUpRestoreSuccess', () => { + it('should return true when in restoreBackup.success state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'restoreBackup.success'), + }; + expect(selectIsBackUpRestoreSuccess(mockState)).toBe(true); + }); + + it('should return false when not in success state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsBackUpRestoreSuccess(mockState)).toBe(false); + }); + }); + + describe('selectIsBackUpRestoreFailure', () => { + it('should return true when in restoreBackup.failure state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'restoreBackup.failure'), + }; + expect(selectIsBackUpRestoreFailure(mockState)).toBe(true); + }); + + it('should return false when not in failure state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsBackUpRestoreFailure(mockState)).toBe(false); + }); + }); + + describe('selectShowRestoreInProgress', () => { + it('should return showRestoreInProgress from context', () => { + const mockState: any = { + context: { + showRestoreInProgress: true, + }, + }; + expect(selectShowRestoreInProgress(mockState)).toBe(true); + }); + + it('should return false when showRestoreInProgress is false', () => { + const mockState: any = { + context: { + showRestoreInProgress: false, + }, + }; + expect(selectShowRestoreInProgress(mockState)).toBe(false); + }); + }); +}); diff --git a/machines/bleShare/commonSelectors.test.ts b/machines/bleShare/commonSelectors.test.ts new file mode 100644 index 00000000..c583253d --- /dev/null +++ b/machines/bleShare/commonSelectors.test.ts @@ -0,0 +1,298 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectIsCancelling, + selectIsReviewing, + selectIsAccepted, + selectIsRejected, + selectIsVerifyingIdentity, + selectIsInvalidIdentity, + selectIsDisconnected, + selectIsBluetoothDenied, + selectBleError, + selectIsExchangingDeviceInfo, + selectIsExchangingDeviceInfoTimeout, + selectIsOffline, + selectIsHandlingBleError, + selectReadyForBluetoothStateCheck, + selectIsNearByDevicesPermissionDenied, + selectIsBluetoothPermissionDenied, + selectIsStartPermissionCheck, + selectIsLocationPermissionRationale, +} from './commonSelectors'; + +describe('commonSelectors', () => { + const mockState: any = { + context: { + bleError: null, + readyForBluetoothStateCheck: false, + }, + matches: jest.fn(() => false), + }; + + describe('selectIsCancelling', () => { + it('should return true when in cancelling state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'cancelling'), + }; + expect(selectIsCancelling(state)).toBe(true); + }); + + it('should return false when not cancelling', () => { + expect(selectIsCancelling(mockState)).toBe(false); + }); + }); + + describe('selectIsReviewing', () => { + it('should return true when in reviewing state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing'), + }; + expect(selectIsReviewing(state)).toBe(true); + }); + + it('should return false when not reviewing', () => { + expect(selectIsReviewing(mockState)).toBe(false); + }); + }); + + describe('selectIsAccepted', () => { + it('should return true when in reviewing.accepted state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.accepted'), + }; + expect(selectIsAccepted(state)).toBe(true); + }); + + it('should call matches with reviewing.accepted', () => { + selectIsAccepted(mockState); + expect(mockState.matches).toHaveBeenCalledWith('reviewing.accepted'); + }); + }); + + describe('selectIsRejected', () => { + it('should return true when in reviewing.rejected state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.rejected'), + }; + expect(selectIsRejected(state)).toBe(true); + }); + + it('should call matches with reviewing.rejected', () => { + selectIsRejected(mockState); + expect(mockState.matches).toHaveBeenCalledWith('reviewing.rejected'); + }); + }); + + describe('selectIsVerifyingIdentity', () => { + it('should return true when in reviewing.verifyingIdentity state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.verifyingIdentity'), + }; + expect(selectIsVerifyingIdentity(state)).toBe(true); + }); + + it('should call matches with reviewing.verifyingIdentity', () => { + selectIsVerifyingIdentity(mockState); + expect(mockState.matches).toHaveBeenCalledWith( + 'reviewing.verifyingIdentity', + ); + }); + }); + + describe('selectIsInvalidIdentity', () => { + it('should return true when in reviewing.invalidIdentity state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.invalidIdentity'), + }; + expect(selectIsInvalidIdentity(state)).toBe(true); + }); + + it('should call matches with reviewing.invalidIdentity', () => { + selectIsInvalidIdentity(mockState); + expect(mockState.matches).toHaveBeenCalledWith( + 'reviewing.invalidIdentity', + ); + }); + }); + + describe('selectIsDisconnected', () => { + it('should return true when in disconnected state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'disconnected'), + }; + expect(selectIsDisconnected(state)).toBe(true); + }); + + it('should call matches with disconnected', () => { + selectIsDisconnected(mockState); + expect(mockState.matches).toHaveBeenCalledWith('disconnected'); + }); + }); + + describe('selectIsBluetoothDenied', () => { + it('should return true when in bluetoothDenied state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'bluetoothDenied'), + }; + expect(selectIsBluetoothDenied(state)).toBe(true); + }); + + it('should call matches with bluetoothDenied', () => { + selectIsBluetoothDenied(mockState); + expect(mockState.matches).toHaveBeenCalledWith('bluetoothDenied'); + }); + }); + + describe('selectBleError', () => { + it('should return BLE error from context', () => { + const bleError = {code: 'BLE_001', message: 'Connection failed'}; + const state: any = { + ...mockState, + context: {...mockState.context, bleError}, + }; + expect(selectBleError(state)).toEqual(bleError); + }); + + it('should return null when no BLE error', () => { + expect(selectBleError(mockState)).toBeNull(); + }); + }); + + describe('TODO selectors (hardcoded)', () => { + it('selectIsExchangingDeviceInfo should always return false', () => { + expect(selectIsExchangingDeviceInfo()).toBe(false); + }); + + it('selectIsExchangingDeviceInfoTimeout should always return false', () => { + expect(selectIsExchangingDeviceInfoTimeout()).toBe(false); + }); + + it('selectIsOffline should always return false', () => { + expect(selectIsOffline()).toBe(false); + }); + }); + + describe('selectIsHandlingBleError', () => { + it('should return true when in handlingBleError state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'handlingBleError'), + }; + expect(selectIsHandlingBleError(state)).toBe(true); + }); + + it('should call matches with handlingBleError', () => { + selectIsHandlingBleError(mockState); + expect(mockState.matches).toHaveBeenCalledWith('handlingBleError'); + }); + }); + + describe('selectReadyForBluetoothStateCheck', () => { + it('should return ready status from context', () => { + expect(selectReadyForBluetoothStateCheck(mockState)).toBe(false); + }); + + it('should return true when ready', () => { + const state: any = { + ...mockState, + context: {...mockState.context, readyForBluetoothStateCheck: true}, + }; + expect(selectReadyForBluetoothStateCheck(state)).toBe(true); + }); + }); + + describe('selectIsNearByDevicesPermissionDenied', () => { + it('should return true when in nearByDevicesPermissionDenied state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'nearByDevicesPermissionDenied'), + }; + expect(selectIsNearByDevicesPermissionDenied(state)).toBe(true); + }); + + it('should call matches with nearByDevicesPermissionDenied', () => { + selectIsNearByDevicesPermissionDenied(mockState); + expect(mockState.matches).toHaveBeenCalledWith( + 'nearByDevicesPermissionDenied', + ); + }); + }); + + describe('selectIsBluetoothPermissionDenied', () => { + it('should return true when in bluetoothPermissionDenied state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'bluetoothPermissionDenied'), + }; + expect(selectIsBluetoothPermissionDenied(state)).toBe(true); + }); + + it('should call matches with bluetoothPermissionDenied', () => { + selectIsBluetoothPermissionDenied(mockState); + expect(mockState.matches).toHaveBeenCalledWith( + 'bluetoothPermissionDenied', + ); + }); + }); + + describe('selectIsStartPermissionCheck', () => { + it('should return true when in startPermissionCheck state', () => { + const state: any = { + ...mockState, + matches: jest.fn((s: string) => s === 'startPermissionCheck'), + }; + expect(selectIsStartPermissionCheck(state)).toBe(true); + }); + + it('should call matches with startPermissionCheck', () => { + selectIsStartPermissionCheck(mockState); + expect(mockState.matches).toHaveBeenCalledWith('startPermissionCheck'); + }); + }); + + describe('selectIsLocationPermissionRationale', () => { + it('should return true when in checkingLocationState.LocationPermissionRationale state', () => { + const state: any = { + ...mockState, + matches: jest.fn( + (s: string) => + s === 'checkingLocationState.LocationPermissionRationale', + ), + }; + expect(selectIsLocationPermissionRationale(state)).toBe(true); + }); + + it('should call matches with correct state path', () => { + selectIsLocationPermissionRationale(mockState); + expect(mockState.matches).toHaveBeenCalledWith( + 'checkingLocationState.LocationPermissionRationale', + ); + }); + }); + + describe('Edge cases', () => { + it('should handle undefined bleError', () => { + const state: any = { + ...mockState, + context: {...mockState.context, bleError: undefined}, + }; + expect(selectBleError(state)).toBeUndefined(); + }); + + it('should handle empty bleError object', () => { + const state: any = { + ...mockState, + context: {...mockState.context, bleError: {}}, + }; + expect(selectBleError(state)).toEqual({}); + }); + }); +}); diff --git a/machines/bleShare/request/selectors.test.ts b/machines/bleShare/request/selectors.test.ts new file mode 100644 index 00000000..ab59228e --- /dev/null +++ b/machines/bleShare/request/selectors.test.ts @@ -0,0 +1,233 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectSenderInfo, + selectCredential, + selectVerifiableCredentialData, + selectIsReviewingInIdle, + selectIsWaitingForConnection, + selectIsCheckingBluetoothService, + selectIsWaitingForVc, + selectIsWaitingForVcTimeout, + selectOpenId4VpUri, + selectIsAccepting, + selectIsDisplayingIncomingVC, + selectIsSavingFailedInIdle, + selectIsDone, + selectIsNavigatingToReceivedCards, + selectIsNavigatingToHome, +} from './selectors'; + +describe('requestMachine selectors', () => { + describe('selectSenderInfo', () => { + it('should return senderInfo from context', () => { + const mockSenderInfo = {name: 'John Doe', deviceId: 'device-123'}; + const mockState: any = { + context: { + senderInfo: mockSenderInfo, + }, + }; + expect(selectSenderInfo(mockState)).toBe(mockSenderInfo); + }); + }); + + describe('selectCredential', () => { + it('should return verifiableCredential from incomingVc', () => { + const mockVC = {credential: {id: 'cred-123'}}; + const mockState: any = { + context: { + incomingVc: { + verifiableCredential: mockVC, + }, + }, + }; + expect(selectCredential(mockState)).toBe(mockVC); + }); + }); + + describe('selectVerifiableCredentialData', () => { + it('should return formatted verifiable credential data', () => { + const mockState: any = { + context: { + incomingVc: { + vcMetadata: { + id: 'vc-001', + issuer: 'Test Issuer', + }, + verifiableCredential: { + credential: { + credentialSubject: { + face: 'base64-face-data', + }, + }, + issuerLogo: 'https://example.com/logo.png', + wellKnown: 'https://example.com/.well-known', + credentialConfigurationId: 'config-123', + }, + }, + }, + }; + + const result = selectVerifiableCredentialData(mockState); + expect(result.issuer).toBe('Test Issuer'); + expect(result.issuerLogo).toBe('https://example.com/logo.png'); + expect(result.wellKnown).toBe('https://example.com/.well-known'); + expect(result.credentialConfigurationId).toBe('config-123'); + expect(result.face).toBe('base64-face-data'); + }); + + it('should use biometrics face when credentialSubject face is not available', () => { + const mockState: any = { + context: { + incomingVc: { + vcMetadata: { + id: 'vc-002', + issuer: 'Mosip', + }, + credential: { + biometrics: { + face: 'biometric-face-data', + }, + }, + verifiableCredential: {}, + }, + }, + }; + + const result = selectVerifiableCredentialData(mockState); + expect(result.face).toBe('biometric-face-data'); + }); + }); + + describe('selectIsReviewingInIdle', () => { + it('should return true when in reviewing.idle state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'reviewing.idle'), + }; + expect(selectIsReviewingInIdle(mockState)).toBe(true); + }); + + it('should return false when not in reviewing.idle state', () => { + const mockState: any = { + matches: jest.fn(() => false), + }; + expect(selectIsReviewingInIdle(mockState)).toBe(false); + }); + }); + + describe('selectIsWaitingForConnection', () => { + it('should return true when in waitingForConnection state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'waitingForConnection'), + }; + expect(selectIsWaitingForConnection(mockState)).toBe(true); + }); + }); + + describe('selectIsCheckingBluetoothService', () => { + it('should return true when in checkingBluetoothService state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'checkingBluetoothService', + ), + }; + expect(selectIsCheckingBluetoothService(mockState)).toBe(true); + }); + }); + + describe('selectIsWaitingForVc', () => { + it('should return true when in waitingForVc.inProgress state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'waitingForVc.inProgress', + ), + }; + expect(selectIsWaitingForVc(mockState)).toBe(true); + }); + }); + + describe('selectIsWaitingForVcTimeout', () => { + it('should return true when in waitingForVc.timeout state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'waitingForVc.timeout'), + }; + expect(selectIsWaitingForVcTimeout(mockState)).toBe(true); + }); + }); + + describe('selectOpenId4VpUri', () => { + it('should return openId4VpUri from context', () => { + const mockState: any = { + context: { + openId4VpUri: 'openid4vp://verify?request=abc123', + }, + }; + expect(selectOpenId4VpUri(mockState)).toBe( + 'openid4vp://verify?request=abc123', + ); + }); + }); + + describe('selectIsAccepting', () => { + it('should return true when in reviewing.accepting state', () => { + const mockState: any = { + matches: jest.fn((state: string) => state === 'reviewing.accepting'), + }; + expect(selectIsAccepting(mockState)).toBe(true); + }); + }); + + describe('selectIsDisplayingIncomingVC', () => { + it('should return true when in reviewing.displayingIncomingVC state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'reviewing.displayingIncomingVC', + ), + }; + expect(selectIsDisplayingIncomingVC(mockState)).toBe(true); + }); + }); + + describe('selectIsSavingFailedInIdle', () => { + it('should return true when in reviewing.savingFailed.idle state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'reviewing.savingFailed.idle', + ), + }; + expect(selectIsSavingFailedInIdle(mockState)).toBe(true); + }); + }); + + describe('selectIsDone', () => { + it('should return true when in reviewing.navigatingToHistory state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'reviewing.navigatingToHistory', + ), + }; + expect(selectIsDone(mockState)).toBe(true); + }); + }); + + describe('selectIsNavigatingToReceivedCards', () => { + it('should return true when in reviewing.navigatingToReceivedCards state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'reviewing.navigatingToReceivedCards', + ), + }; + expect(selectIsNavigatingToReceivedCards(mockState)).toBe(true); + }); + }); + + describe('selectIsNavigatingToHome', () => { + it('should return true when in reviewing.navigatingToHome state', () => { + const mockState: any = { + matches: jest.fn( + (state: string) => state === 'reviewing.navigatingToHome', + ), + }; + expect(selectIsNavigatingToHome(mockState)).toBe(true); + }); + }); +}); diff --git a/machines/bleShare/scan/scanModel.test.ts b/machines/bleShare/scan/scanModel.test.ts new file mode 100644 index 00000000..dd591484 --- /dev/null +++ b/machines/bleShare/scan/scanModel.test.ts @@ -0,0 +1,292 @@ +import {ScanModel} from './scanModel'; +import {VCShareFlowType} from '../../../shared/Utils'; + +describe('ScanModel', () => { + describe('Model structure', () => { + it('should be defined', () => { + expect(ScanModel).toBeDefined(); + }); + + it('should have initialContext', () => { + expect(ScanModel.initialContext).toBeDefined(); + }); + + it('should have events', () => { + expect(ScanModel.events).toBeDefined(); + }); + }); + + describe('Initial Context', () => { + const initialContext = ScanModel.initialContext; + + it('should have serviceRefs as empty object', () => { + expect(initialContext.serviceRefs).toEqual({}); + expect(typeof initialContext.serviceRefs).toBe('object'); + }); + + it('should have senderInfo as empty object', () => { + expect(initialContext.senderInfo).toEqual({}); + expect(typeof initialContext.senderInfo).toBe('object'); + }); + + it('should have receiverInfo as empty object', () => { + expect(initialContext.receiverInfo).toEqual({}); + expect(typeof initialContext.receiverInfo).toBe('object'); + }); + + it('should have selectedVc as empty object', () => { + expect(initialContext.selectedVc).toEqual({}); + expect(typeof initialContext.selectedVc).toBe('object'); + }); + + it('should have bleError as empty object', () => { + expect(initialContext.bleError).toEqual({}); + expect(typeof initialContext.bleError).toBe('object'); + }); + + it('should have loggers as empty array', () => { + expect(initialContext.loggers).toEqual([]); + expect(Array.isArray(initialContext.loggers)).toBe(true); + }); + + it('should have vcName as empty string', () => { + expect(initialContext.vcName).toBe(''); + expect(typeof initialContext.vcName).toBe('string'); + }); + + it('should have flowType as SIMPLE_SHARE', () => { + expect(initialContext.flowType).toBe(VCShareFlowType.SIMPLE_SHARE); + }); + + it('should have openID4VPFlowType as empty string', () => { + expect(initialContext.openID4VPFlowType).toBe(''); + expect(typeof initialContext.openID4VPFlowType).toBe('string'); + }); + + it('should have verificationImage as empty object', () => { + expect(initialContext.verificationImage).toEqual({}); + expect(typeof initialContext.verificationImage).toBe('object'); + }); + + it('should have openId4VpUri as empty string', () => { + expect(initialContext.openId4VpUri).toBe(''); + expect(typeof initialContext.openId4VpUri).toBe('string'); + }); + + it('should have shareLogType as empty string', () => { + expect(initialContext.shareLogType).toBe(''); + expect(typeof initialContext.shareLogType).toBe('string'); + }); + + it('should have QrLoginRef as empty object', () => { + expect(initialContext.QrLoginRef).toEqual({}); + expect(typeof initialContext.QrLoginRef).toBe('object'); + }); + + it('should have OpenId4VPRef as empty object', () => { + expect(initialContext.OpenId4VPRef).toEqual({}); + expect(typeof initialContext.OpenId4VPRef).toBe('object'); + }); + + it('should have showQuickShareSuccessBanner as false', () => { + expect(initialContext.showQuickShareSuccessBanner).toBe(false); + expect(typeof initialContext.showQuickShareSuccessBanner).toBe('boolean'); + }); + + it('should have linkCode as empty string', () => { + expect(initialContext.linkCode).toBe(''); + expect(typeof initialContext.linkCode).toBe('string'); + }); + + it('should have authorizationRequest as empty string', () => { + expect(initialContext.authorizationRequest).toBe(''); + expect(typeof initialContext.authorizationRequest).toBe('string'); + }); + + it('should have quickShareData as empty object', () => { + expect(initialContext.quickShareData).toEqual({}); + expect(typeof initialContext.quickShareData).toBe('object'); + }); + + it('should have isQrLoginViaDeepLink as false', () => { + expect(initialContext.isQrLoginViaDeepLink).toBe(false); + expect(typeof initialContext.isQrLoginViaDeepLink).toBe('boolean'); + }); + + it('should have isOVPViaDeepLink as false', () => { + expect(initialContext.isOVPViaDeepLink).toBe(false); + expect(typeof initialContext.isOVPViaDeepLink).toBe('boolean'); + }); + + it('should have showFaceAuthConsent as true', () => { + expect(initialContext.showFaceAuthConsent).toBe(true); + expect(typeof initialContext.showFaceAuthConsent).toBe('boolean'); + }); + + it('should have readyForBluetoothStateCheck as false', () => { + expect(initialContext.readyForBluetoothStateCheck).toBe(false); + expect(typeof initialContext.readyForBluetoothStateCheck).toBe('boolean'); + }); + + it('should have showFaceCaptureSuccessBanner as false', () => { + expect(initialContext.showFaceCaptureSuccessBanner).toBe(false); + expect(typeof initialContext.showFaceCaptureSuccessBanner).toBe( + 'boolean', + ); + }); + + it('should have all 23 required properties', () => { + const properties = Object.keys(initialContext); + expect(properties).toHaveLength(23); + }); + }); + + describe('String properties', () => { + const context = ScanModel.initialContext; + + it('all empty string properties should be empty', () => { + const emptyStrings = [ + context.vcName, + context.openID4VPFlowType, + context.openId4VpUri, + context.shareLogType, + context.linkCode, + context.authorizationRequest, + ]; + + emptyStrings.forEach(str => { + expect(str).toBe(''); + expect(typeof str).toBe('string'); + }); + }); + + it('flowType should be SIMPLE_SHARE enum value', () => { + expect(context.flowType).toBe(VCShareFlowType.SIMPLE_SHARE); + }); + }); + + describe('Array properties', () => { + const context = ScanModel.initialContext; + + it('loggers should be empty array', () => { + expect(Array.isArray(context.loggers)).toBe(true); + expect(context.loggers).toHaveLength(0); + }); + }); + + describe('Object properties', () => { + const context = ScanModel.initialContext; + + it('all empty object properties should be empty objects', () => { + const emptyObjects = [ + context.serviceRefs, + context.senderInfo, + context.receiverInfo, + context.selectedVc, + context.bleError, + context.verificationImage, + context.QrLoginRef, + context.OpenId4VPRef, + context.quickShareData, + ]; + + emptyObjects.forEach(obj => { + expect(typeof obj).toBe('object'); + expect(Object.keys(obj)).toHaveLength(0); + }); + }); + }); + + describe('Boolean properties', () => { + const context = ScanModel.initialContext; + + it('showQuickShareSuccessBanner should be false', () => { + expect(context.showQuickShareSuccessBanner).toBe(false); + expect(typeof context.showQuickShareSuccessBanner).toBe('boolean'); + }); + + it('isQrLoginViaDeepLink should be false', () => { + expect(context.isQrLoginViaDeepLink).toBe(false); + expect(typeof context.isQrLoginViaDeepLink).toBe('boolean'); + }); + + it('isOVPViaDeepLink should be false', () => { + expect(context.isOVPViaDeepLink).toBe(false); + expect(typeof context.isOVPViaDeepLink).toBe('boolean'); + }); + + it('showFaceAuthConsent should be true', () => { + expect(context.showFaceAuthConsent).toBe(true); + expect(typeof context.showFaceAuthConsent).toBe('boolean'); + }); + + it('readyForBluetoothStateCheck should be false', () => { + expect(context.readyForBluetoothStateCheck).toBe(false); + expect(typeof context.readyForBluetoothStateCheck).toBe('boolean'); + }); + + it('showFaceCaptureSuccessBanner should be false', () => { + expect(context.showFaceCaptureSuccessBanner).toBe(false); + expect(typeof context.showFaceCaptureSuccessBanner).toBe('boolean'); + }); + + it('should have correct initial values for boolean properties', () => { + const falseProps = [ + context.showQuickShareSuccessBanner, + context.isQrLoginViaDeepLink, + context.isOVPViaDeepLink, + context.readyForBluetoothStateCheck, + context.showFaceCaptureSuccessBanner, + ]; + + falseProps.forEach(prop => { + expect(prop).toBe(false); + expect(typeof prop).toBe('boolean'); + }); + + expect(context.showFaceAuthConsent).toBe(true); + expect(typeof context.showFaceAuthConsent).toBe('boolean'); + }); + }); + + describe('Model events', () => { + it('should have events object', () => { + expect(ScanModel.events).toBeDefined(); + expect(typeof ScanModel.events).toBe('object'); + }); + + it('should have event creators', () => { + const eventKeys = Object.keys(ScanModel.events); + expect(eventKeys.length).toBeGreaterThan(0); + }); + }); + + describe('Property types validation', () => { + const context = ScanModel.initialContext; + + it('should have correct types for all properties', () => { + expect(typeof context.vcName).toBe('string'); + expect(typeof context.openID4VPFlowType).toBe('string'); + expect(typeof context.openId4VpUri).toBe('string'); + expect(typeof context.shareLogType).toBe('string'); + expect(typeof context.linkCode).toBe('string'); + expect(typeof context.authorizationRequest).toBe('string'); + expect(Array.isArray(context.loggers)).toBe(true); + expect(typeof context.showQuickShareSuccessBanner).toBe('boolean'); + expect(typeof context.isQrLoginViaDeepLink).toBe('boolean'); + expect(typeof context.isOVPViaDeepLink).toBe('boolean'); + expect(typeof context.showFaceAuthConsent).toBe('boolean'); + expect(typeof context.readyForBluetoothStateCheck).toBe('boolean'); + expect(typeof context.showFaceCaptureSuccessBanner).toBe('boolean'); + expect(typeof context.serviceRefs).toBe('object'); + expect(typeof context.senderInfo).toBe('object'); + expect(typeof context.receiverInfo).toBe('object'); + expect(typeof context.selectedVc).toBe('object'); + expect(typeof context.bleError).toBe('object'); + expect(typeof context.verificationImage).toBe('object'); + expect(typeof context.QrLoginRef).toBe('object'); + expect(typeof context.OpenId4VPRef).toBe('object'); + expect(typeof context.quickShareData).toBe('object'); + }); + }); +}); diff --git a/machines/bleShare/scan/scanSelectors.test.ts b/machines/bleShare/scan/scanSelectors.test.ts new file mode 100644 index 00000000..9a07adda --- /dev/null +++ b/machines/bleShare/scan/scanSelectors.test.ts @@ -0,0 +1,500 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + selectFlowType, + selectOpenID4VPFlowType, + selectReceiverInfo, + selectVcName, + selectCredential, + selectVerifiableCredentialData, + selectQrLoginRef, + selectIsScanning, + selectIsQuickShareDone, + selectShowQuickShareSuccessBanner, + selectIsConnecting, + selectIsConnectingTimeout, + selectIsSelectingVc, + selectIsSendingVc, + selectIsSendingVP, + selectIsSendingVPError, + selectIsSendingVPSuccess, + selectIsFaceIdentityVerified, + selectIsSendingVcTimeout, + selectIsSendingVPTimeout, + selectIsSent, + selectIsInvalid, + selectIsLocationDenied, + selectIsLocationDisabled, + selectIsShowQrLogin, + selectIsQrLoginDone, + selectIsQrLoginDoneViaDeeplink, + selectIsQrLoginStoring, + selectIsDone, + selectIsMinimumStorageRequiredForAuditEntryLimitReached, + selectIsFaceVerificationConsent, + selectIsOVPViaDeepLink, +} from './scanSelectors'; + +describe('scanSelectors', () => { + const mockState: any = { + context: { + flowType: '', + openID4VPFlowType: '', + receiverInfo: null, + vcName: '', + selectedVc: null, + QrLoginRef: null, + showQuickShareSuccessBanner: false, + isOVPViaDeepLink: false, + showFaceCaptureSuccessBanner: false, + }, + matches: jest.fn(() => false), + }; + + describe('selectFlowType', () => { + it('should return flowType from context', () => { + const state = { + ...mockState, + context: {...mockState.context, flowType: 'simple_share'}, + }; + expect(selectFlowType(state)).toBe('simple_share'); + }); + }); + + describe('selectOpenID4VPFlowType', () => { + it('should return openID4VPFlowType from context', () => { + const state = { + ...mockState, + context: {...mockState.context, openID4VPFlowType: 'ovp_flow'}, + }; + expect(selectOpenID4VPFlowType(state)).toBe('ovp_flow'); + }); + }); + + describe('selectReceiverInfo', () => { + it('should return receiverInfo from context', () => { + const receiverInfo = {name: 'Test', id: '123'}; + const state = { + ...mockState, + context: {...mockState.context, receiverInfo}, + }; + expect(selectReceiverInfo(state)).toBe(receiverInfo); + }); + }); + + describe('selectVcName', () => { + it('should return vcName from context', () => { + const state = { + ...mockState, + context: {...mockState.context, vcName: 'National ID'}, + }; + expect(selectVcName(state)).toBe('National ID'); + }); + }); + + describe('selectCredential', () => { + it('should return credential when verifiableCredential has credential property', () => { + const mockCredential = {id: 'cred123'}; + const state = { + ...mockState, + context: { + ...mockState.context, + selectedVc: { + verifiableCredential: {credential: mockCredential}, + }, + }, + }; + const result = selectCredential(state); + expect(result).toEqual([mockCredential]); + }); + + it('should return verifiableCredential when no credential property', () => { + const mockVC = {id: 'vc456'}; + const state = { + ...mockState, + context: { + ...mockState.context, + selectedVc: { + verifiableCredential: mockVC, + }, + }, + }; + const result = selectCredential(state); + expect(result).toEqual([mockVC]); + }); + }); + + describe('selectVerifiableCredentialData', () => { + it('should return formatted credential data with face from credentialSubject', () => { + const mockCredential = { + credentialSubject: { + face: '/9j/4AAQSkZJRgABAQAAAQ...', + gender: 'Male', + UIN: '123456789', + }, + issuer: 'did:web:example.com', + issuanceDate: '2023-01-01T00:00:00Z', + }; + const state = { + ...mockState, + context: { + ...mockState.context, + selectedVc: { + verifiableCredential: {credential: mockCredential}, + vcMetadata: {}, + credential: mockCredential, + format: 'ldp_vc', + }, + }, + }; + const result = selectVerifiableCredentialData(state); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result[0]).toBeDefined(); + expect(result[0].face).toBeDefined(); + }); + + it('should use biometrics face when credentialSubject face is not available', () => { + const mockCredential = { + credentialSubject: { + gender: 'Male', + UIN: '123456789', + }, + biometrics: { + face: '/9j/4AAQSkZJRgABAQBBBB...', + }, + issuer: 'did:web:example.com', + issuanceDate: '2023-01-01T00:00:00Z', + }; + const state = { + ...mockState, + context: { + ...mockState.context, + selectedVc: { + verifiableCredential: {credential: mockCredential}, + vcMetadata: {}, + credential: mockCredential, + format: 'ldp_vc', + }, + }, + }; + const result = selectVerifiableCredentialData(state); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result[0].face).toBe('/9j/4AAQSkZJRgABAQBBBB...'); + }); + + it('should handle credential without face data', () => { + const mockCredential = { + credentialSubject: { + gender: 'Male', + UIN: '123456789', + }, + issuer: 'did:web:example.com', + issuanceDate: '2023-01-01T00:00:00Z', + }; + const state = { + ...mockState, + context: { + ...mockState.context, + selectedVc: { + verifiableCredential: {credential: mockCredential}, + vcMetadata: {}, + credential: mockCredential, + format: 'ldp_vc', + }, + }, + }; + const result = selectVerifiableCredentialData(state); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result[0].face).toBeUndefined(); + }); + }); + + describe('selectQrLoginRef', () => { + it('should return QrLoginRef from context', () => { + const ref = {current: 'test'}; + const state = { + ...mockState, + context: {...mockState.context, QrLoginRef: ref}, + }; + expect(selectQrLoginRef(state)).toBe(ref); + }); + }); + + describe('selectIsScanning', () => { + it('should return true when in findingConnection state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'findingConnection'), + }; + expect(selectIsScanning(state)).toBe(true); + }); + + it('should return false when not in findingConnection state', () => { + expect(selectIsScanning(mockState)).toBe(false); + }); + }); + + describe('selectIsQuickShareDone', () => { + it('should return true when in loadVCS.navigatingToHome state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'loadVCS.navigatingToHome'), + }; + expect(selectIsQuickShareDone(state)).toBe(true); + }); + + it('should return false when not in state', () => { + expect(selectIsQuickShareDone(mockState)).toBe(false); + }); + }); + + describe('selectShowQuickShareSuccessBanner', () => { + it('should return showQuickShareSuccessBanner from context', () => { + const state = { + ...mockState, + context: {...mockState.context, showQuickShareSuccessBanner: true}, + }; + expect(selectShowQuickShareSuccessBanner(state)).toBe(true); + }); + }); + + describe('selectIsConnecting', () => { + it('should return true when in connecting.inProgress state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'connecting.inProgress'), + }; + expect(selectIsConnecting(state)).toBe(true); + }); + }); + + describe('selectIsConnectingTimeout', () => { + it('should return true when in connecting.timeout state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'connecting.timeout'), + }; + expect(selectIsConnectingTimeout(state)).toBe(true); + }); + }); + + describe('selectIsSelectingVc', () => { + it('should return true when in reviewing.selectingVc state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.selectingVc'), + }; + expect(selectIsSelectingVc(state)).toBe(true); + }); + }); + + describe('selectIsSendingVc', () => { + it('should return true when in reviewing.sendingVc.inProgress state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.sendingVc.inProgress'), + }; + expect(selectIsSendingVc(state)).toBe(true); + }); + }); + + describe('selectIsSendingVP', () => { + it('should return true when in startVPSharing.inProgress state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'startVPSharing.inProgress'), + }; + expect(selectIsSendingVP(state)).toBe(true); + }); + }); + + describe('selectIsSendingVPError', () => { + it('should return true when in startVPSharing.showError state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'startVPSharing.showError'), + }; + expect(selectIsSendingVPError(state)).toBe(true); + }); + }); + + describe('selectIsSendingVPSuccess', () => { + it('should return true when in startVPSharing.success state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'startVPSharing.success'), + }; + expect(selectIsSendingVPSuccess(state)).toBe(true); + }); + }); + + describe('selectIsFaceIdentityVerified', () => { + it('should return true when sendingVc and banner is true', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.sendingVc.inProgress'), + context: {...mockState.context, showFaceCaptureSuccessBanner: true}, + }; + expect(selectIsFaceIdentityVerified(state)).toBe(true); + }); + + it('should return false when not in sendingVc state', () => { + const state = { + ...mockState, + context: {...mockState.context, showFaceCaptureSuccessBanner: true}, + }; + expect(selectIsFaceIdentityVerified(state)).toBe(false); + }); + }); + + describe('selectIsSendingVcTimeout', () => { + it('should return true when in reviewing.sendingVc.timeout state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.sendingVc.timeout'), + }; + expect(selectIsSendingVcTimeout(state)).toBe(true); + }); + }); + + describe('selectIsSendingVPTimeout', () => { + it('should return true when in startVPSharing.timeout state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'startVPSharing.timeout'), + }; + expect(selectIsSendingVPTimeout(state)).toBe(true); + }); + }); + + describe('selectIsSent', () => { + it('should return true when in reviewing.sendingVc.sent state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.sendingVc.sent'), + }; + expect(selectIsSent(state)).toBe(true); + }); + }); + + describe('selectIsInvalid', () => { + it('should return true when in invalid state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'invalid'), + }; + expect(selectIsInvalid(state)).toBe(true); + }); + }); + + describe('selectIsLocationDenied', () => { + it('should return true when in checkingLocationState.denied state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'checkingLocationState.denied'), + }; + expect(selectIsLocationDenied(state)).toBe(true); + }); + }); + + describe('selectIsLocationDisabled', () => { + it('should return true when in checkingLocationState.disabled state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'checkingLocationState.disabled'), + }; + expect(selectIsLocationDisabled(state)).toBe(true); + }); + }); + + describe('selectIsShowQrLogin', () => { + it('should return true when in showQrLogin state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'showQrLogin'), + }; + expect(selectIsShowQrLogin(state)).toBe(true); + }); + }); + + describe('selectIsQrLoginDone', () => { + it('should return true when in showQrLogin.navigatingToHistory state', () => { + const state = { + ...mockState, + matches: jest.fn( + (s: string) => s === 'showQrLogin.navigatingToHistory', + ), + }; + expect(selectIsQrLoginDone(state)).toBe(true); + }); + }); + + describe('selectIsQrLoginDoneViaDeeplink', () => { + it('should return true when in showQrLogin.navigatingToHome state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'showQrLogin.navigatingToHome'), + }; + expect(selectIsQrLoginDoneViaDeeplink(state)).toBe(true); + }); + }); + + describe('selectIsQrLoginStoring', () => { + it('should return true when in showQrLogin.storing state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'showQrLogin.storing'), + }; + expect(selectIsQrLoginStoring(state)).toBe(true); + }); + }); + + describe('selectIsDone', () => { + it('should return true when in reviewing.disconnect state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'reviewing.disconnect'), + }; + expect(selectIsDone(state)).toBe(true); + }); + }); + + describe('selectIsMinimumStorageRequiredForAuditEntryLimitReached', () => { + it('should return true when in restrictSharingVc state', () => { + const state = { + ...mockState, + matches: jest.fn((s: string) => s === 'restrictSharingVc'), + }; + expect( + selectIsMinimumStorageRequiredForAuditEntryLimitReached(state), + ).toBe(true); + }); + }); + + describe('selectIsFaceVerificationConsent', () => { + it('should return true when in reviewing.faceVerificationConsent state', () => { + const state = { + ...mockState, + matches: jest.fn( + (s: string) => s === 'reviewing.faceVerificationConsent', + ), + }; + expect(selectIsFaceVerificationConsent(state)).toBe(true); + }); + }); + + describe('selectIsOVPViaDeepLink', () => { + it('should return isOVPViaDeepLink from context', () => { + const state = { + ...mockState, + context: {...mockState.context, isOVPViaDeepLink: true}, + }; + expect(selectIsOVPViaDeepLink(state)).toBe(true); + }); + }); +}); diff --git a/machines/openID4VP/openID4VPModel.test.ts b/machines/openID4VP/openID4VPModel.test.ts new file mode 100644 index 00000000..0bedccdd --- /dev/null +++ b/machines/openID4VP/openID4VPModel.test.ts @@ -0,0 +1,350 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {openID4VPModel} from './openID4VPModel'; +import {KeyTypes} from '../../shared/cryptoutil/KeyTypes'; + +describe('openID4VPModel', () => { + describe('Model context', () => { + it('should initialize with default context values', () => { + const initialContext = openID4VPModel.initialContext; + + expect(initialContext.urlEncodedAuthorizationRequest).toBe(''); + expect(initialContext.checkedAll).toBe(false); + expect(initialContext.isShareWithSelfie).toBe(false); + expect(initialContext.showFaceAuthConsent).toBe(true); + expect(initialContext.purpose).toBe(''); + expect(initialContext.error).toBe(''); + expect(initialContext.publicKey).toBe(''); + expect(initialContext.privateKey).toBe(''); + expect(initialContext.keyType).toBe(KeyTypes.ED25519); + expect(initialContext.flowType).toBe(''); + expect(initialContext.openID4VPRetryCount).toBe(0); + expect(initialContext.showFaceCaptureSuccessBanner).toBe(false); + expect(initialContext.isFaceVerificationRetryAttempt).toBe(false); + expect(initialContext.requestedClaims).toBe(''); + expect(initialContext.showLoadingScreen).toBe(false); + expect(initialContext.isOVPViaDeepLink).toBe(false); + expect(initialContext.showTrustConsentModal).toBe(false); + }); + + it('should initialize with empty object contexts', () => { + const initialContext = openID4VPModel.initialContext; + + expect(initialContext.authenticationResponse).toEqual({}); + expect(initialContext.vcsMatchingAuthRequest).toEqual({}); + expect(initialContext.selectedVCs).toEqual({}); + expect(initialContext.selectedDisclosuresByVc).toEqual({}); + expect(initialContext.miniViewSelectedVC).toEqual({}); + }); + + it('should initialize with empty array contexts', () => { + const initialContext = openID4VPModel.initialContext; + + expect(initialContext.trustedVerifiers).toEqual([]); + }); + }); + + describe('Events', () => { + describe('AUTHENTICATE', () => { + it('should create AUTHENTICATE event with all parameters', () => { + const encodedAuthRequest = 'encoded_request_123'; + const flowType = 'OpenID4VP'; + const selectedVC = {id: 'vc123'}; + const isOVPViaDeepLink = true; + + const event = openID4VPModel.events.AUTHENTICATE( + encodedAuthRequest, + flowType, + selectedVC, + isOVPViaDeepLink, + ); + + expect(event.encodedAuthRequest).toBe(encodedAuthRequest); + expect(event.flowType).toBe(flowType); + expect(event.selectedVC).toEqual(selectedVC); + expect(event.isOVPViaDeepLink).toBe(isOVPViaDeepLink); + }); + + it('should create AUTHENTICATE event with false deeplink flag', () => { + const event = openID4VPModel.events.AUTHENTICATE( + 'request', + 'flow', + {}, + false, + ); + + expect(event.isOVPViaDeepLink).toBe(false); + }); + }); + + describe('DOWNLOADED_VCS', () => { + it('should create DOWNLOADED_VCS event with VCs array', () => { + const vcs: any[] = [ + {id: 'vc1', credential: {}}, + {id: 'vc2', credential: {}}, + ]; + + const event = openID4VPModel.events.DOWNLOADED_VCS(vcs); + + expect(event.vcs).toEqual(vcs); + expect(event.vcs.length).toBe(2); + }); + + it('should create DOWNLOADED_VCS event with empty array', () => { + const event = openID4VPModel.events.DOWNLOADED_VCS([]); + + expect(event.vcs).toEqual([]); + }); + }); + + describe('SELECT_VC', () => { + it('should create SELECT_VC event with vcKey and inputDescriptorId', () => { + const vcKey = 'vc_key_123'; + const inputDescriptorId = 'descriptor_456'; + + const event = openID4VPModel.events.SELECT_VC(vcKey, inputDescriptorId); + + expect(event.vcKey).toBe(vcKey); + expect(event.inputDescriptorId).toBe(inputDescriptorId); + }); + + it('should create SELECT_VC event with null inputDescriptorId', () => { + const event = openID4VPModel.events.SELECT_VC('key', null); + + expect(event.vcKey).toBe('key'); + expect(event.inputDescriptorId).toBeNull(); + }); + }); + + describe('ACCEPT_REQUEST', () => { + it('should create ACCEPT_REQUEST event with selectedVCs and disclosures', () => { + const selectedVCs = { + descriptor1: [{id: 'vc1'}] as any[], + descriptor2: [{id: 'vc2'}] as any[], + }; + const selectedDisclosuresByVc = { + vc1: ['claim1', 'claim2'], + vc2: ['claim3'], + }; + + const event = openID4VPModel.events.ACCEPT_REQUEST( + selectedVCs, + selectedDisclosuresByVc, + ); + + expect(event.selectedVCs).toEqual(selectedVCs); + expect(event.selectedDisclosuresByVc).toEqual(selectedDisclosuresByVc); + }); + + it('should create ACCEPT_REQUEST event with empty objects', () => { + const event = openID4VPModel.events.ACCEPT_REQUEST({}, {}); + + expect(event.selectedVCs).toEqual({}); + expect(event.selectedDisclosuresByVc).toEqual({}); + }); + }); + + describe('VERIFIER_TRUST_CONSENT_GIVEN', () => { + it('should create VERIFIER_TRUST_CONSENT_GIVEN event', () => { + const event = openID4VPModel.events.VERIFIER_TRUST_CONSENT_GIVEN(); + + expect(event.type).toBe('VERIFIER_TRUST_CONSENT_GIVEN'); + }); + }); + + describe('VERIFY_AND_ACCEPT_REQUEST', () => { + it('should create VERIFY_AND_ACCEPT_REQUEST event with selectedVCs and disclosures', () => { + const selectedVCs = {descriptor1: [{id: 'vc1'}] as any[]}; + const selectedDisclosuresByVc = {vc1: ['claim1']}; + + const event = openID4VPModel.events.VERIFY_AND_ACCEPT_REQUEST( + selectedVCs, + selectedDisclosuresByVc, + ); + + expect(event.selectedVCs).toEqual(selectedVCs); + expect(event.selectedDisclosuresByVc).toEqual(selectedDisclosuresByVc); + }); + }); + + describe('CONFIRM', () => { + it('should create CONFIRM event', () => { + const event = openID4VPModel.events.CONFIRM(); + + expect(event.type).toBe('CONFIRM'); + }); + }); + + describe('CANCEL', () => { + it('should create CANCEL event', () => { + const event = openID4VPModel.events.CANCEL(); + + expect(event.type).toBe('CANCEL'); + }); + }); + + describe('FACE_VERIFICATION_CONSENT', () => { + it('should create FACE_VERIFICATION_CONSENT event with checked flag true', () => { + const event = openID4VPModel.events.FACE_VERIFICATION_CONSENT(true); + + expect(event.isDoNotAskAgainChecked).toBe(true); + }); + + it('should create FACE_VERIFICATION_CONSENT event with checked flag false', () => { + const event = openID4VPModel.events.FACE_VERIFICATION_CONSENT(false); + + expect(event.isDoNotAskAgainChecked).toBe(false); + }); + }); + + describe('FACE_VALID', () => { + it('should create FACE_VALID event', () => { + const event = openID4VPModel.events.FACE_VALID(); + + expect(event.type).toBe('FACE_VALID'); + }); + }); + + describe('FACE_INVALID', () => { + it('should create FACE_INVALID event', () => { + const event = openID4VPModel.events.FACE_INVALID(); + + expect(event.type).toBe('FACE_INVALID'); + }); + }); + + describe('DISMISS', () => { + it('should create DISMISS event', () => { + const event = openID4VPModel.events.DISMISS(); + + expect(event.type).toBe('DISMISS'); + }); + }); + + describe('DISMISS_POPUP', () => { + it('should create DISMISS_POPUP event', () => { + const event = openID4VPModel.events.DISMISS_POPUP(); + + expect(event.type).toBe('DISMISS_POPUP'); + }); + }); + + describe('RETRY_VERIFICATION', () => { + it('should create RETRY_VERIFICATION event', () => { + const event = openID4VPModel.events.RETRY_VERIFICATION(); + + expect(event.type).toBe('RETRY_VERIFICATION'); + }); + }); + + describe('STORE_RESPONSE', () => { + it('should create STORE_RESPONSE event with response object', () => { + const response = { + status: 'success', + data: {id: '123'}, + }; + + const event = openID4VPModel.events.STORE_RESPONSE(response); + + expect(event.response).toEqual(response); + }); + + it('should create STORE_RESPONSE event with null response', () => { + const event = openID4VPModel.events.STORE_RESPONSE(null); + + expect(event.response).toBeNull(); + }); + }); + + describe('GO_BACK', () => { + it('should create GO_BACK event', () => { + const event = openID4VPModel.events.GO_BACK(); + + expect(event.type).toBe('GO_BACK'); + }); + }); + + describe('CHECK_SELECTED_VC', () => { + it('should create CHECK_SELECTED_VC event', () => { + const event = openID4VPModel.events.CHECK_SELECTED_VC(); + + expect(event.type).toBe('CHECK_SELECTED_VC'); + }); + }); + + describe('SET_SELECTED_VC', () => { + it('should create SET_SELECTED_VC event', () => { + const event = openID4VPModel.events.SET_SELECTED_VC(); + + expect(event.type).toBe('SET_SELECTED_VC'); + }); + }); + + describe('CHECK_FOR_IMAGE', () => { + it('should create CHECK_FOR_IMAGE event', () => { + const event = openID4VPModel.events.CHECK_FOR_IMAGE(); + + expect(event.type).toBe('CHECK_FOR_IMAGE'); + }); + }); + + describe('RETRY', () => { + it('should create RETRY event', () => { + const event = openID4VPModel.events.RETRY(); + + expect(event.type).toBe('RETRY'); + }); + }); + + describe('RESET_RETRY_COUNT', () => { + it('should create RESET_RETRY_COUNT event', () => { + const event = openID4VPModel.events.RESET_RETRY_COUNT(); + + expect(event.type).toBe('RESET_RETRY_COUNT'); + }); + }); + + describe('RESET_ERROR', () => { + it('should create RESET_ERROR event', () => { + const event = openID4VPModel.events.RESET_ERROR(); + + expect(event.type).toBe('RESET_ERROR'); + }); + }); + + describe('CLOSE_BANNER', () => { + it('should create CLOSE_BANNER event', () => { + const event = openID4VPModel.events.CLOSE_BANNER(); + + expect(event.type).toBe('CLOSE_BANNER'); + }); + }); + + describe('LOG_ACTIVITY', () => { + it('should create LOG_ACTIVITY event with SHARED_SUCCESSFULLY logType', () => { + const event = openID4VPModel.events.LOG_ACTIVITY('SHARED_SUCCESSFULLY'); + + expect(event.logType).toBe('SHARED_SUCCESSFULLY'); + }); + + it('should create LOG_ACTIVITY event with USER_DECLINED_CONSENT logType', () => { + const event = openID4VPModel.events.LOG_ACTIVITY( + 'USER_DECLINED_CONSENT', + ); + + expect(event.logType).toBe('USER_DECLINED_CONSENT'); + }); + + it('should create LOG_ACTIVITY event with TECHNICAL_ERROR logType', () => { + const event = openID4VPModel.events.LOG_ACTIVITY('TECHNICAL_ERROR'); + + expect(event.logType).toBe('TECHNICAL_ERROR'); + }); + + it('should create LOG_ACTIVITY event with empty string logType', () => { + const event = openID4VPModel.events.LOG_ACTIVITY(''); + + expect(event.logType).toBe(''); + }); + }); + }); +}); diff --git a/machines/openID4VP/openID4VPSelectors.test.ts b/machines/openID4VP/openID4VPSelectors.test.ts new file mode 100644 index 00000000..aad10ad3 --- /dev/null +++ b/machines/openID4VP/openID4VPSelectors.test.ts @@ -0,0 +1,637 @@ +import { + selectIsGetVCsSatisfyingAuthRequest, + selectVCsMatchingAuthRequest, + selectSelectedVCs, + selectAreAllVCsChecked, + selectIsGetVPSharingConsent, + selectIsFaceVerificationConsent, + selectIsVerifyingIdentity, + selectIsInvalidIdentity, + selectIsSharingVP, + selectIsShowLoadingScreen, + selectCredentials, + selectVerifiableCredentialsData, + selectPurpose, + selectShowConfirmationPopup, + selectIsSelectingVcs, + selectIsError, + selectOpenID4VPRetryCount, + selectIsOVPViaDeeplink, + selectIsFaceVerifiedInVPSharing, + selectVerifierNameInVPSharing, + selectRequestedClaimsByVerifier, + selectshowTrustConsentModal, + selectVerifierNameInTrustModal, + selectVerifierLogoInTrustModal, +} from './openID4VPSelectors'; + +describe('openID4VPSelectors', () => { + describe('selectIsGetVCsSatisfyingAuthRequest', () => { + it('should return true when in getVCsSatisfyingAuthRequest state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectIsGetVCsSatisfyingAuthRequest(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('getVCsSatisfyingAuthRequest'); + }); + }); + + describe('selectVCsMatchingAuthRequest', () => { + it('should return vcsMatchingAuthRequest from context', () => { + const mockVCs = [{id: 'vc1'}, {id: 'vc2'}]; + const state = { + context: { + vcsMatchingAuthRequest: mockVCs, + }, + } as any; + + expect(selectVCsMatchingAuthRequest(state)).toEqual(mockVCs); + }); + }); + + describe('selectSelectedVCs', () => { + it('should return selectedVCs from context', () => { + const mockSelectedVCs = {vc1: {data: 'test'}}; + const state = { + context: { + selectedVCs: mockSelectedVCs, + }, + } as any; + + expect(selectSelectedVCs(state)).toEqual(mockSelectedVCs); + }); + }); + + describe('selectAreAllVCsChecked', () => { + it('should return checkedAll from context', () => { + const state = { + context: { + checkedAll: true, + }, + } as any; + + expect(selectAreAllVCsChecked(state)).toBe(true); + }); + }); + + describe('selectIsGetVPSharingConsent', () => { + it('should return true when in getConsentForVPSharing state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectIsGetVPSharingConsent(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('getConsentForVPSharing'); + }); + }); + + describe('selectIsFaceVerificationConsent', () => { + it('should return true when in faceVerificationConsent state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectIsFaceVerificationConsent(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('faceVerificationConsent'); + }); + }); + + describe('selectIsVerifyingIdentity', () => { + it('should return true when in verifyingIdentity state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectIsVerifyingIdentity(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('verifyingIdentity'); + }); + }); + + describe('selectIsInvalidIdentity', () => { + it('should return true when in invalidIdentity state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectIsInvalidIdentity(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('invalidIdentity'); + }); + }); + + describe('selectIsSharingVP', () => { + it('should return true when in sendingVP state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectIsSharingVP(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('sendingVP'); + }); + }); + + describe('selectIsShowLoadingScreen', () => { + it('should return showLoadingScreen from context', () => { + const state = { + context: { + showLoadingScreen: true, + }, + } as any; + + expect(selectIsShowLoadingScreen(state)).toBe(true); + }); + }); + + describe('selectCredentials', () => { + it('should return empty array when selectedVCs is empty', () => { + const state = { + context: { + selectedVCs: {}, + }, + } as any; + + expect(selectCredentials(state)).toEqual([]); + }); + + it('should process and return credentials from selectedVCs', () => { + const mockCredential = {data: 'test'}; + const state = { + context: { + selectedVCs: { + key1: { + inner1: [ + { + verifiableCredential: { + credential: mockCredential, + }, + }, + ], + }, + }, + }, + } as any; + + const result = selectCredentials(state); + expect(result).toEqual([mockCredential]); + }); + + it('should handle credentials without nested credential property', () => { + const mockCredential = {data: 'test'}; + const state = { + context: { + selectedVCs: { + key1: { + inner1: [ + { + verifiableCredential: mockCredential, + }, + ], + }, + }, + }, + } as any; + + const result = selectCredentials(state); + expect(result).toEqual([mockCredential]); + }); + }); + + describe('selectVerifiableCredentialsData', () => { + it('should return empty array when no selectedVCs', () => { + const state = { + context: { + selectedVCs: {}, + }, + } as any; + + expect(selectVerifiableCredentialsData(state)).toEqual([]); + }); + + it('should process and return verifiable credentials data', () => { + const state = { + context: { + selectedVCs: { + key1: { + inner1: [ + { + vcMetadata: { + issuer: 'TestIssuer', + }, + verifiableCredential: { + issuerLogo: 'logo.png', + wellKnown: 'wellknown', + credentialTypes: ['type1'], + credential: { + credentialSubject: { + face: 'faceData', + }, + }, + }, + }, + ], + }, + }, + }, + } as any; + + const result = selectVerifiableCredentialsData(state); + expect(result).toHaveLength(1); + expect(result[0].issuer).toBe('TestIssuer'); + expect(result[0].face).toBe('faceData'); + }); + }); + + describe('selectPurpose', () => { + it('should return purpose from context', () => { + const state = { + context: { + purpose: 'verification', + }, + } as any; + + expect(selectPurpose(state)).toBe('verification'); + }); + }); + + describe('selectShowConfirmationPopup', () => { + it('should return true when in showConfirmationPopup state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectShowConfirmationPopup(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('showConfirmationPopup'); + }); + }); + + describe('selectIsSelectingVcs', () => { + it('should return true when in selectingVCs state', () => { + const state = { + matches: jest.fn(() => true), + context: {}, + } as any; + + expect(selectIsSelectingVcs(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('selectingVCs'); + }); + }); + + describe('selectIsError', () => { + it('should return error from context', () => { + const mockError = new Error('Test error'); + const state = { + context: { + error: mockError, + }, + } as any; + + expect(selectIsError(state)).toBe(mockError); + }); + }); + + describe('selectOpenID4VPRetryCount', () => { + it('should return openID4VPRetryCount from context', () => { + const state = { + context: { + openID4VPRetryCount: 3, + }, + } as any; + + expect(selectOpenID4VPRetryCount(state)).toBe(3); + }); + }); + + describe('selectIsOVPViaDeeplink', () => { + it('should return isOVPViaDeepLink from context', () => { + const state = { + context: { + isOVPViaDeepLink: true, + }, + } as any; + + expect(selectIsOVPViaDeeplink(state)).toBe(true); + }); + }); + + describe('selectIsFaceVerifiedInVPSharing', () => { + it('should return true when in sendingVP state and banner shown', () => { + const state = { + matches: jest.fn(() => true), + context: { + showFaceCaptureSuccessBanner: true, + }, + } as any; + + expect(selectIsFaceVerifiedInVPSharing(state)).toBe(true); + expect(state.matches).toHaveBeenCalledWith('sendingVP'); + }); + + it('should return false when not in sendingVP state', () => { + const state = { + matches: jest.fn(() => false), + context: { + showFaceCaptureSuccessBanner: true, + }, + } as any; + + expect(selectIsFaceVerifiedInVPSharing(state)).toBe(false); + }); + }); + + describe('selectVerifierNameInVPSharing', () => { + it('should return client_name from client_metadata', () => { + const state = { + context: { + authenticationResponse: { + client_metadata: { + client_name: 'TestVerifier', + }, + }, + }, + } as any; + + expect(selectVerifierNameInVPSharing(state)).toBe('TestVerifier'); + }); + + it('should return client_id when client_name not available', () => { + const state = { + context: { + authenticationResponse: { + client_id: 'verifier123', + }, + }, + } as any; + + expect(selectVerifierNameInVPSharing(state)).toBe('verifier123'); + }); + }); + + describe('selectRequestedClaimsByVerifier', () => { + it('should return requestedClaims from context', () => { + const mockClaims = ['name', 'age']; + const state = { + context: { + requestedClaims: mockClaims, + }, + } as any; + + expect(selectRequestedClaimsByVerifier(state)).toEqual(mockClaims); + }); + }); + + describe('selectshowTrustConsentModal', () => { + it('should return showTrustConsentModal from context', () => { + const state = { + context: { + showTrustConsentModal: true, + }, + } as any; + + expect(selectshowTrustConsentModal(state)).toBe(true); + }); + }); + + describe('selectVerifierNameInTrustModal', () => { + it('should return client_name from client_metadata', () => { + const state = { + context: { + authenticationResponse: { + client_metadata: { + client_name: 'TrustedVerifier', + }, + }, + }, + } as any; + + expect(selectVerifierNameInTrustModal(state)).toBe('TrustedVerifier'); + }); + }); + + describe('selectVerifierLogoInTrustModal', () => { + it('should return logo_uri from client_metadata', () => { + const state = { + context: { + authenticationResponse: { + client_metadata: { + logo_uri: 'https://example.com/logo.png', + }, + }, + }, + } as any; + + expect(selectVerifierLogoInTrustModal(state)).toBe( + 'https://example.com/logo.png', + ); + }); + }); + + describe('selectCredentials', () => { + it('should return array of credentials from selectedVCs', () => { + const mockState: any = { + context: { + selectedVCs: { + type1: { + vc1: { + verifiableCredential: { + credential: {id: 'cred-1', name: 'John'}, + }, + }, + }, + type2: { + vc2: { + verifiableCredential: {id: 'cred-2', name: 'Jane'}, + }, + }, + }, + }, + }; + + const result: any = selectCredentials(mockState); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + }); + + it('should handle verifiableCredential without credential property', () => { + const mockState: any = { + context: { + selectedVCs: { + type1: { + vc1: { + verifiableCredential: {id: 'direct-vc', name: 'Direct'}, + }, + }, + }, + }, + }; + + const result: any = selectCredentials(mockState); + expect(result.length).toBe(1); + }); + + it('should return undefined when no credentials selected', () => { + const mockState: any = { + context: { + selectedVCs: {}, + }, + }; + + const result: any = selectCredentials(mockState); + expect(Array.isArray(result) ? result.length : result).toBe(0); + }); + + it('should flatten nested credential structures', () => { + const mockState: any = { + context: { + selectedVCs: { + type1: { + vc1: {verifiableCredential: {credential: {id: '1'}}}, + vc2: {verifiableCredential: {credential: {id: '2'}}}, + }, + type2: { + vc3: {verifiableCredential: {credential: {id: '3'}}}, + }, + }, + }, + }; + + const result: any = selectCredentials(mockState); + expect(result.length).toBe(3); + }); + }); + + describe('selectVerifiableCredentialsData', () => { + it('should return array of formatted verifiable credential data', () => { + const mockState: any = { + context: { + selectedVCs: { + type1: { + vc1: { + vcMetadata: { + id: 'vc-001', + issuer: 'Test Issuer', + }, + verifiableCredential: { + issuerLogo: 'https://example.com/logo.png', + wellKnown: 'https://example.com/.well-known', + credentialTypes: ['VerifiableCredential', 'NationalID'], + credential: { + credentialSubject: { + face: 'base64-image-data', + }, + }, + }, + }, + }, + }, + }, + }; + + const result = selectVerifiableCredentialsData(mockState); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0].issuer).toBe('Test Issuer'); + expect(result[0].issuerLogo).toBe('https://example.com/logo.png'); + expect(result[0].wellKnown).toBe('https://example.com/.well-known'); + }); + + it('should use getMosipLogo when issuerLogo is not available', () => { + const mockState: any = { + context: { + selectedVCs: { + type1: { + vc1: { + vcMetadata: { + id: 'vc-002', + issuer: 'Mosip', + }, + verifiableCredential: { + credential: { + credentialSubject: {}, + }, + }, + }, + }, + }, + }, + }; + + const result = selectVerifiableCredentialsData(mockState); + expect(result[0].issuerLogo).toBeDefined(); + }); + + it('should handle face from credential.biometrics.face', () => { + const mockState: any = { + context: { + selectedVCs: { + type1: { + vc1: { + vcMetadata: { + id: 'vc-003', + issuer: 'Issuer', + }, + credential: { + biometrics: { + face: 'biometric-face-data', + }, + }, + verifiableCredential: {}, + }, + }, + }, + }, + }; + + const result = selectVerifiableCredentialsData(mockState); + expect(result[0].face).toBe('biometric-face-data'); + }); + + it('should handle empty selectedVCs', () => { + const mockState: any = { + context: { + selectedVCs: {}, + }, + }; + + const result = selectVerifiableCredentialsData(mockState); + expect(result).toEqual([]); + }); + + it('should handle multiple VCs from different types', () => { + const mockState: any = { + context: { + selectedVCs: { + type1: { + vc1: { + vcMetadata: {id: '1', issuer: 'Issuer1'}, + verifiableCredential: {}, + }, + }, + type2: { + vc2: { + vcMetadata: {id: '2', issuer: 'Issuer2'}, + verifiableCredential: {}, + }, + vc3: { + vcMetadata: {id: '3', issuer: 'Issuer3'}, + verifiableCredential: {}, + }, + }, + }, + }, + }; + + const result = selectVerifiableCredentialsData(mockState); + expect(result.length).toBe(3); + expect(result[0].issuer).toBe('Issuer1'); + expect(result[1].issuer).toBe('Issuer2'); + expect(result[2].issuer).toBe('Issuer3'); + }); + }); +}); diff --git a/routes/routesConstants.test.ts b/routes/routesConstants.test.ts new file mode 100644 index 00000000..9dbf9348 --- /dev/null +++ b/routes/routesConstants.test.ts @@ -0,0 +1,139 @@ +import { + BOTTOM_TAB_ROUTES, + SCAN_ROUTES, + REQUEST_ROUTES, + SETTINGS_ROUTES, + AUTH_ROUTES, +} from './routesConstants'; + +describe('routesConstants', () => { + describe('BOTTOM_TAB_ROUTES', () => { + it('should be defined', () => { + expect(BOTTOM_TAB_ROUTES).toBeDefined(); + }); + + it('should have home route', () => { + expect(BOTTOM_TAB_ROUTES.home).toBe('home'); + }); + + it('should have share route', () => { + expect(BOTTOM_TAB_ROUTES.share).toBe('share'); + }); + + it('should have history route', () => { + expect(BOTTOM_TAB_ROUTES.history).toBe('history'); + }); + + it('should have settings route', () => { + expect(BOTTOM_TAB_ROUTES.settings).toBe('settings'); + }); + + it('should have exactly 4 routes', () => { + expect(Object.keys(BOTTOM_TAB_ROUTES)).toHaveLength(4); + }); + }); + + describe('SCAN_ROUTES', () => { + it('should be defined', () => { + expect(SCAN_ROUTES).toBeDefined(); + }); + + it('should have ScanScreen route', () => { + expect(SCAN_ROUTES.ScanScreen).toBe('ScanScreen'); + }); + + it('should have SendVcScreen route', () => { + expect(SCAN_ROUTES.SendVcScreen).toBe('SendVcScreen'); + }); + + it('should have SendVPScreen route', () => { + expect(SCAN_ROUTES.SendVPScreen).toBe('SendVPScreen'); + }); + + it('should have exactly 3 routes', () => { + expect(Object.keys(SCAN_ROUTES)).toHaveLength(3); + }); + }); + + describe('REQUEST_ROUTES', () => { + it('should be defined', () => { + expect(REQUEST_ROUTES).toBeDefined(); + }); + + it('should have Request route', () => { + expect(REQUEST_ROUTES.Request).toBe('Request'); + }); + + it('should have RequestScreen route', () => { + expect(REQUEST_ROUTES.RequestScreen).toBe('RequestScreen'); + }); + + it('should have ReceiveVcScreen route', () => { + expect(REQUEST_ROUTES.ReceiveVcScreen).toBe('ReceiveVcScreen'); + }); + + it('should have exactly 3 routes', () => { + expect(Object.keys(REQUEST_ROUTES)).toHaveLength(3); + }); + }); + + describe('SETTINGS_ROUTES', () => { + it('should be defined', () => { + expect(SETTINGS_ROUTES).toBeDefined(); + }); + + it('should have KeyManagement route', () => { + expect(SETTINGS_ROUTES.KeyManagement).toBe('KeyManagement'); + }); + + it('should have exactly 1 route', () => { + expect(Object.keys(SETTINGS_ROUTES)).toHaveLength(1); + }); + }); + + describe('AUTH_ROUTES', () => { + it('should be defined', () => { + expect(AUTH_ROUTES).toBeDefined(); + }); + + it('should have AuthView route', () => { + expect(AUTH_ROUTES.AuthView).toBe('AuthView'); + }); + + it('should have exactly 1 route', () => { + expect(Object.keys(AUTH_ROUTES)).toHaveLength(1); + }); + }); + + describe('Route constants structure', () => { + it('all BOTTOM_TAB_ROUTES values should be strings', () => { + Object.values(BOTTOM_TAB_ROUTES).forEach(route => { + expect(typeof route).toBe('string'); + }); + }); + + it('all SCAN_ROUTES values should be strings', () => { + Object.values(SCAN_ROUTES).forEach(route => { + expect(typeof route).toBe('string'); + }); + }); + + it('all REQUEST_ROUTES values should be strings', () => { + Object.values(REQUEST_ROUTES).forEach(route => { + expect(typeof route).toBe('string'); + }); + }); + + it('all SETTINGS_ROUTES values should be strings', () => { + Object.values(SETTINGS_ROUTES).forEach(route => { + expect(typeof route).toBe('string'); + }); + }); + + it('all AUTH_ROUTES values should be strings', () => { + Object.values(AUTH_ROUTES).forEach(route => { + expect(typeof route).toBe('string'); + }); + }); + }); +}); diff --git a/shared/Utils.test.ts b/shared/Utils.test.ts new file mode 100644 index 00000000..fb29ffa8 --- /dev/null +++ b/shared/Utils.test.ts @@ -0,0 +1,405 @@ +import { + getVCsOrderedByPinStatus, + VCShareFlowType, + VCItemContainerFlowType, + CameraPosition, + isMosipVC, + parseJSON, + isNetworkError, + UUID, + formatTextWithGivenLimit, + DEEPLINK_FLOWS, + base64ToByteArray, + createCacheObject, + isCacheExpired, + getVerifierKey, +} from './Utils'; +import {VCMetadata} from './VCMetadata'; + +describe('getVCsOrderedByPinStatus', () => { + it('should be defined', () => { + expect(getVCsOrderedByPinStatus).toBeDefined(); + }); + + it('should return pinned VCs first', () => { + const vcMetadatas = [ + new VCMetadata({id: '1', isPinned: false}), + new VCMetadata({id: '2', isPinned: true}), + new VCMetadata({id: '3', isPinned: false}), + new VCMetadata({id: '4', isPinned: true}), + ]; + + const result = getVCsOrderedByPinStatus(vcMetadatas); + + expect(result[0].isPinned).toBe(true); + expect(result[1].isPinned).toBe(true); + expect(result[2].isPinned).toBe(false); + expect(result[3].isPinned).toBe(false); + }); + + it('should handle empty array', () => { + const result = getVCsOrderedByPinStatus([]); + expect(result).toEqual([]); + }); + + it('should handle all pinned VCs', () => { + const vcMetadatas = [ + new VCMetadata({id: '1', isPinned: true}), + new VCMetadata({id: '2', isPinned: true}), + ]; + + const result = getVCsOrderedByPinStatus(vcMetadatas); + expect(result.every(vc => vc.isPinned)).toBe(true); + }); + + it('should handle all unpinned VCs', () => { + const vcMetadatas = [ + new VCMetadata({id: '1', isPinned: false}), + new VCMetadata({id: '2', isPinned: false}), + ]; + + const result = getVCsOrderedByPinStatus(vcMetadatas); + expect(result.every(vc => !vc.isPinned)).toBe(true); + }); +}); + +describe('VCShareFlowType enum', () => { + it('should have SIMPLE_SHARE defined', () => { + expect(VCShareFlowType.SIMPLE_SHARE).toBe('simple share'); + }); + + it('should have MINI_VIEW_SHARE defined', () => { + expect(VCShareFlowType.MINI_VIEW_SHARE).toBe('mini view share'); + }); + + it('should have MINI_VIEW_SHARE_WITH_SELFIE defined', () => { + expect(VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE).toBe( + 'mini view share with selfie', + ); + }); + + it('should have MINI_VIEW_QR_LOGIN defined', () => { + expect(VCShareFlowType.MINI_VIEW_QR_LOGIN).toBe('mini view qr login'); + }); + + it('should have OPENID4VP defined', () => { + expect(VCShareFlowType.OPENID4VP).toBe('OpenID4VP'); + }); + + it('should have MINI_VIEW_SHARE_OPENID4VP defined', () => { + expect(VCShareFlowType.MINI_VIEW_SHARE_OPENID4VP).toBe( + 'OpenID4VP share from mini view', + ); + }); + + it('should have MINI_VIEW_SHARE_WITH_SELFIE_OPENID4VP defined', () => { + expect(VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE_OPENID4VP).toBe( + 'OpenID4VP share with selfie from mini view', + ); + }); +}); + +describe('VCItemContainerFlowType enum', () => { + it('should have QR_LOGIN defined', () => { + expect(VCItemContainerFlowType.QR_LOGIN).toBe('qr login'); + }); + + it('should have VC_SHARE defined', () => { + expect(VCItemContainerFlowType.VC_SHARE).toBe('vc share'); + }); + + it('should have VP_SHARE defined', () => { + expect(VCItemContainerFlowType.VP_SHARE).toBe('vp share'); + }); +}); + +describe('CameraPosition enum', () => { + it('should have FRONT defined', () => { + expect(CameraPosition.FRONT).toBe('front'); + }); + + it('should have BACK defined', () => { + expect(CameraPosition.BACK).toBe('back'); + }); +}); + +describe('isMosipVC', () => { + it('should be defined', () => { + expect(isMosipVC).toBeDefined(); + }); + + it('should return true for Mosip issuer', () => { + const result = isMosipVC('Mosip'); + expect(result).toBe(true); + }); + + it('should return true for MosipOtp issuer', () => { + const result = isMosipVC('MosipOtp'); + expect(result).toBe(true); + }); + + it('should return false for other issuers', () => { + expect(isMosipVC('SomeOtherIssuer')).toBe(false); + expect(isMosipVC('')).toBe(false); + expect(isMosipVC('mosip')).toBe(false); + }); +}); + +describe('parseJSON', () => { + it('should be defined', () => { + expect(parseJSON).toBeDefined(); + }); + + it('should parse valid JSON string', () => { + const jsonStr = '{"key": "value"}'; + const result = parseJSON(jsonStr); + expect(result).toEqual({key: 'value'}); + }); + + it('should handle object input', () => { + const obj = {key: 'value'}; + const result = parseJSON(obj); + expect(result).toEqual({key: 'value'}); + }); + + it('should handle invalid JSON gracefully', () => { + const invalidJson = '{invalid json}'; + const result = parseJSON(invalidJson); + expect(result).toBeDefined(); + }); + + it('should handle nested objects', () => { + const jsonStr = '{"key": {"nested": "value"}}'; + const result = parseJSON(jsonStr); + expect(result.key.nested).toBe('value'); + }); +}); + +describe('isNetworkError', () => { + it('should be defined', () => { + expect(isNetworkError).toBeDefined(); + }); + + it('should return true for network request failed error', () => { + const error = 'Network request failed'; + expect(isNetworkError(error)).toBe(true); + }); + + it('should return false for other errors', () => { + expect(isNetworkError('Some other error')).toBe(false); + expect(isNetworkError('')).toBe(false); + }); + + it('should handle partial matches', () => { + const error = 'Error: Network request failed - timeout'; + expect(isNetworkError(error)).toBe(true); + }); +}); + +describe('UUID', () => { + it('should be defined', () => { + expect(UUID).toBeDefined(); + }); + + it('should generate a valid UUID', () => { + const uuid = UUID.generate(); + expect(uuid).toBeDefined(); + expect(typeof uuid).toBe('string'); + expect(uuid.length).toBeGreaterThan(0); + }); + + it('should generate unique UUIDs', () => { + const uuid1 = UUID.generate(); + const uuid2 = UUID.generate(); + expect(uuid1).not.toBe(uuid2); + }); + + it('should match UUID format', () => { + const uuid = UUID.generate(); + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + expect(uuidRegex.test(uuid)).toBe(true); + }); +}); + +describe('formatTextWithGivenLimit', () => { + it('should be defined', () => { + expect(formatTextWithGivenLimit).toBeDefined(); + }); + + it('should truncate text longer than limit', () => { + const text = 'This is a very long text'; + const result = formatTextWithGivenLimit(text, 10); + expect(result).toBe('This is a ...'); + }); + + it('should return text as is if shorter than limit', () => { + const text = 'Short text'; + const result = formatTextWithGivenLimit(text, 15); + expect(result).toBe('Short text'); + }); + + it('should use default limit of 15 if not provided', () => { + const text = 'This is a longer text than 15 characters'; + const result = formatTextWithGivenLimit(text); + expect(result).toBe('This is a longe...'); + }); + + it('should handle empty string', () => { + const result = formatTextWithGivenLimit('', 10); + expect(result).toBe(''); + }); + + it('should handle exact limit length', () => { + const text = 'Exactly 10'; + const result = formatTextWithGivenLimit(text, 10); + expect(result).toBe('Exactly 10'); + }); +}); + +describe('DEEPLINK_FLOWS enum', () => { + it('should have QR_LOGIN defined', () => { + expect(DEEPLINK_FLOWS.QR_LOGIN).toBe('qrLoginFlow'); + }); + + it('should have OVP defined', () => { + expect(DEEPLINK_FLOWS.OVP).toBe('ovpFlow'); + }); +}); + +describe('base64ToByteArray', () => { + it('should be defined', () => { + expect(base64ToByteArray).toBeDefined(); + }); + + it('should convert base64 string to byte array', () => { + const base64 = 'SGVsbG8gV29ybGQ='; // "Hello World" + const result = base64ToByteArray(base64); + expect(result).toBeInstanceOf(Uint8Array); + expect(result.length).toBeGreaterThan(0); + }); + + it('should handle base64url encoding', () => { + const base64url = 'SGVsbG8gV29ybGQ'; // without padding + const result = base64ToByteArray(base64url); + expect(result).toBeInstanceOf(Uint8Array); + }); + + it('should throw error for invalid base64', () => { + expect(() => { + base64ToByteArray('!!!invalid base64!!!'); + }).toThrow(); + }); + + it('should handle strings with whitespace', () => { + const base64 = ' SGVsbG8gV29ybGQ= '; + const result = base64ToByteArray(base64); + expect(result).toBeInstanceOf(Uint8Array); + }); + + it('should handle URL-safe base64 characters', () => { + const base64 = 'SGVsbG8tV29ybGQ_'; // with - and _ + const result = base64ToByteArray(base64); + expect(result).toBeInstanceOf(Uint8Array); + }); + + it('should handle empty string', () => { + const result = base64ToByteArray(''); + expect(result).toBeInstanceOf(Uint8Array); + expect(result.length).toBe(0); + }); +}); + +describe('createCacheObject', () => { + it('should create cache object with response and timestamp', () => { + const response = {data: 'test data'}; + const result = createCacheObject(response); + + expect(result).toHaveProperty('response'); + expect(result).toHaveProperty('cachedTime'); + expect(result.response).toBe(response); + expect(typeof result.cachedTime).toBe('number'); + }); + + it('should use current timestamp', () => { + const before = Date.now(); + const result = createCacheObject({}); + const after = Date.now(); + + expect(result.cachedTime).toBeGreaterThanOrEqual(before); + expect(result.cachedTime).toBeLessThanOrEqual(after); + }); + + it('should handle null response', () => { + const result = createCacheObject(null); + expect(result.response).toBeNull(); + expect(result.cachedTime).toBeDefined(); + }); + + it('should handle complex objects', () => { + const complexResponse = { + data: [1, 2, 3], + metadata: {key: 'value'}, + nested: {deep: {value: true}}, + }; + const result = createCacheObject(complexResponse); + expect(result.response).toBe(complexResponse); + }); +}); + +describe('isCacheExpired', () => { + it('should return false for recent timestamp', () => { + const recentTimestamp = Date.now() - 1000; // 1 second ago + expect(isCacheExpired(recentTimestamp)).toBe(false); + }); + + it('should return true for old timestamp', () => { + const oldTimestamp = Date.now() - (60 * 60 * 1000 + 1000); // Over 1 hour ago + expect(isCacheExpired(oldTimestamp)).toBe(true); + }); + + it('should return false for current timestamp', () => { + const currentTimestamp = Date.now(); + expect(isCacheExpired(currentTimestamp)).toBe(false); + }); + + it('should handle edge case at exact TTL boundary', () => { + const boundaryTimestamp = Date.now() - 60 * 60 * 1000; // Exactly 1 hour ago + const result = isCacheExpired(boundaryTimestamp); + expect(typeof result).toBe('boolean'); + }); +}); + +describe('getVerifierKey', () => { + it('should create verifier key with prefix', () => { + const verifier = 'example.com'; + const result = getVerifierKey(verifier); + expect(result).toBe('trusted_verifier_example.com'); + }); + + it('should handle empty string', () => { + const result = getVerifierKey(''); + expect(result).toBe('trusted_verifier_'); + }); + + it('should preserve verifier name exactly', () => { + const verifier = 'TestVerifier123'; + const result = getVerifierKey(verifier); + expect(result).toBe('trusted_verifier_TestVerifier123'); + }); + + it('should handle special characters', () => { + const verifier = 'verifier-with-dashes_and_underscores'; + const result = getVerifierKey(verifier); + expect(result).toBe( + 'trusted_verifier_verifier-with-dashes_and_underscores', + ); + }); +}); + +describe('canonicalize', () => { + it('should be defined', async () => { + const {canonicalize} = await import('./Utils'); + expect(canonicalize).toBeDefined(); + }); +}); diff --git a/shared/VCFormat.test.ts b/shared/VCFormat.test.ts new file mode 100644 index 00000000..416a321e --- /dev/null +++ b/shared/VCFormat.test.ts @@ -0,0 +1,37 @@ +import {VCFormat} from './VCFormat'; + +describe('VCFormat', () => { + it('should have ldp_vc format', () => { + expect(VCFormat.ldp_vc).toBe('ldp_vc'); + }); + + it('should have mso_mdoc format', () => { + expect(VCFormat.mso_mdoc).toBe('mso_mdoc'); + }); + + it('should have vc_sd_jwt format', () => { + expect(VCFormat.vc_sd_jwt).toBe('vc+sd-jwt'); + }); + + it('should have dc_sd_jwt format', () => { + expect(VCFormat.dc_sd_jwt).toBe('dc+sd-jwt'); + }); + + it('should have exactly 4 formats', () => { + const formatCount = Object.keys(VCFormat).length; + expect(formatCount).toBe(4); + }); + + it('should allow access via enum key', () => { + expect(VCFormat['ldp_vc']).toBe('ldp_vc'); + expect(VCFormat['mso_mdoc']).toBe('mso_mdoc'); + expect(VCFormat['vc_sd_jwt']).toBe('vc+sd-jwt'); + expect(VCFormat['dc_sd_jwt']).toBe('dc+sd-jwt'); + }); + + it('should have all unique values', () => { + const values = Object.values(VCFormat); + const uniqueValues = new Set(values); + expect(uniqueValues.size).toBe(values.length); + }); +}); diff --git a/shared/VCMetadata.test.ts b/shared/VCMetadata.test.ts new file mode 100644 index 00000000..654270fd --- /dev/null +++ b/shared/VCMetadata.test.ts @@ -0,0 +1,499 @@ +import {VCMetadata, parseMetadatas, getVCMetadata} from './VCMetadata'; +import {VCFormat} from './VCFormat'; +import {UUID} from './Utils'; + +describe('VCMetadata', () => { + describe('constructor', () => { + it('should create instance with default values', () => { + const metadata = new VCMetadata(); + + expect(metadata.idType).toBe(''); + expect(metadata.requestId).toBe(''); + expect(metadata.isPinned).toBe(false); + expect(metadata.id).toBe(''); + expect(metadata.issuer).toBe(''); + expect(metadata.protocol).toBe(''); + expect(metadata.timestamp).toBe(''); + expect(metadata.isVerified).toBe(false); + expect(metadata.mosipIndividualId).toBe(''); + expect(metadata.format).toBe(''); + expect(metadata.isExpired).toBe(false); + }); + + it('should create instance with provided values', () => { + const metadata = new VCMetadata({ + idType: 'UIN', + requestId: 'req123', + isPinned: true, + id: 'id123', + issuer: 'TestIssuer', + protocol: 'OpenId4VCI', + timestamp: '2024-01-01', + isVerified: true, + mosipIndividualId: 'mosip123', + format: 'ldp_vc', + downloadKeyType: 'ED25519', + isExpired: false, + credentialType: 'NationalID', + issuerHost: 'https://test.com', + }); + + expect(metadata.idType).toBe('UIN'); + expect(metadata.requestId).toBe('req123'); + expect(metadata.isPinned).toBe(true); + expect(metadata.id).toBe('id123'); + expect(metadata.issuer).toBe('TestIssuer'); + expect(metadata.protocol).toBe('OpenId4VCI'); + expect(metadata.timestamp).toBe('2024-01-01'); + expect(metadata.isVerified).toBe(true); + expect(metadata.mosipIndividualId).toBe('mosip123'); + expect(metadata.format).toBe('ldp_vc'); + expect(metadata.downloadKeyType).toBe('ED25519'); + expect(metadata.isExpired).toBe(false); + expect(metadata.credentialType).toBe('NationalID'); + expect(metadata.issuerHost).toBe('https://test.com'); + }); + }); + + describe('fromVC', () => { + it('should create VCMetadata from VC object', () => { + const vc = { + idType: 'VID', + requestId: 'req456', + id: 'vc123', + issuer: 'Issuer1', + format: VCFormat.ldp_vc, + }; + + const metadata = VCMetadata.fromVC(vc); + + expect(metadata.idType).toBe('VID'); + expect(metadata.requestId).toBe('req456'); + expect(metadata.id).toBe('vc123'); + expect(metadata.issuer).toBe('Issuer1'); + expect(metadata.format).toBe(VCFormat.ldp_vc); + }); + + it('should use default format if not provided', () => { + const vc = {id: 'vc123'}; + const metadata = VCMetadata.fromVC(vc); + + expect(metadata.format).toBe(VCFormat.ldp_vc); + }); + + it('should handle isPinned default value', () => { + const vc = {id: 'vc123'}; + const metadata = VCMetadata.fromVC(vc); + + expect(metadata.isPinned).toBe(false); + }); + }); + + describe('fromVcMetadataString', () => { + it('should parse JSON string to VCMetadata', () => { + const jsonStr = JSON.stringify({ + id: 'vc123', + issuer: 'TestIssuer', + format: 'ldp_vc', + }); + + const metadata = VCMetadata.fromVcMetadataString(jsonStr); + + expect(metadata.id).toBe('vc123'); + expect(metadata.issuer).toBe('TestIssuer'); + expect(metadata.format).toBe('ldp_vc'); + }); + + it('should handle object input', () => { + const obj = { + id: 'vc456', + issuer: 'AnotherIssuer', + }; + + const metadata = VCMetadata.fromVcMetadataString(obj); + + expect(metadata.id).toBe('vc456'); + expect(metadata.issuer).toBe('AnotherIssuer'); + }); + + it('should return empty VCMetadata on parse error', () => { + const invalidJson = '{invalid json}'; + const metadata = VCMetadata.fromVcMetadataString(invalidJson); + + expect(metadata).toBeInstanceOf(VCMetadata); + expect(metadata.id).toBe(''); + }); + }); + + describe('isVCKey', () => { + it('should return true for valid VC key', () => { + expect(VCMetadata.isVCKey('VC_1234567890_abc123')).toBe(true); + expect(VCMetadata.isVCKey('VC_timestamp_id')).toBe(true); + }); + + it('should return false for invalid VC key', () => { + expect(VCMetadata.isVCKey('INVALID_KEY')).toBe(false); + expect(VCMetadata.isVCKey('VC')).toBe(false); + expect(VCMetadata.isVCKey('')).toBe(false); + expect(VCMetadata.isVCKey('VC_')).toBe(false); + }); + + it('should handle keys with special characters properly', () => { + expect(VCMetadata.isVCKey('VC_123_abc-def')).toBe(true); + expect(VCMetadata.isVCKey('VC_123_abc_def')).toBe(true); + }); + }); + + describe('isFromOpenId4VCI', () => { + it('should return true when protocol is OpenId4VCI', () => { + const metadata = new VCMetadata({protocol: 'OpenId4VCI'}); + expect(metadata.isFromOpenId4VCI()).toBe(true); + }); + + it('should return false when protocol is not OpenId4VCI', () => { + const metadata = new VCMetadata({protocol: 'OtherProtocol'}); + expect(metadata.isFromOpenId4VCI()).toBe(false); + }); + + it('should return false when protocol is empty', () => { + const metadata = new VCMetadata(); + expect(metadata.isFromOpenId4VCI()).toBe(false); + }); + }); + + describe('getVcKey', () => { + it('should generate VC key with timestamp', () => { + const metadata = new VCMetadata({ + timestamp: '1234567890', + id: 'abc123', + }); + + expect(metadata.getVcKey()).toBe('VC_1234567890_abc123'); + }); + + it('should generate VC key without timestamp', () => { + const metadata = new VCMetadata({ + timestamp: '', + id: 'xyz789', + }); + + expect(metadata.getVcKey()).toBe('VC_xyz789'); + }); + + it('should match the VC key regex pattern', () => { + const metadata = new VCMetadata({ + timestamp: '1234567890', + id: 'test-id_123', + }); + + const key = metadata.getVcKey(); + expect(VCMetadata.isVCKey(key)).toBe(true); + }); + }); + + describe('equals', () => { + it('should return true for equal VCMetadata instances', () => { + const metadata1 = new VCMetadata({ + timestamp: '1234567890', + id: 'abc123', + }); + const metadata2 = new VCMetadata({ + timestamp: '1234567890', + id: 'abc123', + }); + + expect(metadata1.equals(metadata2)).toBe(true); + }); + + it('should return false for different VCMetadata instances', () => { + const metadata1 = new VCMetadata({ + timestamp: '1234567890', + id: 'abc123', + }); + const metadata2 = new VCMetadata({ + timestamp: '0987654321', + id: 'xyz789', + }); + + expect(metadata1.equals(metadata2)).toBe(false); + }); + + it('should return true when comparing instance with itself', () => { + const metadata = new VCMetadata({ + timestamp: '1234567890', + id: 'abc123', + }); + + expect(metadata.equals(metadata)).toBe(true); + }); + }); + + describe('vcKeyRegExp', () => { + it('should be defined as a RegExp', () => { + expect(VCMetadata.vcKeyRegExp).toBeInstanceOf(RegExp); + }); + }); +}); + +describe('parseMetadatas', () => { + it('should be defined', () => { + expect(parseMetadatas).toBeDefined(); + }); + + it('should parse array of metadata objects', () => { + const metadataObjects = [ + {id: 'vc1', issuer: 'Issuer1'}, + {id: 'vc2', issuer: 'Issuer2'}, + {id: 'vc3', issuer: 'Issuer3'}, + ]; + + const result = parseMetadatas(metadataObjects); + + expect(result).toHaveLength(3); + expect(result[0]).toBeInstanceOf(VCMetadata); + expect(result[0].id).toBe('vc1'); + expect(result[1].id).toBe('vc2'); + expect(result[2].id).toBe('vc3'); + }); + + it('should handle empty array', () => { + const result = parseMetadatas([]); + expect(result).toEqual([]); + }); + + it('should create VCMetadata instances for each object', () => { + const metadataObjects = [ + {id: 'test1', format: 'ldp_vc', isPinned: true}, + {id: 'test2', format: 'mso_mdoc', isPinned: false}, + ]; + + const result = parseMetadatas(metadataObjects); + + expect(result[0].id).toBe('test1'); + expect(result[0].format).toBe('ldp_vc'); + expect(result[0].isPinned).toBe(true); + + expect(result[1].id).toBe('test2'); + expect(result[1].format).toBe('mso_mdoc'); + expect(result[1].isPinned).toBe(false); + }); +}); + +describe('getVCMetadata', () => { + beforeEach(() => { + jest.spyOn(UUID, 'generate').mockReturnValue('test-uuid-12345'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should create VCMetadata with generated credential ID', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'https://issuer.example.com', + issuer_id: 'TestIssuer', + protocol: 'OpenId4VCI', + }, + timestamp: '1234567890', + vcMetadata: { + isVerified: false, + isExpired: false, + }, + verifiableCredential: null, + credentialWrapper: { + format: VCFormat.ldp_vc, + }, + selectedCredentialType: null, + }; + + const result = getVCMetadata(mockContext, 'ED25519'); + + expect(result.requestId).toContain('test-uuid-12345'); + expect(result.requestId).toContain('issuer'); + expect(result.issuer).toBe('TestIssuer'); + expect(result.protocol).toBe('OpenId4VCI'); + expect(result.timestamp).toBe('1234567890'); + expect(result.downloadKeyType).toBe('ED25519'); + expect(result.format).toBe(VCFormat.ldp_vc); + }); + + it('should handle credential_issuer when credential_issuer_host is not available', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer: 'https://backup.example.com', + issuer_id: 'BackupIssuer', + protocol: 'OpenId4VCI', + }, + timestamp: '', + vcMetadata: {}, + verifiableCredential: null, + credentialWrapper: { + format: VCFormat.mso_mdoc, + }, + selectedCredentialType: null, + }; + + const result = getVCMetadata(mockContext, 'RSA'); + + expect(result.issuer).toBe('BackupIssuer'); + expect(result.issuerHost).toBe('https://backup.example.com'); + expect(result.format).toBe(VCFormat.mso_mdoc); + }); + + it('should use credential_issuer as fallback for issuer_id', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'https://issuer.test.com', + credential_issuer: 'FallbackIssuer', + protocol: 'OIDC', + }, + timestamp: '9876543210', + vcMetadata: { + isVerified: true, + isExpired: false, + }, + verifiableCredential: null, + credentialWrapper: { + format: VCFormat.vc_sd_jwt, + }, + selectedCredentialType: null, + }; + + const result = getVCMetadata(mockContext, 'ECDSA'); + + expect(result.issuer).toBe('FallbackIssuer'); + expect(result.isVerified).toBe(true); + expect(result.downloadKeyType).toBe('ECDSA'); + }); + + it('should extract issuer name from URL', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'https://subdomain.example.org', + issuer_id: 'ExampleOrg', + protocol: 'OpenId4VCI', + }, + timestamp: '', + vcMetadata: {}, + verifiableCredential: null, + credentialWrapper: {format: VCFormat.ldp_vc}, + selectedCredentialType: null, + }; + + const result = getVCMetadata(mockContext, 'ED25519'); + + expect(result.requestId).toContain('subdomain'); + }); + + it('should handle invalid URL gracefully', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'not-a-valid-url', + issuer_id: 'TestIssuer', + protocol: 'OpenId4VCI', + }, + timestamp: '', + vcMetadata: {}, + verifiableCredential: null, + credentialWrapper: {format: VCFormat.ldp_vc}, + selectedCredentialType: null, + }; + + const result = getVCMetadata(mockContext, 'ED25519'); + + expect(result.requestId).toContain('not-a-valid-url'); + expect(result.issuerHost).toBe('not-a-valid-url'); + }); + + it('should handle Mosip VC with UIN', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'https://mosip.example.com', + issuer_id: 'Mosip', + protocol: 'OpenId4VCI', + }, + timestamp: '', + vcMetadata: {}, + verifiableCredential: { + credential: { + credentialSubject: { + UIN: '1234567890', + }, + }, + }, + credentialWrapper: {format: VCFormat.ldp_vc}, + selectedCredentialType: null, + }; + + const result = getVCMetadata(mockContext, 'ED25519'); + + expect(result.mosipIndividualId).toBe('1234567890'); + }); + + it('should handle Mosip VC with VID', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'https://mosip.example.com', + issuer_id: 'Mosip', + protocol: 'OpenId4VCI', + }, + timestamp: '', + vcMetadata: {}, + verifiableCredential: { + credential: { + credentialSubject: { + VID: '9876543210', + }, + }, + }, + credentialWrapper: {format: VCFormat.ldp_vc}, + selectedCredentialType: null, + }; + + const result = getVCMetadata(mockContext, 'ED25519'); + + expect(result.mosipIndividualId).toBe('9876543210'); + }); + + it('should set credential type when provided', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'https://issuer.example.com', + issuer_id: 'TestIssuer', + protocol: 'OpenId4VCI', + }, + timestamp: '1234567890', + vcMetadata: {}, + verifiableCredential: null, + credentialWrapper: {format: VCFormat.mso_mdoc}, + selectedCredentialType: 'org.iso.18013.5.1.mDL', + }; + + const result = getVCMetadata(mockContext, 'RSA'); + + expect(result.credentialType).toBeDefined(); + expect(result.format).toBe(VCFormat.mso_mdoc); + }); + + it('should handle different key types', () => { + const mockContext: any = { + selectedIssuer: { + credential_issuer_host: 'https://issuer.example.com', + issuer_id: 'TestIssuer', + protocol: 'OpenId4VCI', + }, + timestamp: '', + vcMetadata: {}, + verifiableCredential: null, + credentialWrapper: {format: VCFormat.vc_sd_jwt}, + selectedCredentialType: null, + }; + + const resultRSA = getVCMetadata(mockContext, 'RS256'); + expect(resultRSA.downloadKeyType).toBe('RS256'); + + const resultEC = getVCMetadata(mockContext, 'ES256'); + expect(resultEC.downloadKeyType).toBe('ES256'); + }); +}); diff --git a/shared/api.test.ts b/shared/api.test.ts new file mode 100644 index 00000000..f748daf2 --- /dev/null +++ b/shared/api.test.ts @@ -0,0 +1,216 @@ +import {API_URLS} from './api'; + +describe('API_URLS configuration', () => { + describe('trustedVerifiersList', () => { + it('should have GET method', () => { + expect(API_URLS.trustedVerifiersList.method).toBe('GET'); + }); + + it('should build correct URL', () => { + expect(API_URLS.trustedVerifiersList.buildURL()).toBe( + '/v1/mimoto/verifiers', + ); + }); + }); + + describe('issuersList', () => { + it('should have GET method', () => { + expect(API_URLS.issuersList.method).toBe('GET'); + }); + + it('should build correct URL', () => { + expect(API_URLS.issuersList.buildURL()).toBe('/v1/mimoto/issuers'); + }); + }); + + describe('issuerConfig', () => { + it('should have GET method', () => { + expect(API_URLS.issuerConfig.method).toBe('GET'); + }); + + it('should build correct URL with issuer id', () => { + expect(API_URLS.issuerConfig.buildURL('test-issuer')).toBe( + '/v1/mimoto/issuers/test-issuer', + ); + }); + }); + + describe('issuerWellknownConfig', () => { + it('should have GET method', () => { + expect(API_URLS.issuerWellknownConfig.method).toBe('GET'); + }); + + it('should build correct URL with credential issuer', () => { + expect( + API_URLS.issuerWellknownConfig.buildURL('https://example.com'), + ).toBe('https://example.com/.well-known/openid-credential-issuer'); + }); + }); + + describe('authorizationServerMetadataConfig', () => { + it('should have GET method', () => { + expect(API_URLS.authorizationServerMetadataConfig.method).toBe('GET'); + }); + + it('should build correct URL with authorization server URL', () => { + expect( + API_URLS.authorizationServerMetadataConfig.buildURL( + 'https://auth.example.com', + ), + ).toBe('https://auth.example.com/.well-known/oauth-authorization-server'); + }); + }); + + describe('allProperties', () => { + it('should have GET method', () => { + expect(API_URLS.allProperties.method).toBe('GET'); + }); + + it('should build correct URL', () => { + expect(API_URLS.allProperties.buildURL()).toBe( + '/v1/mimoto/allProperties', + ); + }); + }); + + describe('getIndividualId', () => { + it('should have POST method', () => { + expect(API_URLS.getIndividualId.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.getIndividualId.buildURL()).toBe( + '/v1/mimoto/aid/get-individual-id', + ); + }); + }); + + describe('reqIndividualOTP', () => { + it('should have POST method', () => { + expect(API_URLS.reqIndividualOTP.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.reqIndividualOTP.buildURL()).toBe( + '/v1/mimoto/req/individualId/otp', + ); + }); + }); + + describe('walletBinding', () => { + it('should have POST method', () => { + expect(API_URLS.walletBinding.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.walletBinding.buildURL()).toBe( + '/v1/mimoto/wallet-binding', + ); + }); + }); + + describe('bindingOtp', () => { + it('should have POST method', () => { + expect(API_URLS.bindingOtp.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.bindingOtp.buildURL()).toBe('/v1/mimoto/binding-otp'); + }); + }); + + describe('requestOtp', () => { + it('should have POST method', () => { + expect(API_URLS.requestOtp.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.requestOtp.buildURL()).toBe('/v1/mimoto/req/otp'); + }); + }); + + describe('credentialRequest', () => { + it('should have POST method', () => { + expect(API_URLS.credentialRequest.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.credentialRequest.buildURL()).toBe( + '/v1/mimoto/credentialshare/request', + ); + }); + }); + + describe('credentialStatus', () => { + it('should have GET method', () => { + expect(API_URLS.credentialStatus.method).toBe('GET'); + }); + + it('should build correct URL with id', () => { + expect(API_URLS.credentialStatus.buildURL('request-123')).toBe( + '/v1/mimoto/credentialshare/request/status/request-123', + ); + }); + }); + + describe('credentialDownload', () => { + it('should have POST method', () => { + expect(API_URLS.credentialDownload.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.credentialDownload.buildURL()).toBe( + '/v1/mimoto/credentialshare/download', + ); + }); + }); + + describe('linkTransaction', () => { + it('should have POST method', () => { + expect(API_URLS.linkTransaction.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.linkTransaction.buildURL()).toBe( + '/v1/esignet/linked-authorization/v2/link-transaction', + ); + }); + }); + + describe('authenticate', () => { + it('should have POST method', () => { + expect(API_URLS.authenticate.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.authenticate.buildURL()).toBe( + '/v1/esignet/linked-authorization/v2/authenticate', + ); + }); + }); + + describe('sendConsent', () => { + it('should have POST method', () => { + expect(API_URLS.sendConsent.method).toBe('POST'); + }); + + it('should build correct URL', () => { + expect(API_URLS.sendConsent.buildURL()).toBe( + '/v1/esignet/linked-authorization/v2/consent', + ); + }); + }); + + describe('googleAccountProfileInfo', () => { + it('should have GET method', () => { + expect(API_URLS.googleAccountProfileInfo.method).toBe('GET'); + }); + + it('should build correct URL with access token', () => { + const accessToken = 'test-token-123'; + expect(API_URLS.googleAccountProfileInfo.buildURL(accessToken)).toBe( + `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${accessToken}`, + ); + }); + }); +}); diff --git a/shared/commonUtil.test.ts b/shared/commonUtil.test.ts index 40b241a0..c6c80fac 100644 --- a/shared/commonUtil.test.ts +++ b/shared/commonUtil.test.ts @@ -1,4 +1,3 @@ -import {useState} from 'react'; import testIDProps, { bytesToMB, faceMatchConfig, @@ -12,8 +11,20 @@ import testIDProps, { logState, removeWhiteSpace, sleep, + getRandomInt, + getMosipIdentifier, + isTranslationKeyFound, + getAccountType, + BYTES_IN_MEGABYTE, } from './commonUtil'; -import {argon2iConfig} from './constants'; +import { + argon2iConfig, + GOOGLE_DRIVE_NAME, + ICLOUD_DRIVE_NAME, + GMAIL, + APPLE, +} from './constants'; +import {CredentialSubject} from '../machines/VerifiableCredential/VCMetaMachine/vc.d'; describe('hashData', () => { it('should expose a function', () => { @@ -74,6 +85,27 @@ describe('removeWhiteSpace', () => { const response = removeWhiteSpace('React Native Unit Testing'); expect(response).toBe('ReactNativeUnitTesting'); }); + + it('should handle empty string', () => { + expect(removeWhiteSpace('')).toBe(''); + }); + + it('should handle string with only spaces', () => { + expect(removeWhiteSpace(' ')).toBe(''); + }); + + it('should handle string with tabs and newlines', () => { + expect(removeWhiteSpace('Hello\tWorld\n')).toBe('HelloWorld'); + }); + + it('should handle string with multiple types of whitespace', () => { + expect(removeWhiteSpace(' Test \t String \n ')).toBe('TestString'); + }); + + it('should remove all whitespace from string', () => { + const result = removeWhiteSpace('Hello World Test'); + expect(result).toBe('HelloWorldTest'); + }); }); describe('logState', () => { @@ -97,17 +129,27 @@ describe('getMaskedText', () => { const maskedTxt = getMaskedText(id); expect(maskedTxt).toBe('******7890'); }); -}); -describe('faceMatchConfig', () => { - it('should expose a function', () => { - expect(faceMatchConfig).toBeDefined(); + it('should handle exactly 4 characters', () => { + expect(getMaskedText('1234')).toBe('1234'); }); - // it('faceMatchConfig should return expected output', () => { - // // const retValue = faceMatchConfig(resp); - // expect(false).toBeTruthy(); - // }); + it('should handle long strings', () => { + const longString = '12345678901234567890'; + const masked = getMaskedText(longString); + expect(masked.endsWith('7890')).toBe(true); + expect(masked.length).toBe(longString.length); + }); + + it('should handle short strings', () => { + const result = getMaskedText('ABCDEF'); + expect(result).toBe('**CDEF'); + }); + + it('should handle exactly 4 characters (ABCD)', () => { + const result = getMaskedText('ABCD'); + expect(result).toBe('ABCD'); + }); }); describe('getBackupFileName', () => { @@ -116,26 +158,19 @@ describe('getBackupFileName', () => { }); }); -describe('bytesToMB', () => { - it('bytesToMB returns a string', () => { - expect(bytesToMB(0)).toBe('0'); - }); - - it('10^6 bytes is 1MB', () => { - expect(bytesToMB(1e6)).toBe('1.000'); - }); -}); - describe('getDriveName', () => { it('should expose a function', () => { expect(getDriveName).toBeDefined(); }); - it('getDriveName should return Google Drive on Android', () => { - expect(getDriveName()).toBe('Google Drive'); + it('should return a string', () => { + const result = getDriveName(); + expect(typeof result).toBe('string'); }); - it('getDriveName should return Google Drive on Android', () => { - expect(getDriveName()).toBe('Google Drive'); + + it('should return Google Drive for Android or iCloud for iOS', () => { + const result = getDriveName(); + expect([GOOGLE_DRIVE_NAME, ICLOUD_DRIVE_NAME]).toContain(result); }); }); @@ -149,6 +184,19 @@ describe('sleep : The promise resolves after a certain time', () => { const promise = sleep(time); expect(promise).toBeInstanceOf(Promise); }); + + it('should delay for specified milliseconds', async () => { + const start = Date.now(); + await sleep(100); + const end = Date.now(); + const elapsed = end - start; + expect(elapsed).toBeGreaterThanOrEqual(90); // Allow small margin + }); + + it('should resolve after timeout', async () => { + const promise = sleep(50); + await expect(promise).resolves.toBeUndefined(); + }); }); describe('getScreenHeight', () => { @@ -160,4 +208,240 @@ describe('getScreenHeight', () => { const height = getScreenHeight(); expect(typeof height).toBe('object'); }); + + it('should return a value', () => { + const height = getScreenHeight(); + expect(height).toBeDefined(); + }); +}); + +describe('getRandomInt', () => { + it('should expose a function', () => { + expect(getRandomInt).toBeDefined(); + }); + + it('should return a number within the specified range', () => { + const min = 1; + const max = 10; + const result = getRandomInt(min, max); + expect(result).toBeGreaterThanOrEqual(min); + expect(result).toBeLessThanOrEqual(max); + expect(Number.isInteger(result)).toBe(true); + }); + + it('should return min when min and max are equal', () => { + const value = 5; + const result = getRandomInt(value, value); + expect(result).toBe(value); + }); + + it('should handle negative ranges', () => { + const result = getRandomInt(-10, -1); + expect(result).toBeGreaterThanOrEqual(-10); + expect(result).toBeLessThanOrEqual(-1); + }); + + it('should handle larger ranges', () => { + const result = getRandomInt(100, 200); + expect(result).toBeGreaterThanOrEqual(100); + expect(result).toBeLessThanOrEqual(200); + }); +}); + +describe('getMosipIdentifier', () => { + it('should expose a function', () => { + expect(getMosipIdentifier).toBeDefined(); + }); + + it('should return UIN when UIN is present', () => { + const credentialSubject = { + UIN: '123456789', + VID: '987654321', + } as Partial; + const result = getMosipIdentifier(credentialSubject as CredentialSubject); + expect(result).toBe('123456789'); + }); + + it('should return VID when UIN is not present', () => { + const credentialSubject = {VID: '987654321'} as Partial; + const result = getMosipIdentifier(credentialSubject as CredentialSubject); + expect(result).toBe('987654321'); + }); + + it('should return undefined when neither UIN nor VID is present', () => { + const credentialSubject = {} as Partial; + const result = getMosipIdentifier(credentialSubject as CredentialSubject); + expect(result).toBeUndefined(); + }); + + it('should prioritize UIN over VID', () => { + const credSubject = { + UIN: '1111111111', + VID: '2222222222', + } as CredentialSubject; + expect(getMosipIdentifier(credSubject)).toBe('1111111111'); + }); +}); + +describe('isTranslationKeyFound', () => { + it('should expose a function', () => { + expect(isTranslationKeyFound).toBeDefined(); + }); + + it('should return true when translation key is found', () => { + const mockT = jest.fn(() => 'Translated text'); + const result = isTranslationKeyFound('someKey', mockT); + expect(result).toBe(true); + }); + + it('should return false when translation key is not found', () => { + const mockT = jest.fn((key: string) => key); + const result = isTranslationKeyFound('someKey', mockT); + expect(result).toBe(false); + }); + + it('should return true when key is translated', () => { + const mockT = () => 'Translated value'; + expect(isTranslationKeyFound('any.key', mockT)).toBe(true); + }); + + it('should return false when translation key not found', () => { + const mockT = (key: string) => key; // returns same key + expect(isTranslationKeyFound('some.unknown.key', mockT)).toBe(false); + }); + + it('should return true for errors.notFound key when translation is found', () => { + const mockT = (key: string) => { + if (key === 'errors.notFound') return 'Error Not Found'; + return key; + }; + expect(isTranslationKeyFound('errors.notFound', mockT)).toBe(true); + }); +}); + +describe('getAccountType', () => { + it('should expose a function', () => { + expect(getAccountType).toBeDefined(); + }); + + it('should return a string', () => { + const result = getAccountType(); + expect(typeof result).toBe('string'); + }); + + it('should return Gmail for Android or Apple for iOS', () => { + const result = getAccountType(); + expect([GMAIL, APPLE]).toContain(result); + }); +}); + +describe('faceMatchConfig', () => { + it('should expose a function', () => { + expect(faceMatchConfig).toBeDefined(); + }); + + it('should return a valid configuration object', () => { + const config = faceMatchConfig(); + expect(config).toBeDefined(); + expect(config.withFace).toBeDefined(); + expect(config.withFace.encoder).toBeDefined(); + expect(config.withFace.matcher).toBeDefined(); + expect(config.withFace.encoder.tfModel).toBeDefined(); + expect(config.withFace.matcher.threshold).toBe(1); + }); + + it('should return config with correct structure', () => { + const config = faceMatchConfig(); + expect(config).toHaveProperty('withFace'); + expect(config.withFace).toHaveProperty('encoder'); + expect(config.withFace).toHaveProperty('matcher'); + expect(config.withFace.encoder.tfModel).toHaveProperty('path'); + expect(config.withFace.encoder.tfModel).toHaveProperty('modelChecksum'); + expect(config.withFace.matcher.threshold).toBe(1); + }); +}); + +describe('BYTES_IN_MEGABYTE', () => { + it('should be defined', () => { + expect(BYTES_IN_MEGABYTE).toBeDefined(); + }); + + it('should equal 1,000,000', () => { + expect(BYTES_IN_MEGABYTE).toBe(1000000); + }); + + it('should be 1000 * 1000', () => { + expect(BYTES_IN_MEGABYTE).toBe(1000 * 1000); + }); + + it('should be a number', () => { + expect(typeof BYTES_IN_MEGABYTE).toBe('number'); + }); + + it('should be positive', () => { + expect(BYTES_IN_MEGABYTE).toBeGreaterThan(0); + }); +}); + +describe('bytesToMB', () => { + it('bytesToMB returns a string', () => { + expect(bytesToMB(0)).toBe('0'); + }); + + it('10^6 bytes is 1MB', () => { + expect(bytesToMB(1e6)).toBe('1.000'); + }); + + it('should return "0" for negative bytes', () => { + expect(bytesToMB(-100)).toBe('0'); + }); + + it('should convert 1,000,000 bytes to "1.000" MB', () => { + expect(bytesToMB(1000000)).toBe('1.000'); + }); + + it('should convert 2,500,000 bytes to "2.500" MB', () => { + expect(bytesToMB(2500000)).toBe('2.500'); + }); + + it('should convert 500,000 bytes to "0.500" MB', () => { + expect(bytesToMB(500000)).toBe('0.500'); + }); + + it('should handle large byte values', () => { + expect(bytesToMB(10000000)).toBe('10.000'); + }); + + it('should handle small byte values', () => { + expect(bytesToMB(1000)).toBe('0.001'); + }); + + it('should return three decimal places', () => { + const result = bytesToMB(1234567); + expect(result).toMatch(/^\d+\.\d{3}$/); + }); + + it('should handle fractional megabytes', () => { + expect(bytesToMB(1234567)).toBe('1.235'); + }); + + it('should handle very small values', () => { + expect(bytesToMB(100)).toBe('0.000'); + }); + + it('should handle exactly one byte', () => { + expect(bytesToMB(1)).toBe('0.000'); + }); + + it('should convert bytes to megabytes', () => { + const bytes = BYTES_IN_MEGABYTE * 5; // 5 MB + const result = bytesToMB(bytes); + expect(result).toBe('5.000'); + }); + + it('should handle fractional megabytes with BYTES_IN_MEGABYTE constant', () => { + const bytes = BYTES_IN_MEGABYTE * 2.5; + const result = bytesToMB(bytes); + expect(result).toBe('2.500'); + }); }); diff --git a/shared/cryptoutil/KeyTypes.test.ts b/shared/cryptoutil/KeyTypes.test.ts new file mode 100644 index 00000000..9deb65cc --- /dev/null +++ b/shared/cryptoutil/KeyTypes.test.ts @@ -0,0 +1,37 @@ +import {KeyTypes} from './KeyTypes'; + +describe('KeyTypes', () => { + it('should have RS256 key type', () => { + expect(KeyTypes.RS256).toBe('RS256'); + }); + + it('should have ES256 key type', () => { + expect(KeyTypes.ES256).toBe('ES256'); + }); + + it('should have ES256K key type', () => { + expect(KeyTypes.ES256K).toBe('ES256K'); + }); + + it('should have ED25519 key type', () => { + expect(KeyTypes.ED25519).toBe('Ed25519'); + }); + + it('should have exactly 4 key types', () => { + const keyTypeCount = Object.keys(KeyTypes).length; + expect(keyTypeCount).toBe(4); + }); + + it('should allow access via enum key', () => { + expect(KeyTypes['RS256']).toBe('RS256'); + expect(KeyTypes['ES256']).toBe('ES256'); + expect(KeyTypes['ES256K']).toBe('ES256K'); + expect(KeyTypes['ED25519']).toBe('Ed25519'); + }); + + it('should have all unique values', () => { + const values = Object.values(KeyTypes); + const uniqueValues = new Set(values); + expect(uniqueValues.size).toBe(values.length); + }); +}); diff --git a/shared/error/BiometricCancellationError.test.ts b/shared/error/BiometricCancellationError.test.ts new file mode 100644 index 00000000..d5b2a1bf --- /dev/null +++ b/shared/error/BiometricCancellationError.test.ts @@ -0,0 +1,31 @@ +import {BiometricCancellationError} from './BiometricCancellationError'; + +describe('BiometricCancellationError', () => { + it('should create an error instance with the correct message', () => { + const errorMessage = 'User cancelled biometric authentication'; + const error = new BiometricCancellationError(errorMessage); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(BiometricCancellationError); + expect(error.message).toBe(errorMessage); + }); + + it('should have the correct error name', () => { + const error = new BiometricCancellationError('Test error'); + + expect(error.name).toBe('BiometricCancellationError'); + }); + + it('should maintain the error stack trace', () => { + const error = new BiometricCancellationError('Stack trace test'); + + expect(error.stack).toBeDefined(); + }); + + it('should handle empty message', () => { + const error = new BiometricCancellationError(''); + + expect(error.message).toBe(''); + expect(error.name).toBe('BiometricCancellationError'); + }); +}); diff --git a/shared/error/UnsupportedVCFormat.test.ts b/shared/error/UnsupportedVCFormat.test.ts new file mode 100644 index 00000000..67c63d24 --- /dev/null +++ b/shared/error/UnsupportedVCFormat.test.ts @@ -0,0 +1,38 @@ +import {UnsupportedVcFormat} from './UnsupportedVCFormat'; + +describe('UnsupportedVcFormat', () => { + it('should create an error instance with the correct format message', () => { + const format = 'jwt_vc_json'; + const error = new UnsupportedVcFormat(format); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(UnsupportedVcFormat); + expect(error.message).toBe(format); + }); + + it('should have the correct error name', () => { + const error = new UnsupportedVcFormat('ldp_vc'); + + expect(error.name).toBe('UnsupportedVcFormat'); + }); + + it('should maintain the error stack trace', () => { + const error = new UnsupportedVcFormat('custom_format'); + + expect(error.stack).toBeDefined(); + }); + + it('should handle empty format string', () => { + const error = new UnsupportedVcFormat(''); + + expect(error.message).toBe(''); + expect(error.name).toBe('UnsupportedVcFormat'); + }); + + it('should handle complex format strings', () => { + const format = 'application/vc+sd-jwt'; + const error = new UnsupportedVcFormat(format); + + expect(error.message).toBe(format); + }); +}); diff --git a/shared/javascript.test.ts b/shared/javascript.test.ts new file mode 100644 index 00000000..4114b8ae --- /dev/null +++ b/shared/javascript.test.ts @@ -0,0 +1,73 @@ +import {groupBy} from './javascript'; + +describe('javascript utils', () => { + describe('groupBy', () => { + it('should group elements based on predicate', () => { + const array = [1, 2, 3, 4, 5, 6]; + const predicate = (num: number) => num % 2 === 0; + + const [trueElements, falseElements] = groupBy(array, predicate); + + expect(trueElements).toEqual([2, 4, 6]); + expect(falseElements).toEqual([1, 3, 5]); + }); + + it('should return empty arrays for empty input', () => { + const array: number[] = []; + const predicate = (num: number) => num > 0; + + const [trueElements, falseElements] = groupBy(array, predicate); + + expect(trueElements).toEqual([]); + expect(falseElements).toEqual([]); + }); + + it('should put all elements in true group when predicate always returns true', () => { + const array = ['a', 'b', 'c']; + const predicate = () => true; + + const [trueElements, falseElements] = groupBy(array, predicate); + + expect(trueElements).toEqual(['a', 'b', 'c']); + expect(falseElements).toEqual([]); + }); + + it('should put all elements in false group when predicate always returns false', () => { + const array = ['a', 'b', 'c']; + const predicate = () => false; + + const [trueElements, falseElements] = groupBy(array, predicate); + + expect(trueElements).toEqual([]); + expect(falseElements).toEqual(['a', 'b', 'c']); + }); + + it('should handle complex objects', () => { + const array = [ + {name: 'John', age: 30}, + {name: 'Jane', age: 25}, + {name: 'Bob', age: 35}, + ]; + const predicate = (person: {name: string; age: number}) => + person.age >= 30; + + const [trueElements, falseElements] = groupBy(array, predicate); + + expect(trueElements).toEqual([ + {name: 'John', age: 30}, + {name: 'Bob', age: 35}, + ]); + expect(falseElements).toEqual([{name: 'Jane', age: 25}]); + }); + + it('should handle undefined array', () => { + const array = undefined as any; + const predicate = (num: number) => num > 0; + + const [trueElements, falseElements] = groupBy(array, predicate); + + expect(trueElements).toEqual([]); + expect(falseElements).toEqual([]); + }); + }); +}); diff --git a/shared/openId4VCI/Utils.test.ts b/shared/openId4VCI/Utils.test.ts new file mode 100644 index 00000000..31c9e9ce --- /dev/null +++ b/shared/openId4VCI/Utils.test.ts @@ -0,0 +1,407 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + Protocols, + Issuers, + isActivationNeeded, + ACTIVATION_NEEDED, + Issuers_Key_Ref, + getDisplayObjectForCurrentLanguage, + removeBottomSectionFields, + getMatchingCredentialIssuerMetadata, + selectCredentialRequestKey, + updateCredentialInformation, +} from './Utils'; +import {VCFormat} from '../VCFormat'; + +// Mock VCProcessor +jest.mock('../../components/VC/common/VCProcessor', () => ({ + VCProcessor: { + processForRendering: jest.fn().mockResolvedValue({ + processedData: 'mocked-processed-credential', + }), + }, +})); + +describe('openId4VCI Utils', () => { + describe('Protocols', () => { + it('should have OpenId4VCI protocol defined', () => { + expect(Protocols.OpenId4VCI).toBe('OpenId4VCI'); + }); + + it('should have OTP protocol defined', () => { + expect(Protocols.OTP).toBe('OTP'); + }); + }); + + describe('Issuers', () => { + it('should have MosipOtp issuer defined', () => { + expect(Issuers.MosipOtp).toBe('MosipOtp'); + }); + + it('should have Mosip issuer defined', () => { + expect(Issuers.Mosip).toBe('Mosip'); + }); + }); + + describe('ACTIVATION_NEEDED', () => { + it('should contain Mosip', () => { + expect(ACTIVATION_NEEDED).toContain(Issuers.Mosip); + }); + + it('should contain MosipOtp', () => { + expect(ACTIVATION_NEEDED).toContain(Issuers.MosipOtp); + }); + + it('should have exactly 2 issuers', () => { + expect(ACTIVATION_NEEDED).toHaveLength(2); + }); + }); + + describe('isActivationNeeded', () => { + it('should return true for Mosip issuer', () => { + expect(isActivationNeeded('Mosip')).toBe(true); + }); + + it('should return true for MosipOtp issuer', () => { + expect(isActivationNeeded('MosipOtp')).toBe(true); + }); + + it('should return false for other issuers', () => { + expect(isActivationNeeded('SomeOtherIssuer')).toBe(false); + }); + + it('should return false for empty string', () => { + expect(isActivationNeeded('')).toBe(false); + }); + + it('should return false for undefined', () => { + expect(isActivationNeeded(undefined as any)).toBe(false); + }); + + it('should be case sensitive', () => { + expect(isActivationNeeded('mosip')).toBe(false); + expect(isActivationNeeded('MOSIP')).toBe(false); + }); + }); + + describe('Issuers_Key_Ref', () => { + it('should have correct key reference', () => { + expect(Issuers_Key_Ref).toBe('OpenId4VCI_KeyPair'); + }); + }); + + describe('getDisplayObjectForCurrentLanguage', () => { + it('should return display object for current language', () => { + const display = [ + {language: 'en', name: 'English Name', logo: 'en-logo.png'}, + {language: 'hi', name: 'Hindi Name', logo: 'hi-logo.png'}, + ] as any; + + const result = getDisplayObjectForCurrentLanguage(display); + expect(result).toBeDefined(); + expect(result.name).toBeDefined(); + }); + + it('should return first display object when language not found', () => { + const display = [ + {language: 'fr', name: 'French Name', logo: 'fr-logo.png'}, + {language: 'de', name: 'German Name', logo: 'de-logo.png'}, + ] as any; + + const result = getDisplayObjectForCurrentLanguage(display); + expect(result).toBeDefined(); + expect(result.name).toBe('French Name'); + }); + + it('should return empty object when display array is empty', () => { + const result = getDisplayObjectForCurrentLanguage([]); + expect(result).toEqual({}); + }); + + it('should return empty object when display is null', () => { + const result = getDisplayObjectForCurrentLanguage(null as any); + expect(result).toEqual({}); + }); + + it('should handle locale key instead of language key', () => { + const display = [ + {locale: 'en-US', name: 'English Name', logo: 'en-logo.png'}, + {locale: 'hi-IN', name: 'Hindi Name', logo: 'hi-logo.png'}, + ] as any; + + const result = getDisplayObjectForCurrentLanguage(display); + expect(result).toBeDefined(); + }); + + it('should fallback to en-US when current language not found', () => { + const display = [ + {language: 'fr', name: 'French Name'}, + {language: 'en-US', name: 'English US Name'}, + ] as any; + + const result = getDisplayObjectForCurrentLanguage(display); + expect(result.name).toBe('English US Name'); + }); + }); + + describe('removeBottomSectionFields', () => { + it('should remove bottom section fields for SD-JWT format', () => { + const fields = ['name', 'age', 'photo', 'signature', 'address']; + const result = removeBottomSectionFields(fields, VCFormat.vc_sd_jwt); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should remove bottom section fields for DC-SD-JWT format', () => { + const fields = ['name', 'age', 'photo', 'signature']; + const result = removeBottomSectionFields(fields, VCFormat.dc_sd_jwt); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should remove address field for LDP format', () => { + const fields = ['name', 'age', 'address', 'photo']; + const result = removeBottomSectionFields(fields, VCFormat.ldp_vc); + + expect(result).toBeDefined(); + expect(result).not.toContain('address'); + }); + + it('should handle empty fields array', () => { + const result = removeBottomSectionFields([], VCFormat.ldp_vc); + expect(result).toEqual([]); + }); + }); + + describe('getMatchingCredentialIssuerMetadata', () => { + it('should return matching credential configuration', () => { + const wellknown = { + credential_configurations_supported: { + MOSIPVerifiableCredential: { + format: 'ldp_vc', + order: ['name', 'age'], + }, + AnotherCredential: { + format: 'jwt_vc', + }, + }, + }; + + const result = getMatchingCredentialIssuerMetadata( + wellknown, + 'MOSIPVerifiableCredential', + ); + + expect(result).toBeDefined(); + expect(result.format).toBe('ldp_vc'); + expect(result.order).toEqual(['name', 'age']); + }); + + it('should throw error when credential type not found', () => { + const wellknown = { + credential_configurations_supported: { + SomeCredential: {format: 'ldp_vc'}, + }, + }; + + expect(() => { + getMatchingCredentialIssuerMetadata(wellknown, 'NonExistentCredential'); + }).toThrow(); + }); + + it('should handle multiple credential configurations', () => { + const wellknown = { + credential_configurations_supported: { + Credential1: {format: 'ldp_vc'}, + Credential2: {format: 'jwt_vc'}, + Credential3: {format: 'mso_mdoc'}, + }, + }; + + const result = getMatchingCredentialIssuerMetadata( + wellknown, + 'Credential2', + ); + + expect(result).toBeDefined(); + expect(result.format).toBe('jwt_vc'); + }); + }); + + describe('selectCredentialRequestKey', () => { + it('should select first supported key type', () => { + const proofSigningAlgos = ['RS256', 'ES256']; + const keyOrder = {'0': 'RS256', '1': 'ES256', '2': 'Ed25519'}; + + const result = selectCredentialRequestKey(proofSigningAlgos, keyOrder); + expect(result).toBe('RS256'); + }); + + it('should return first key when no match found', () => { + const proofSigningAlgos = ['UNKNOWN_ALGO']; + const keyOrder = {'0': 'RS256', '1': 'ES256'}; + + const result = selectCredentialRequestKey(proofSigningAlgos, keyOrder); + expect(result).toBe('RS256'); + }); + + it('should handle empty proofSigningAlgos', () => { + const keyOrder = {'0': 'RS256', '1': 'ES256'}; + + const result = selectCredentialRequestKey([], keyOrder); + expect(result).toBe('RS256'); + }); + + it('should select matching key from middle of order', () => { + const proofSigningAlgos = ['ES256']; + const keyOrder = {'0': 'RS256', '1': 'ES256', '2': 'Ed25519'}; + + const result = selectCredentialRequestKey(proofSigningAlgos, keyOrder); + expect(result).toBe('ES256'); + }); + }); + + describe('updateCredentialInformation', () => { + it('should update credential information for MSO_MDOC format', async () => { + const mockContext = { + selectedCredentialType: { + id: 'TestCredential', + format: VCFormat.mso_mdoc, + }, + selectedIssuer: { + display: [{language: 'en', logo: 'test-logo.png'}], + }, + vcMetadata: { + id: 'test-id', + }, + }; + + const mockCredential = { + credential: 'test-credential-data', + } as any; + + const result = await updateCredentialInformation( + mockContext, + mockCredential, + ); + + expect(result).toBeDefined(); + expect(result.format).toBe(VCFormat.mso_mdoc); + expect(result.verifiableCredential).toBeDefined(); + expect(result.verifiableCredential.credentialConfigurationId).toBe( + 'TestCredential', + ); + expect(result.generatedOn).toBeInstanceOf(Date); + }); + + it('should update credential information for SD-JWT format', async () => { + const mockContext = { + selectedCredentialType: { + id: 'SDJWTCredential', + format: VCFormat.vc_sd_jwt, + }, + selectedIssuer: { + display: [{language: 'en', logo: 'sd-jwt-logo.png'}], + }, + vcMetadata: { + id: 'sd-jwt-id', + }, + }; + + const mockCredential = { + credential: 'sd-jwt-credential-data', + } as any; + + const result = await updateCredentialInformation( + mockContext, + mockCredential, + ); + + expect(result).toBeDefined(); + expect(result.format).toBe(VCFormat.vc_sd_jwt); + expect(result.vcMetadata.format).toBe(VCFormat.vc_sd_jwt); + }); + + it('should update credential information for DC-SD-JWT format', async () => { + const mockContext = { + selectedCredentialType: { + id: 'DCSDJWTCredential', + format: VCFormat.dc_sd_jwt, + }, + selectedIssuer: { + display: [{language: 'en', logo: 'dc-logo.png'}], + }, + vcMetadata: { + id: 'dc-jwt-id', + }, + }; + + const mockCredential = { + credential: 'dc-sd-jwt-credential-data', + } as any; + + const result = await updateCredentialInformation( + mockContext, + mockCredential, + ); + + expect(result).toBeDefined(); + expect(result.format).toBe(VCFormat.dc_sd_jwt); + }); + + it('should handle credential without logo in display', async () => { + const mockContext = { + selectedCredentialType: { + id: 'NoLogoCredential', + format: VCFormat.ldp_vc, + }, + selectedIssuer: { + display: [{language: 'en'}], + }, + vcMetadata: {}, + }; + + const mockCredential = { + credential: 'no-logo-credential', + } as any; + + const result = await updateCredentialInformation( + mockContext, + mockCredential, + ); + + expect(result).toBeDefined(); + expect(result.verifiableCredential.issuerLogo).toBe(''); + }); + + it('should include vcMetadata with format', async () => { + const mockContext = { + selectedCredentialType: { + id: 'MetadataTest', + format: VCFormat.ldp_vc, + }, + selectedIssuer: { + display: [], + }, + vcMetadata: { + id: 'metadata-id', + }, + }; + + const mockCredential = { + credential: 'metadata-test', + } as any; + + const result = await updateCredentialInformation( + mockContext, + mockCredential, + ); + + expect(result.vcMetadata).toBeDefined(); + expect(result.vcMetadata.format).toBe(VCFormat.ldp_vc); + expect(result.vcMetadata.id).toBe('metadata-id'); + }); + }); +}); diff --git a/shared/sharing/imageUtils.test.ts b/shared/sharing/imageUtils.test.ts new file mode 100644 index 00000000..fcc5a816 --- /dev/null +++ b/shared/sharing/imageUtils.test.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {shareImageToAllSupportedApps} from './imageUtils'; +import RNShare from 'react-native-share'; + +jest.mock('react-native-share', () => ({ + open: jest.fn(), +})); + +describe('imageUtils', () => { + describe('shareImageToAllSupportedApps', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return true when sharing is successful', async () => { + const mockShareOptions = { + url: 'file://path/to/image.jpg', + type: 'image/jpeg', + }; + + (RNShare.open as jest.Mock).mockResolvedValue({success: true}); + + const result = await shareImageToAllSupportedApps(mockShareOptions); + + expect(result).toBe(true); + expect(RNShare.open).toHaveBeenCalledWith(mockShareOptions); + }); + + it('should return false when sharing fails', async () => { + const mockShareOptions = { + url: 'file://path/to/image.jpg', + type: 'image/jpeg', + }; + + (RNShare.open as jest.Mock).mockResolvedValue({success: false}); + + const result = await shareImageToAllSupportedApps(mockShareOptions); + + expect(result).toBe(false); + }); + + it('should return false when an exception occurs', async () => { + const mockShareOptions = { + url: 'file://path/to/image.jpg', + type: 'image/jpeg', + }; + + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + (RNShare.open as jest.Mock).mockRejectedValue(new Error('Share failed')); + + const result = await shareImageToAllSupportedApps(mockShareOptions); + + expect(result).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Exception while sharing image::', + expect.any(Error), + ); + + consoleErrorSpy.mockRestore(); + }); + + it('should handle different share options', async () => { + const mockShareOptions = { + url: 'file://path/to/qr-code.png', + type: 'image/png', + title: 'Share QR Code', + }; + + (RNShare.open as jest.Mock).mockResolvedValue({success: true}); + + const result = await shareImageToAllSupportedApps(mockShareOptions); + + expect(result).toBe(true); + expect(RNShare.open).toHaveBeenCalledWith(mockShareOptions); + }); + + it('should handle user dismissing share dialog', async () => { + const mockShareOptions = { + url: 'file://path/to/image.jpg', + }; + + (RNShare.open as jest.Mock).mockRejectedValue({ + message: 'User did not share', + }); + + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + const result = await shareImageToAllSupportedApps(mockShareOptions); + + expect(result).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Exception while sharing image::', + expect.objectContaining({message: 'User did not share'}), + ); + consoleErrorSpy.mockRestore(); + }); + }); +});