diff --git a/app/tests/src/screens/verification/DocumentSelectorForProvingScreen.test.tsx b/app/tests/src/screens/verification/DocumentSelectorForProvingScreen.test.tsx index 6da08e6ee..81e5cd90d 100644 --- a/app/tests/src/screens/verification/DocumentSelectorForProvingScreen.test.tsx +++ b/app/tests/src/screens/verification/DocumentSelectorForProvingScreen.test.tsx @@ -10,24 +10,23 @@ import type { DocumentMetadata, IDDocument, } from '@selfxyz/common/utils/types'; -import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { + getDocumentAttributes, + isDocumentValidForProving, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { usePassport } from '@/providers/passportDataProvider'; import { DocumentSelectorForProvingScreen } from '@/screens/verification/DocumentSelectorForProvingScreen'; -jest.mock('@/providers/passportDataProvider', () => ({ - usePassport: jest.fn(), +jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ + useSelfClient: jest.fn(), + getDocumentAttributes: jest.fn(), + isDocumentValidForProving: jest.fn(), })); -jest.mock('@/utils/documentAttributes', () => ({ - checkDocumentExpiration: jest.fn( - (expiryDateSlice: string) => expiryDateSlice === 'expired', - ), - getDocumentAttributes: jest.fn( - (documentData: { expiryDateSlice?: string }) => ({ - expiryDateSlice: documentData.expiryDateSlice, - }), - ), +jest.mock('@/providers/passportDataProvider', () => ({ + usePassport: jest.fn(), })); const mockUseNavigation = useNavigation as jest.MockedFunction< @@ -36,6 +35,12 @@ const mockUseNavigation = useNavigation as jest.MockedFunction< const mockUseSelfClient = useSelfClient as jest.MockedFunction< typeof useSelfClient >; +const mockGetDocumentAttributes = + getDocumentAttributes as jest.MockedFunction; +const mockIsDocumentValidForProving = + isDocumentValidForProving as jest.MockedFunction< + typeof isDocumentValidForProving + >; const mockUsePassport = usePassport as jest.MockedFunction; type MockDocumentEntry = { @@ -58,6 +63,7 @@ const createMetadata = ( const createDocumentEntry = ( metadata: DocumentMetadata, expiryDateSlice?: string, + nationalitySlice?: string, ): MockDocumentEntry => ({ metadata, data: { @@ -65,6 +71,7 @@ const createDocumentEntry = ( documentCategory: metadata.documentCategory as any, mock: metadata.mock, expiryDateSlice, + nationalitySlice, } as unknown as IDDocument, }); @@ -84,6 +91,12 @@ const mockSelfApp = { endpoint: 'https://example.com', logoBase64: 'https://example.com/logo.png', sessionId: 'session-id', + disclosures: { + name: true, + passport_number: true, + }, + userId: '0x1234567890abcdef1234567890abcdef12345678', + userIdType: 'hex', }; const mockNavigate = jest.fn(); @@ -122,6 +135,17 @@ describe('DocumentSelectorForProvingScreen', () => { mockUseSelfClient.mockReturnValue(stableSelfClient as any); mockUsePassport.mockReturnValue(stablePassportContext as any); + + mockIsDocumentValidForProving.mockImplementation( + (_metadata, documentData) => + (documentData as { expiryDateSlice?: string } | undefined) + ?.expiryDateSlice !== 'expired', + ); + mockGetDocumentAttributes.mockImplementation( + (documentData: { nationalitySlice?: string }) => ({ + nationalitySlice: documentData.nationalitySlice, + }), + ); }); it('renders loading state initially', () => { @@ -143,13 +167,78 @@ describe('DocumentSelectorForProvingScreen', () => { ); await waitFor(() => { - expect(getByTestId('document-selector-logo')).toBeTruthy(); + expect(getByTestId('document-selector-card-header-logo')).toBeTruthy(); }); expect(getByText('example.com')).toBeTruthy(); expect(getByText('Example App')).toBeTruthy(); }); + it('renders disclosure items and wallet badge for the proof request card', async () => { + const passport = createMetadata({ + id: 'doc-1', + documentType: 'us', + isRegistered: true, + }); + const catalog: DocumentCatalog = { + documents: [passport], + selectedDocumentId: 'doc-1', + }; + + mockLoadDocumentCatalog.mockResolvedValue(catalog); + mockGetAllDocuments.mockResolvedValue( + createAllDocuments([createDocumentEntry(passport)]), + ); + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('document-selector-card')).toBeTruthy(); + }); + + expect( + getByTestId('document-selector-disclosure-name'), + ).toBeTruthy(); + expect( + getByTestId('document-selector-disclosure-passport_number'), + ).toBeTruthy(); + expect(getByTestId('document-selector-wallet-badge')).toBeTruthy(); + }); + + it('opens the wallet modal when the wallet badge is pressed', async () => { + const passport = createMetadata({ + id: 'doc-1', + documentType: 'us', + isRegistered: true, + }); + const catalog: DocumentCatalog = { + documents: [passport], + selectedDocumentId: 'doc-1', + }; + + mockLoadDocumentCatalog.mockResolvedValue(catalog); + mockGetAllDocuments.mockResolvedValue( + createAllDocuments([createDocumentEntry(passport)]), + ); + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('document-selector-wallet-badge')).toBeTruthy(); + }); + + fireEvent.press( + getByTestId('document-selector-wallet-badge-pressable'), + ); + + await waitFor(() => { + expect(getByTestId('document-selector-wallet-modal')).toBeTruthy(); + expect( + getByTestId('document-selector-wallet-modal-full-address'), + ).toBeTruthy(); + }); + }); + it('loads and displays all documents from catalog', async () => { const passport = createMetadata({ id: 'doc-1', @@ -367,57 +456,6 @@ describe('DocumentSelectorForProvingScreen', () => { }); }); - it('unregistered documents are selectable for proving', async () => { - const unregisteredPassport = createMetadata({ - id: 'doc-1', - documentType: 'us', - isRegistered: false, - }); - const catalog: DocumentCatalog = { - documents: [unregisteredPassport], - selectedDocumentId: 'doc-1', - }; - - mockLoadDocumentCatalog.mockResolvedValue(catalog); - mockGetAllDocuments.mockResolvedValue( - createAllDocuments([createDocumentEntry(unregisteredPassport)]), - ); - - const { getByTestId } = render(); - - await waitFor(() => { - // Unregistered documents should be selectable - expect( - getByTestId('document-selector-action-bar-approve').props.disabled, - ).toBe(false); - }); - }); - - it('approve button is enabled when valid document selected', async () => { - const validPassport = createMetadata({ - id: 'doc-1', - documentType: 'us', - isRegistered: true, - }); - const catalog: DocumentCatalog = { - documents: [validPassport], - selectedDocumentId: 'doc-1', - }; - - mockLoadDocumentCatalog.mockResolvedValue(catalog); - mockGetAllDocuments.mockResolvedValue( - createAllDocuments([createDocumentEntry(validPassport)]), - ); - - const { getByTestId } = render(); - - await waitFor(() => { - expect( - getByTestId('document-selector-action-bar-approve').props.disabled, - ).toBe(false); - }); - }); - it('selecting a different document updates selection state', async () => { const passport = createMetadata({ id: 'doc-1', diff --git a/app/tests/src/screens/verification/ProvingScreenRouter.test.tsx b/app/tests/src/screens/verification/ProvingScreenRouter.test.tsx new file mode 100644 index 000000000..93f4671e5 --- /dev/null +++ b/app/tests/src/screens/verification/ProvingScreenRouter.test.tsx @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useNavigation } from '@react-navigation/native'; +import { fireEvent, render, waitFor } from '@testing-library/react-native'; + +import type { + DocumentCatalog, + DocumentMetadata, + IDDocument, +} from '@selfxyz/common/utils/types'; +import { + isDocumentValidForProving, + pickBestDocumentToSelect, +} from '@selfxyz/mobile-sdk-alpha'; + +import { ProvingScreenRouter } from '@/screens/verification/ProvingScreenRouter'; +import { usePassport } from '@/providers/passportDataProvider'; +import { useSettingStore } from '@/stores/settingStore'; + +jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ + isDocumentValidForProving: jest.fn(), + pickBestDocumentToSelect: jest.fn(), +})); + +jest.mock('@/providers/passportDataProvider', () => ({ + usePassport: jest.fn(), +})); + +jest.mock('@/stores/settingStore', () => ({ + useSettingStore: jest.fn(), +})); + +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; +const mockIsDocumentValidForProving = + isDocumentValidForProving as jest.MockedFunction< + typeof isDocumentValidForProving + >; +const mockPickBestDocumentToSelect = + pickBestDocumentToSelect as jest.MockedFunction< + typeof pickBestDocumentToSelect + >; +const mockUsePassport = usePassport as jest.MockedFunction; +const mockUseSettingStore = + useSettingStore as jest.MockedFunction; + +const mockReplace = jest.fn(); +const mockLoadDocumentCatalog = jest.fn(); +const mockGetAllDocuments = jest.fn(); +const mockSetSelectedDocument = jest.fn(); + +type MockDocumentEntry = { + metadata: DocumentMetadata; + data: IDDocument; +}; + +const createMetadata = ( + overrides: Partial & { id: string }, +): DocumentMetadata => ({ + id: overrides.id, + documentType: overrides.documentType ?? 'us', + documentCategory: overrides.documentCategory ?? 'passport', + data: overrides.data ?? 'mock-data', + mock: overrides.mock ?? false, + isRegistered: overrides.isRegistered, + registeredAt: overrides.registeredAt, +}); + +const createDocumentEntry = ( + metadata: DocumentMetadata, + expiryDateSlice?: string, +): MockDocumentEntry => ({ + metadata, + data: { + documentType: metadata.documentType as any, + documentCategory: metadata.documentCategory as any, + mock: metadata.mock, + expiryDateSlice, + } as unknown as IDDocument, +}); + +const createAllDocuments = (entries: MockDocumentEntry[]) => + entries.reduce< + Record + >((acc, entry) => { + acc[entry.metadata.id] = { + data: entry.data, + metadata: entry.metadata, + }; + return acc; + }, {}); + +describe('ProvingScreenRouter', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseNavigation.mockReturnValue({ replace: mockReplace } as any); + + mockUsePassport.mockReturnValue({ + loadDocumentCatalog: mockLoadDocumentCatalog, + getAllDocuments: mockGetAllDocuments, + setSelectedDocument: mockSetSelectedDocument, + } as any); + + mockUseSettingStore.mockReturnValue({ + skipDocumentSelector: false, + skipDocumentSelectorIfSingle: false, + } as any); + + mockIsDocumentValidForProving.mockImplementation( + (_metadata, documentData) => + (documentData as { expiryDateSlice?: string } | undefined) + ?.expiryDateSlice !== 'expired', + ); + }); + + it('routes to DocumentDataNotFound when no valid documents exist', async () => { + const passport = createMetadata({ + id: 'doc-1', + documentType: 'us', + isRegistered: true, + }); + const catalog: DocumentCatalog = { + documents: [passport], + }; + + mockLoadDocumentCatalog.mockResolvedValue(catalog); + mockGetAllDocuments.mockResolvedValue( + createAllDocuments([createDocumentEntry(passport, 'expired')]), + ); + + render(); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('DocumentDataNotFound'); + }); + }); + + it('auto-selects and routes to Prove when skipping the selector', async () => { + const passport = createMetadata({ + id: 'doc-1', + documentType: 'us', + isRegistered: true, + }); + const catalog: DocumentCatalog = { + documents: [passport], + }; + + mockUseSettingStore.mockReturnValue({ + skipDocumentSelector: true, + skipDocumentSelectorIfSingle: false, + } as any); + + mockLoadDocumentCatalog.mockResolvedValue(catalog); + mockGetAllDocuments.mockResolvedValue( + createAllDocuments([createDocumentEntry(passport)]), + ); + mockPickBestDocumentToSelect.mockReturnValue('doc-1'); + + render(); + + await waitFor(() => { + expect(mockSetSelectedDocument).toHaveBeenCalledWith('doc-1'); + expect(mockReplace).toHaveBeenCalledWith('Prove'); + }); + }); + + it('routes to the document selector when skipping is disabled', async () => { + const passport = createMetadata({ + id: 'doc-1', + documentType: 'us', + isRegistered: true, + }); + const catalog: DocumentCatalog = { + documents: [passport], + }; + + mockLoadDocumentCatalog.mockResolvedValue(catalog); + mockGetAllDocuments.mockResolvedValue( + createAllDocuments([createDocumentEntry(passport)]), + ); + + render(); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('DocumentSelectorForProving'); + }); + }); + + it('retries loading when document routing fails', async () => { + const passport = createMetadata({ + id: 'doc-1', + documentType: 'us', + isRegistered: true, + }); + const catalog: DocumentCatalog = { + documents: [passport], + }; + + mockLoadDocumentCatalog + .mockRejectedValueOnce(new Error('failure')) + .mockResolvedValueOnce(catalog); + mockGetAllDocuments + .mockResolvedValueOnce({}) + .mockResolvedValueOnce( + createAllDocuments([createDocumentEntry(passport)]), + ); + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('proving-router-error')).toBeTruthy(); + }); + + fireEvent.press(getByTestId('proving-router-retry')); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('DocumentSelectorForProving'); + }); + }); +});