diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index 2262c3301..a5a77be84 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -162,6 +162,7 @@ "zustand": "^4.5.2" }, "devDependencies": { + "@openpassport/zk-kit-lean-imt": "^0.0.6", "@testing-library/react": "^14.1.2", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", @@ -177,6 +178,7 @@ "eslint-plugin-sort-exports": "^0.9.1", "jsdom": "^25.0.1", "lottie-react-native": "7.2.2", + "poseidon-lite": "^0.3.0", "prettier": "^3.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts b/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts new file mode 100644 index 000000000..28b7b77d5 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts @@ -0,0 +1,920 @@ +// 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 { poseidon2 } from 'poseidon-lite'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { AadhaarData, PassportData } from '@selfxyz/common'; +import { + generateCommitment, + genMockIdDoc, + getCircuitNameFromPassportData, + getLeafDscTree, + isMRZDocument, +} from '@selfxyz/common/utils'; +import * as commonUtils from '@selfxyz/common/utils'; +import { generateCommitmentInAppAadhaar } from '@selfxyz/common/utils/passports/validate'; +import { AttestationIdHex } from '@selfxyz/common/utils/types'; + +import { PassportEvents, ProofEvents } from '../../src/constants/analytics'; +import * as documentUtils from '../../src/documents/utils'; +import { useProvingStore } from '../../src/proving/provingMachine'; +import { fetchAllTreesAndCircuits } from '../../src/stores'; +import type { SelfClient } from '../../src/types/public'; +import { actorMock } from './actorMock'; + +import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; + +vi.mock('xstate', async () => { + const actual = await vi.importActual('xstate'); + return { + ...actual, + createActor: vi.fn(() => actorMock), + }; +}); + +vi.mock('../../src/documents/utils', async () => { + const actual = await vi.importActual('../../src/documents/utils'); + return { + ...actual, + loadSelectedDocument: vi.fn(), + storePassportData: vi.fn(), + clearPassportData: vi.fn(), + reStorePassportDataWithRightCSCA: vi.fn(), + markCurrentDocumentAsRegistered: vi.fn(), + }; +}); + +vi.mock('../../src/stores', async () => { + const actual = await vi.importActual('../../src/stores'); + return { + ...actual, + fetchAllTreesAndCircuits: vi.fn(), + }; +}); + +const createCommitmentTree = (commitments: string[]) => { + const tree = new LeanIMT((a, b) => poseidon2([a, b])); + if (commitments.length > 0) { + tree.insertMany(commitments.map(commitment => BigInt(commitment))); + } + return tree.export(); +}; + +const createDscTree = (leaves: string[]) => createCommitmentTree(leaves); + +const buildPassportFixture = (): PassportData => + ({ + mrz: 'P; + publicKeys?: string[]; +}) => ({ + passport: { + commitment_tree: commitmentTree, + dsc_tree: dscTree, + csca_tree: null, + deployed_circuits: deployedCircuits, + circuits_dns_mapping: null, + alternative_csca: alternativeCsca ?? {}, + ofac_trees: null, + fetch_all: vi.fn(), + fetch_deployed_circuits: vi.fn(), + fetch_circuits_dns_mapping: vi.fn(), + fetch_csca_tree: vi.fn(), + fetch_dsc_tree: vi.fn(), + fetch_identity_tree: vi.fn(), + fetch_alternative_csca: vi.fn(), + fetch_ofac_trees: vi.fn(), + }, + id_card: { + commitment_tree: commitmentTree, + dsc_tree: dscTree, + csca_tree: null, + deployed_circuits: deployedCircuits, + circuits_dns_mapping: null, + alternative_csca: alternativeCsca ?? {}, + ofac_trees: null, + fetch_all: vi.fn(), + fetch_deployed_circuits: vi.fn(), + fetch_circuits_dns_mapping: vi.fn(), + fetch_csca_tree: vi.fn(), + fetch_dsc_tree: vi.fn(), + fetch_identity_tree: vi.fn(), + fetch_alternative_csca: vi.fn(), + fetch_ofac_trees: vi.fn(), + }, + aadhaar: { + commitment_tree: commitmentTree, + dsc_tree: null, + csca_tree: null, + deployed_circuits: deployedCircuits, + circuits_dns_mapping: null, + public_keys: publicKeys ?? [], + ofac_trees: null, + fetch_all: vi.fn(), + fetch_deployed_circuits: vi.fn(), + fetch_circuits_dns_mapping: vi.fn(), + fetch_csca_tree: vi.fn(), + fetch_dsc_tree: vi.fn(), + fetch_identity_tree: vi.fn(), + fetch_alternative_csca: vi.fn(), + fetch_ofac_trees: vi.fn(), + }, +}); + +const createSelfClient = (protocolState: ReturnType) => + ({ + trackEvent: vi.fn(), + logProofEvent: vi.fn(), + emit: vi.fn(), + getPrivateKey: vi.fn().mockResolvedValue('123456789'), + getProvingState: () => useProvingStore.getState(), + getSelfAppState: () => ({ selfApp: null }), + getProtocolState: () => protocolState, + }) as unknown as SelfClient; + +describe('parseIDDocument', () => { + const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument); + const storePassportDataMock = vi.mocked(documentUtils.storePassportData); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('parses passport data successfully and updates state with parsed result', async () => { + const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + const getSKIPEMSpy = vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + const state = useProvingStore.getState(); + expect(getSKIPEMSpy).toHaveBeenCalledWith('staging'); + expect(storePassportDataMock).toHaveBeenCalledWith(selfClient, state.passportData); + if (state.passportData && isMRZDocument(state.passportData)) { + expect(state.passportData.passportMetadata).toBeDefined(); + } + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_SUCCESS' }); + if (state.passportData && isMRZDocument(state.passportData)) { + expect(selfClient.trackEvent).toHaveBeenCalledWith( + PassportEvents.PASSPORT_PARSED, + expect.objectContaining({ + success: true, + country_code: state.passportData.passportMetadata?.countryCode, + }), + ); + } + }); + + it('handles missing passport data with PARSE_ERROR and analytics event', async () => { + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: genMockIdDoc({ idType: 'mock_passport' }) } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: null }); + + await useProvingStore.getState().parseIDDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(PassportEvents.PASSPORT_PARSE_FAILED, { + error: 'PassportData is not available', + }); + }); + + it('surfaces parsing failures when the DSC cannot be parsed', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc: 'invalid-certificate', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith( + PassportEvents.PASSPORT_PARSE_FAILED, + expect.objectContaining({ + error: expect.stringMatching(/asn\\.1|parsing/i), + }), + ); + }); + + it('continues when DSC metadata cannot be read and logs empty dsc payload', async () => { + const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData; + let metadataProxy: PassportData['passportMetadata']; + Object.defineProperty(passportData, 'passportMetadata', { + get() { + return metadataProxy; + }, + set(value) { + metadataProxy = new Proxy(value, { + get(target, prop) { + if (prop === 'dsc') { + throw new Error('dsc parse failed'); + } + return target[prop as keyof typeof target]; + }, + }); + }, + configurable: true, + }); + + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + const parsedEvent = vi + .mocked(selfClient.trackEvent) + .mock.calls.find(([event]) => event === PassportEvents.PASSPORT_PARSED)?.[1]; + + expect(parsedEvent).toEqual( + expect.objectContaining({ + dsc: {}, + }), + ); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_SUCCESS' }); + }); + + it('emits PARSE_ERROR when storing parsed passport data fails', async () => { + const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + storePassportDataMock.mockRejectedValue(new Error('storage unavailable')); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(PassportEvents.PASSPORT_PARSE_FAILED, { + error: 'storage unavailable', + }); + }); +}); + +describe('startFetchingData', () => { + const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument); + const fetchAllTreesMock = vi.mocked(fetchAllTreesAndCircuits); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('fetches trees and circuits for passport documents', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc_parsed: { authorityKeyIdentifier: 'KEY123' } as any, + documentCategory: 'passport', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, env: 'prod' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(fetchAllTreesMock).toHaveBeenCalledWith(selfClient, 'passport', 'prod', 'KEY123'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_SUCCESS); + }); + + it('fetches trees and circuits for id cards', async () => { + const idCardData = { + ...(genMockIdDoc({ idType: 'mock_id_card' }) as PassportData), + dsc_parsed: { authorityKeyIdentifier: 'IDKEY' } as any, + documentCategory: 'id_card', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: idCardData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: idCardData, env: 'stg' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(fetchAllTreesMock).toHaveBeenCalledWith(selfClient, 'id_card', 'stg', 'IDKEY'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + }); + + it('fetches aadhaar protocol data via aadhaar fetcher', async () => { + const aadhaarData = genMockIdDoc({ idType: 'mock_aadhaar' }) as AadhaarData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: aadhaarData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: aadhaarData, env: 'prod' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(protocolState.aadhaar.fetch_all).toHaveBeenCalledWith('prod'); + expect(fetchAllTreesMock).not.toHaveBeenCalled(); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + }); + + it('emits FETCH_ERROR when passport data is missing', async () => { + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: genMockIdDoc({ idType: 'mock_passport' }) } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: null }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'PassportData is not available', + }); + }); + + it('emits FETCH_ERROR when DSC data is missing for passports', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc_parsed: undefined, + documentCategory: 'passport', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, env: 'stg' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'Missing parsed DSC in passport data', + }); + }); + + it('emits FETCH_ERROR when protocol fetch fails', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc_parsed: { authorityKeyIdentifier: 'KEY123' } as any, + documentCategory: 'passport', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + fetchAllTreesMock.mockRejectedValue(new Error('network down')); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, env: 'prod' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'network down', + }); + }); +}); + +describe('validatingDocument', () => { + const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument); + const clearPassportDataMock = vi.mocked(documentUtils.clearPassportData); + const reStorePassportDataWithRightCSCMock = vi.mocked(documentUtils.reStorePassportDataWithRightCSCA); + const markCurrentDocumentAsRegisteredMock = vi.mocked(documentUtils.markCurrentDocumentAsRegistered); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('clears data and emits PASSPORT_NOT_SUPPORTED when document is unsupported', async () => { + const passportData = buildPassportFixture(); + const unsupportedCircuits = { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: unsupportedCircuits, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret: '123456789', circuitType: 'register' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(clearPassportDataMock).toHaveBeenCalledWith(selfClient); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PASSPORT_NOT_SUPPORTED' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith( + PassportEvents.COMING_SOON, + expect.objectContaining({ status: 'registration_circuit_not_supported' }), + ); + }); + + it('validates disclose when the user is registered', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const commitment = generateCommitment(secret, AttestationIdHex.passport, passportData); + const commitmentTree = createCommitmentTree([commitment]); + + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree, + dscTree: null, + deployedCircuits, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'disclose'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'disclose' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_SUCCESS' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.VALIDATION_SUCCESS); + }); + + it('emits PASSPORT_DATA_NOT_FOUND when disclose document is not registered', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const commitmentTree = createCommitmentTree([]); + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree, + dscTree: null, + deployedCircuits, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'disclose'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'disclose' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PASSPORT_DATA_NOT_FOUND' }); + }); + + it('restores data when aadhaar is already registered with alternative keys', async () => { + const aadhaarData = genMockIdDoc({ idType: 'mock_aadhaar' }) as AadhaarData; + const secret = '123456789'; + const { commitment_list: commitmentList } = generateCommitmentInAppAadhaar( + secret, + AttestationIdHex.aadhaar, + aadhaarData, + { + public_key_0: aadhaarData.publicKey, + }, + ); + const commitmentTree = createCommitmentTree([commitmentList[0]]); + const deployedCircuits = { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree, + dscTree: null, + deployedCircuits, + publicKeys: [aadhaarData.publicKey], + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: aadhaarData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: aadhaarData, secret, circuitType: 'register' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(reStorePassportDataWithRightCSCMock).toHaveBeenCalledWith(selfClient, aadhaarData, aadhaarData.publicKey); + expect(markCurrentDocumentAsRegisteredMock).toHaveBeenCalledWith(selfClient); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'ALREADY_REGISTERED' }); + }); + + it('routes to account recovery when nullifier is on chain', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree: createCommitmentTree([]), + dscTree: null, + deployedCircuits, + alternativeCsca: {}, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ data: true }), + } as Response), + ); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'register' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'ACCOUNT_RECOVERY_CHOICE' }); + + globalThis.fetch = originalFetch; + }); + + it('switches to register circuit when DSC is already in the tree', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + const dscLeaf = getLeafDscTree(passportData.dsc_parsed!, passportData.csca_parsed!); + const dscTree = createDscTree([dscLeaf]); + + const protocolState = buildProtocolState({ + commitmentTree: createCommitmentTree([]), + dscTree, + deployedCircuits, + alternativeCsca: {}, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ data: false }), + } as Response), + ); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'dsc' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(useProvingStore.getState().circuitType).toBe('register'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_SUCCESS' }); + + globalThis.fetch = originalFetch; + }); + + it('emits VALIDATION_ERROR when validation throws', async () => { + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: buildPassportFixture() } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: null }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.VALIDATION_FAILED, { + message: 'PassportData is not available', + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index d9e270a75..85f78edea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8700,6 +8700,7 @@ __metadata: resolution: "@selfxyz/mobile-sdk-alpha@workspace:packages/mobile-sdk-alpha" dependencies: "@babel/runtime": "npm:^7.28.3" + "@openpassport/zk-kit-lean-imt": "npm:^0.0.6" "@selfxyz/common": "workspace:^" "@selfxyz/euclid": "npm:^0.6.1" "@testing-library/react": "npm:^14.1.2" @@ -8719,6 +8720,7 @@ __metadata: jsdom: "npm:^25.0.1" lottie-react-native: "npm:7.2.2" node-forge: "npm:^1.3.1" + poseidon-lite: "npm:^0.3.0" prettier: "npm:^3.5.3" react: "npm:^18.3.1" react-dom: "npm:^18.3.1"