diff --git a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts index e0ea3d361..44a610263 100644 --- a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts +++ b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts @@ -1045,17 +1045,30 @@ export const useProvingStore = create((set, get) => { set({ passportData, secret, env }); set({ circuitType }); - // Skip parsing for disclosure if passport is already parsed + // Only skip parsing when the document has already been parsed for non-DSC circuits. // Re-parsing would overwrite the alternative CSCA used during registration and is unnecessary - // skip also the register circuit as the passport already got parsed in during the dsc step - console.log('circuitType', circuitType); - if (circuitType !== 'dsc') { - console.log('skipping id document parsing'); - actor.send({ type: 'FETCH_DATA' }); - selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); - } else { + // for already parsed passports or ID cards. + // Aadhaar and KYC documents do not require DSC parsing at all. + const needsDscParsing = + passportData.documentCategory === 'passport' || passportData.documentCategory === 'id_card'; + const hasParsedDsc = needsDscParsing && Boolean(passportData.dsc_parsed?.authorityKeyIdentifier); + + if (circuitType === 'dsc' && !needsDscParsing) { + console.error(`DSC circuit is not supported for ${passportData.documentCategory} documents`); + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { + message: `DSC circuit not supported for ${passportData.documentCategory}`, + }); + actor.send({ type: 'ERROR' }); + return; + } + + const shouldParseDocument = circuitType === 'dsc' || (needsDscParsing && !hasParsedDsc); + + if (shouldParseDocument) { actor.send({ type: 'PARSE_ID_DOCUMENT' }); selfClient.trackEvent(ProofEvents.PARSE_ID_DOCUMENT_STARTED); + } else { + actor.send({ type: 'FETCH_DATA' }); } }, @@ -1158,14 +1171,15 @@ export const useProvingStore = create((set, get) => { switch (passportData.documentCategory) { case 'passport': case 'id_card': - if (!passportData?.dsc_parsed) { - selfClient.logProofEvent('error', 'Missing parsed DSC', context, { + if (!passportData?.dsc_parsed?.authorityKeyIdentifier) { + const docType = passportData.documentCategory; + selfClient.logProofEvent('error', `Missing parsed DSC in ${docType} data`, context, { failure: 'PROOF_FAILED_DATA_FETCH', duration_ms: Date.now() - startTime, }); - console.error('Missing parsed DSC in passport data'); + console.error(`Missing parsed DSC in ${docType} data`); selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { - message: 'Missing parsed DSC in passport data', + message: `Missing parsed DSC in ${docType} data`, }); actor!.send({ type: 'FETCH_ERROR' }); return; diff --git a/packages/mobile-sdk-alpha/tests/proving/provingMachine.startFetchingData.test.ts b/packages/mobile-sdk-alpha/tests/proving/provingMachine.startFetchingData.test.ts index e4e641e44..cc805cbdb 100644 --- a/packages/mobile-sdk-alpha/tests/proving/provingMachine.startFetchingData.test.ts +++ b/packages/mobile-sdk-alpha/tests/proving/provingMachine.startFetchingData.test.ts @@ -6,6 +6,7 @@ import type { SelfClient } from '../../src'; import { ProofEvents } from '../../src/constants/analytics'; import * as documentUtils from '../../src/documents/utils'; import { useProvingStore } from '../../src/proving/provingMachine'; +import { fetchAllTreesAndCircuits } from '../../src/stores'; import { useProtocolStore } from '../../src/stores/protocolStore'; import { useSelfAppStore } from '../../src/stores/selfAppStore'; import { actorMock } from './actorMock'; @@ -36,46 +37,359 @@ vitest.mock('../../src/stores', async () => { }; }); -describe('startFetchingData', () => { - let mockSelfClient: SelfClient; - beforeEach(async () => { +const createMockSelfClient = () => + ({ + getPrivateKey: vitest.fn().mockResolvedValue('secret'), + trackEvent: vitest.fn(), + logProofEvent: vitest.fn(), + getSelfAppState: () => useSelfAppStore.getState(), + getProvingState: () => useProvingStore.getState(), + getProtocolState: () => useProtocolStore.getState(), + }) as unknown as SelfClient; + +function mockDocument(data: Record) { + vitest.spyOn(documentUtils, 'loadSelectedDocument').mockResolvedValue({ data } as any); +} + +describe('init parsing decision', () => { + beforeEach(() => { vitest.clearAllMocks(); - - const loadSelectedDocumentSpy = vitest.spyOn(documentUtils, 'loadSelectedDocument'); - - loadSelectedDocumentSpy.mockResolvedValue({ - data: { - documentCategory: 'passport', - mock: false, - dsc_parsed: { authorityKeyIdentifier: 'key' } as any, - }, - } as any); - - // Create mock selfClient - mockSelfClient = { - getPrivateKey: vitest.fn().mockResolvedValue('secret'), // or mock-secret? - trackEvent: vitest.fn(), - logProofEvent: vitest.fn(), - getSelfAppState: () => useSelfAppStore.getState(), - getProvingState: () => useProvingStore.getState(), - getProtocolState: () => useProtocolStore.getState(), - } as unknown as SelfClient; - - await useProvingStore.getState().init(mockSelfClient, 'register'); - actorMock.send.mockClear(); - - useProvingStore.setState({ - passportData: { documentCategory: 'passport', mock: false }, - env: 'prod', - } as any); + useProvingStore.setState({ circuitType: null, passportData: null, env: null }); }); - it('emits FETCH_ERROR when dsc_parsed is missing', async () => { + describe('passport', () => { + it('skips parsing for disclose when dsc_parsed has authorityKeyIdentifier', async () => { + const client = createMockSelfClient(); + mockDocument({ + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'key' }, + }); + + await useProvingStore.getState().init(client, 'disclose'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(client.trackEvent).not.toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED); + }); + + it('parses for disclose when dsc_parsed is missing', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'passport', mock: false }); + + await useProvingStore.getState().init(client, 'disclose'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED); + }); + + it('parses when dsc_parsed exists but authorityKeyIdentifier is missing', async () => { + const client = createMockSelfClient(); + mockDocument({ + documentCategory: 'passport', + mock: false, + dsc_parsed: { issuer: 'some-issuer' }, + }); + + await useProvingStore.getState().init(client, 'disclose'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + }); + + it('skips parsing for register when dsc_parsed has authorityKeyIdentifier', async () => { + const client = createMockSelfClient(); + mockDocument({ + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'key' }, + }); + + await useProvingStore.getState().init(client, 'register'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + }); + + it('parses for register when dsc_parsed is missing', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'passport', mock: false }); + + await useProvingStore.getState().init(client, 'register'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + }); + }); + + describe('id_card', () => { + it('skips parsing for disclose when dsc_parsed has authorityKeyIdentifier', async () => { + const client = createMockSelfClient(); + mockDocument({ + documentCategory: 'id_card', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'key' }, + }); + + await useProvingStore.getState().init(client, 'disclose'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + }); + + it('parses when dsc_parsed exists but authorityKeyIdentifier is missing', async () => { + const client = createMockSelfClient(); + mockDocument({ + documentCategory: 'id_card', + mock: false, + dsc_parsed: { issuer: 'some-issuer' }, + }); + + await useProvingStore.getState().init(client, 'disclose'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + }); + + it('parses for register when dsc_parsed is missing', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'id_card', mock: false }); + + await useProvingStore.getState().init(client, 'register'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + }); + }); + + describe('dsc circuit', () => { + it('always parses even when dsc_parsed already exists', async () => { + const client = createMockSelfClient(); + mockDocument({ + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'key' }, + }); + + await useProvingStore.getState().init(client, 'dsc'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED); + }); + + it('parses when dsc_parsed is missing', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'passport', mock: false }); + + await useProvingStore.getState().init(client, 'dsc'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + }); + + it('rejects dsc circuit for aadhaar documents', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'aadhaar', mock: false }); + + await useProvingStore.getState().init(client, 'dsc'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'ERROR' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PROOF_FAILED, { + message: 'DSC circuit not supported for aadhaar', + }); + }); + + it('rejects dsc circuit for kyc documents', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'kyc', mock: false }); + + await useProvingStore.getState().init(client, 'dsc'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'ERROR' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PROOF_FAILED, { + message: 'DSC circuit not supported for kyc', + }); + }); + }); + + describe('aadhaar', () => { + it('skips parsing for disclose (does not require DSC)', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'aadhaar', mock: false }); + + await useProvingStore.getState().init(client, 'disclose'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(client.trackEvent).not.toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED); + }); + + it('skips parsing for register (does not require DSC)', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'aadhaar', mock: false }); + + await useProvingStore.getState().init(client, 'register'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + }); + }); + + describe('kyc', () => { + it('skips parsing for disclose (does not require DSC)', async () => { + const client = createMockSelfClient(); + mockDocument({ documentCategory: 'kyc', mock: false }); + + await useProvingStore.getState().init(client, 'disclose'); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' }); + expect(client.trackEvent).not.toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED); + }); + }); +}); + +describe('startFetchingData', () => { + let mockSelfClient: SelfClient; + + /** Run init() with a valid passport so the actor is ready, then clear mocks + * and set passportData to whatever the individual test needs. */ + async function setupForFetch(passportData: Record) { + vitest.clearAllMocks(); + mockDocument({ + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'key' }, + }); + mockSelfClient = createMockSelfClient(); + await useProvingStore.getState().init(mockSelfClient, 'register'); + actorMock.send.mockClear(); + (mockSelfClient.trackEvent as ReturnType).mockClear(); + (mockSelfClient.logProofEvent as ReturnType).mockClear(); + useProvingStore.setState({ passportData, env: 'prod' } as any); + } + + it('emits FETCH_ERROR when dsc_parsed is missing for passport', async () => { + await setupForFetch({ documentCategory: 'passport', mock: false }); + await useProvingStore.getState().startFetchingData(mockSelfClient); + expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_STARTED); expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { message: 'Missing parsed DSC in passport data', }); + expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + }); + + it('emits FETCH_ERROR when dsc_parsed exists but authorityKeyIdentifier is missing', async () => { + await setupForFetch({ + documentCategory: 'passport', + mock: false, + dsc_parsed: { issuer: 'some-issuer' }, + }); + + await useProvingStore.getState().startFetchingData(mockSelfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'Missing parsed DSC in passport data', + }); + }); + + it('emits FETCH_ERROR when dsc_parsed is missing for id_card', async () => { + await setupForFetch({ documentCategory: 'id_card', mock: false }); + + await useProvingStore.getState().startFetchingData(mockSelfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'Missing parsed DSC in id_card data', + }); + }); + + it('calls fetchAllTreesAndCircuits for passport with valid dsc_parsed', async () => { + await setupForFetch({ + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'test-aki-123' }, + }); + + await useProvingStore.getState().startFetchingData(mockSelfClient); + + expect(fetchAllTreesAndCircuits).toHaveBeenCalledWith(mockSelfClient, 'passport', 'prod', 'test-aki-123'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_SUCCESS); + }); + + it('calls fetchAllTreesAndCircuits for id_card with valid dsc_parsed', async () => { + await setupForFetch({ + documentCategory: 'id_card', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'card-aki-456' }, + }); + + await useProvingStore.getState().startFetchingData(mockSelfClient); + + expect(fetchAllTreesAndCircuits).toHaveBeenCalledWith(mockSelfClient, 'id_card', 'prod', 'card-aki-456'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + }); + + it('calls aadhaar.fetch_all for aadhaar documents', async () => { + const mockFetchAll = vitest.fn().mockResolvedValue(undefined); + const client = createMockSelfClient(); + (client as any).getProtocolState = () => ({ + aadhaar: { fetch_all: mockFetchAll }, + }); + + // Bootstrap actor via init + vitest.clearAllMocks(); + mockDocument({ documentCategory: 'aadhaar', mock: false }); + (client as any).getPrivateKey = vitest.fn().mockResolvedValue('secret'); + await useProvingStore.getState().init(client, 'disclose'); + actorMock.send.mockClear(); + (client.trackEvent as ReturnType).mockClear(); + + useProvingStore.setState({ + passportData: { documentCategory: 'aadhaar', mock: false }, + env: 'prod', + } as any); + + await useProvingStore.getState().startFetchingData(client); + + expect(mockFetchAll).toHaveBeenCalledWith('prod'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + expect(fetchAllTreesAndCircuits).not.toHaveBeenCalled(); + }); + + it('calls kyc.fetch_all for kyc documents', async () => { + const mockFetchAll = vitest.fn().mockResolvedValue(undefined); + const client = createMockSelfClient(); + (client as any).getProtocolState = () => ({ + kyc: { fetch_all: mockFetchAll }, + }); + + vitest.clearAllMocks(); + mockDocument({ documentCategory: 'kyc', mock: false }); + (client as any).getPrivateKey = vitest.fn().mockResolvedValue('secret'); + await useProvingStore.getState().init(client, 'disclose'); + actorMock.send.mockClear(); + (client.trackEvent as ReturnType).mockClear(); + + useProvingStore.setState({ + passportData: { documentCategory: 'kyc', mock: false }, + env: 'prod', + } as any); + + await useProvingStore.getState().startFetchingData(client); + + expect(mockFetchAll).toHaveBeenCalledWith('prod'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + expect(fetchAllTreesAndCircuits).not.toHaveBeenCalled(); }); });