diff --git a/app/src/hooks/useMockDataForm.ts b/app/src/hooks/useMockDataForm.ts new file mode 100644 index 000000000..6c85be7a4 --- /dev/null +++ b/app/src/hooks/useMockDataForm.ts @@ -0,0 +1,59 @@ +// 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 { useState } from 'react'; + +export const useMockDataForm = () => { + const [age, setAge] = useState(21); + const [expiryYears, setExpiryYears] = useState(5); + const [selectedCountry, setSelectedCountry] = useState('USA'); + const [selectedAlgorithm, setSelectedAlgorithm] = useState( + 'sha256 rsa 65537 2048', + ); + const [selectedDocumentType, setSelectedDocumentType] = useState< + 'mock_passport' | 'mock_id_card' + >('mock_passport'); + const [isInOfacList, setIsInOfacList] = useState(true); + + const resetFormValues = () => { + setAge(21); + setExpiryYears(5); + setIsInOfacList(true); + setSelectedDocumentType('mock_passport'); + setSelectedAlgorithm('sha256 rsa 65537 2048'); + setSelectedCountry('USA'); + }; + + const handleCountrySelect = (countryCode: string) => { + setSelectedCountry(countryCode); + }; + + const handleAlgorithmSelect = (algorithm: string) => { + setSelectedAlgorithm(algorithm); + }; + + const handleDocumentTypeSelect = ( + documentType: 'mock_passport' | 'mock_id_card', + ) => { + setSelectedDocumentType(documentType); + }; + + return { + age, + setAge, + expiryYears, + setExpiryYears, + selectedCountry, + handleCountrySelect, + selectedAlgorithm, + handleAlgorithmSelect, + selectedDocumentType, + handleDocumentTypeSelect, + isInOfacList, + setIsInOfacList, + resetFormValues, + }; +}; + +export default useMockDataForm; diff --git a/app/src/navigation/devTools.ts b/app/src/navigation/devTools.ts index a7606ff55..6d6b447e6 100644 --- a/app/src/navigation/devTools.ts +++ b/app/src/navigation/devTools.ts @@ -27,7 +27,7 @@ const devScreens = { CreateMock: { screen: MockDataScreen, options: { - title: 'Mock Passport', + title: 'Mock Document', headerStyle: { backgroundColor: black, }, diff --git a/app/src/screens/dev/MockDataScreen.tsx b/app/src/screens/dev/MockDataScreen.tsx index e3569d4c6..b8219f370 100644 --- a/app/src/screens/dev/MockDataScreen.tsx +++ b/app/src/screens/dev/MockDataScreen.tsx @@ -23,19 +23,17 @@ import { useNavigation } from '@react-navigation/native'; import { ChevronDown, Minus, Plus, X } from '@tamagui/lucide-icons'; import { countryCodes } from '@selfxyz/common/constants'; -import type { IdDocInput } from '@selfxyz/common/utils'; -import { getSKIPEM } from '@selfxyz/common/utils/csca'; import { - generateMockDSC, - genMockIdDoc, - initPassportDataParsing, -} from '@selfxyz/common/utils/passports'; -import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; + generateMockDocument, + signatureAlgorithmToStrictSignatureAlgorithm, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { MockDataEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { PrimaryButton } from '@/components/buttons/PrimaryButton'; import ButtonsContainer from '@/components/ButtonsContainer'; import { Caption } from '@/components/typography/Caption'; +import { useMockDataForm } from '@/hooks/useMockDataForm'; import SelfDevCard from '@/images/card-dev.svg'; import IdIcon from '@/images/icons/id_icon.svg'; import NoteIcon from '@/images/icons/note.svg'; @@ -61,92 +59,7 @@ const documentTypes = { mock_id_card: 'ID Card', }; -const signatureAlgorithmToStrictSignatureAlgorithm = { - 'sha256 rsa 65537 4096': ['sha256', 'sha256', 'rsa_sha256_65537_4096'], - 'sha1 rsa 65537 2048': ['sha1', 'sha1', 'rsa_sha1_65537_2048'], - 'sha256 brainpoolP256r1': [ - 'sha256', - 'sha256', - 'ecdsa_sha256_brainpoolP256r1_256', - ], - 'sha384 brainpoolP384r1': [ - 'sha384', - 'sha384', - 'ecdsa_sha384_brainpoolP384r1_384', - ], - 'sha384 secp384r1': ['sha384', 'sha384', 'ecdsa_sha384_secp384r1_384'], - 'sha256 rsa 65537 2048': ['sha256', 'sha256', 'rsa_sha256_65537_2048'], - 'sha256 rsa 3 2048': ['sha256', 'sha256', 'rsa_sha256_3_2048'], - 'sha256 rsa 65537 3072': ['sha256', 'sha256', 'rsa_sha256_65537_3072'], - 'sha256 rsa 3 4096': ['sha256', 'sha256', 'rsa_sha256_3_4096'], - 'sha384 rsa 65537 4096': ['sha384', 'sha384', 'rsa_sha384_65537_4096'], - 'sha512 rsa 65537 2048': ['sha512', 'sha512', 'rsa_sha512_65537_2048'], - 'sha512 rsa 65537 4096': ['sha512', 'sha512', 'rsa_sha512_65537_4096'], - 'sha1 rsa 65537 4096': ['sha1', 'sha1', 'rsa_sha1_65537_4096'], - 'sha256 rsapss 3 2048': ['sha256', 'sha256', 'rsapss_sha256_3_2048'], - 'sha256 rsapss 3 3072': ['sha256', 'sha256', 'rsapss_sha256_3_3072'], - 'sha256 rsapss 65537 3072': ['sha256', 'sha256', 'rsapss_sha256_65537_3072'], - 'sha256 rsapss 65537 4096': ['sha256', 'sha256', 'rsapss_sha256_65537_4096'], - 'sha384 rsapss 65537 2048': ['sha384', 'sha384', 'rsapss_sha384_65537_2048'], - 'sha384 rsapss 65537 3072': ['sha384', 'sha384', 'rsapss_sha384_65537_3072'], - 'sha512 rsapss 65537 2048': ['sha512', 'sha512', 'rsapss_sha512_65537_2048'], - 'sha512 rsapss 65537 4096': ['sha512', 'sha512', 'rsapss_sha512_65537_4096'], - 'sha1 secp256r1': ['sha1', 'sha1', 'ecdsa_sha1_secp256r1_256'], - 'sha224 secp224r1': ['sha224', 'sha224', 'ecdsa_sha224_secp224r1_224'], - 'sha256 secp256r1': ['sha256', 'sha256', 'ecdsa_sha256_secp256r1_256'], - 'sha256 secp384r1': ['sha256', 'sha256', 'ecdsa_sha256_secp384r1_384'], - 'sha1 brainpoolP224r1': ['sha1', 'sha1', 'ecdsa_sha1_brainpoolP224r1_224'], - 'sha1 brainpoolP256r1': ['sha1', 'sha1', 'ecdsa_sha1_brainpoolP256r1_256'], - 'sha224 brainpoolP224r1': [ - 'sha224', - 'sha224', - 'ecdsa_sha224_brainpoolP224r1_224', - ], - 'sha256 brainpoolP224r1': [ - 'sha256', - 'sha256', - 'ecdsa_sha256_brainpoolP224r1_224', - ], - 'sha384 brainpoolP256r1': [ - 'sha384', - 'sha384', - 'ecdsa_sha384_brainpoolP256r1_256', - ], - 'sha512 brainpoolP256r1': [ - 'sha512', - 'sha512', - 'ecdsa_sha512_brainpoolP256r1_256', - ], - 'sha512 brainpoolP384r1': [ - 'sha512', - 'sha512', - 'ecdsa_sha512_brainpoolP384r1_384', - ], - 'sha512 poland': ['sha512', 'sha512', 'rsa_sha256_65537_4096'], - 'not existing': ['sha512', 'sha384', 'rsa_sha256_65537_4096'], -} as const; - -const formatDateToYYMMDD = (date: Date): string => { - return ( - date.toISOString().slice(2, 4) + - date.toISOString().slice(5, 7) + - date.toISOString().slice(8, 10) - ).toString(); -}; - -const getBirthDateFromAge = (age: number): string => { - const date = new Date(); - date.setFullYear(date.getFullYear() - age); - return formatDateToYYMMDD(date); -}; - -const getExpiryDateFromYears = (years: number): string => { - const date = new Date(); - date.setFullYear(date.getFullYear() + years); - return formatDateToYYMMDD(date); -}; - -const MockPassportTitleCard = () => { +const MockDocumentTitleCard = () => { return ( { - Generate mock passport data + Generate mock document data - Configure data parameters to generate a mock passport for testing + Configure data parameters to generate a mock document for testing purposes on the Self Protocol. @@ -195,7 +108,7 @@ const HeroBanner = () => { /> - + = ({ const MockDataScreen: React.FC = () => { const { trackEvent } = useSelfClient(); const navigation = useNavigation(); - const [age, setAge] = useState(21); - const [expiryYears, setExpiryYears] = useState(5); + const { + age, + setAge, + expiryYears, + setExpiryYears, + selectedCountry, + handleCountrySelect, + selectedAlgorithm, + handleAlgorithmSelect, + selectedDocumentType, + handleDocumentTypeSelect, + isInOfacList, + setIsInOfacList, + resetFormValues, + } = useMockDataForm(); const [isGenerating, setIsGenerating] = useState(false); - const [isInOfacList, setIsInOfacList] = useState(true); - const [selectedDocumentType, setSelectedDocumentType] = useState< - 'mock_passport' | 'mock_id_card' - >('mock_passport'); - const [selectedCountry, setSelectedCountry] = useState('USA'); - const [selectedAlgorithm, setSelectedAlgorithm] = useState( - 'sha256 rsa 65537 2048', - ); const [isCountrySheetOpen, setCountrySheetOpen] = useState(false); const [isAlgorithmSheetOpen, setAlgorithmSheetOpen] = useState(false); const [isDocumentTypeSheetOpen, setDocumentTypeSheetOpen] = useState(false); - const resetFormValues = () => { - setAge(21); - setExpiryYears(5); - setIsInOfacList(true); - setSelectedDocumentType('mock_passport'); - setSelectedAlgorithm('sha256 rsa 65537 2048'); - setSelectedCountry('USA'); - }; - - const handleCountrySelect = (countryCode: string) => { - setSelectedCountry(countryCode); - setCountrySheetOpen(false); - }; - - const handleAlgorithmSelect = (algorithm: string) => { - setSelectedAlgorithm(algorithm); - setAlgorithmSheetOpen(false); - }; - - const handleDocumentTypeSelect = ( - documentType: 'mock_passport' | 'mock_id_card', - ) => { - setSelectedDocumentType(documentType); - setDocumentTypeSheetOpen(false); - }; - const handleGenerate = useCallback(async () => { setIsGenerating(true); try { - const randomPassportNumber = Math.random() - .toString(36) - .substring(2, 11) - .replace(/[^a-z0-9]/gi, '') - .toUpperCase(); - const algorithmMapping = - signatureAlgorithmToStrictSignatureAlgorithm[ - selectedAlgorithm as keyof typeof signatureAlgorithmToStrictSignatureAlgorithm - ]; - const dgHashAlgo = algorithmMapping[0]; - const eContentHashAlgo = algorithmMapping[1]; - const signatureTypeForGeneration = algorithmMapping[2]; - - const idDocInput: Partial = { - nationality: selectedCountry as IdDocInput['nationality'], - idType: selectedDocumentType as IdDocInput['idType'], - dgHashAlgo: dgHashAlgo as IdDocInput['dgHashAlgo'], - eContentHashAlgo: eContentHashAlgo as IdDocInput['eContentHashAlgo'], - signatureType: - signatureTypeForGeneration as IdDocInput['signatureType'], - expiryDate: getExpiryDateFromYears(expiryYears), - passportNumber: randomPassportNumber, - }; - - let dobForGeneration: string; - if (isInOfacList) { - dobForGeneration = '541007'; - idDocInput.lastName = 'HENAO MONTOYA'; - idDocInput.firstName = 'ARCANGEL DE JESUS'; - } else { - dobForGeneration = getBirthDateFromAge(age); - } - idDocInput.birthDate = dobForGeneration; - let mockDSC, rawMockData; - try { - mockDSC = await generateMockDSC( - idDocInput.signatureType || 'rsa_sha256_65537_2048', - ); - rawMockData = genMockIdDoc(idDocInput, mockDSC); - } catch (error) { - console.warn( - 'Falling back to default mock DSC. Error during mock DSC generation:', - error, - ); - rawMockData = genMockIdDoc(idDocInput); - } - const skiPem = await getSKIPEM('staging'); - const parsedMockData = initPassportDataParsing(rawMockData, skiPem); + const parsedMockData = await generateMockDocument({ + age, + expiryYears, + isInOfacList, + selectedAlgorithm, + selectedCountry, + selectedDocumentType, + }); await storePassportData(parsedMockData); navigation.navigate('ConfirmBelongingScreen', {}); } catch (error) { @@ -375,7 +227,7 @@ const MockDataScreen: React.FC = () => { - Mock Passport Parameters + Mock Document Parameters { - + { {isGenerating ? ( ) : ( - 'Generate Mock Passport' + 'Generate Mock Document' )} diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index 3abd1e49a..74670430f 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -52,6 +52,8 @@ export { defaultConfig } from './config/defaults'; /** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */ export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; +export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; + export { getAllDocuments, hasAnyValidRegisteredDocument, loadSelectedDocument } from './documents/utils'; // Core functions diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index 04b6365e4..fbc9879c4 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -86,6 +86,8 @@ export { extractMRZInfo } from './mrz'; export { formatDateToYYMMDD, scanMRZ } from './mrz'; +export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; + // Documents utils export { getAllDocuments, hasAnyValidRegisteredDocument, loadSelectedDocument } from './documents/utils'; diff --git a/packages/mobile-sdk-alpha/src/mock/generator.ts b/packages/mobile-sdk-alpha/src/mock/generator.ts new file mode 100644 index 000000000..4b6aff240 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/mock/generator.ts @@ -0,0 +1,119 @@ +// 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 type { IdDocInput } from '@selfxyz/common/utils'; +import { getSKIPEM } from '@selfxyz/common/utils/csca'; +import { generateMockDSC, genMockIdDoc, initPassportDataParsing } from '@selfxyz/common/utils/passports'; + +export interface GenerateMockDocumentOptions { + age: number; + expiryYears: number; + isInOfacList: boolean; + selectedAlgorithm: string; + selectedCountry: string; + selectedDocumentType: 'mock_passport' | 'mock_id_card'; +} + +const formatDateToYYMMDD = (date: Date): string => { + return (date.toISOString().slice(2, 4) + date.toISOString().slice(5, 7) + date.toISOString().slice(8, 10)).toString(); +}; + +const getBirthDateFromAge = (age: number): string => { + const date = new Date(); + date.setFullYear(date.getFullYear() - age); + return formatDateToYYMMDD(date); +}; + +const getExpiryDateFromYears = (years: number): string => { + const date = new Date(); + date.setFullYear(date.getFullYear() + years); + return formatDateToYYMMDD(date); +}; + +export async function generateMockDocument({ + age, + expiryYears, + isInOfacList, + selectedAlgorithm, + selectedCountry, + selectedDocumentType, +}: GenerateMockDocumentOptions) { + const randomPassportNumber = Math.random() + .toString(36) + .substring(2, 11) + .replace(/[^a-z0-9]/gi, '') + .toUpperCase(); + const [dgHashAlgo, eContentHashAlgo, signatureTypeForGeneration] = + signatureAlgorithmToStrictSignatureAlgorithm[ + selectedAlgorithm as keyof typeof signatureAlgorithmToStrictSignatureAlgorithm + ]; + + const idDocInput: Partial = { + nationality: selectedCountry as IdDocInput['nationality'], + idType: selectedDocumentType as IdDocInput['idType'], + dgHashAlgo: dgHashAlgo as IdDocInput['dgHashAlgo'], + eContentHashAlgo: eContentHashAlgo as IdDocInput['eContentHashAlgo'], + signatureType: signatureTypeForGeneration as IdDocInput['signatureType'], + expiryDate: getExpiryDateFromYears(expiryYears), + passportNumber: randomPassportNumber, + }; + + let dobForGeneration: string; + if (isInOfacList) { + dobForGeneration = '541007'; + idDocInput.lastName = 'HENAO MONTOYA'; + idDocInput.firstName = 'ARCANGEL DE JESUS'; + } else { + dobForGeneration = getBirthDateFromAge(age); + } + idDocInput.birthDate = dobForGeneration; + + let mockDSC, rawMockData; + try { + mockDSC = await generateMockDSC(idDocInput.signatureType || 'rsa_sha256_65537_2048'); + rawMockData = genMockIdDoc(idDocInput, mockDSC); + } catch (error) { + console.warn('Falling back to default mock DSC. Error during mock DSC generation:', error); + rawMockData = genMockIdDoc(idDocInput); + } + const skiPem = await getSKIPEM('staging'); + return initPassportDataParsing(rawMockData, skiPem); +} + +export const signatureAlgorithmToStrictSignatureAlgorithm = { + 'sha256 rsa 65537 4096': ['sha256', 'sha256', 'rsa_sha256_65537_4096'], + 'sha1 rsa 65537 2048': ['sha1', 'sha1', 'rsa_sha1_65537_2048'], + 'sha256 brainpoolP256r1': ['sha256', 'sha256', 'ecdsa_sha256_brainpoolP256r1_256'], + 'sha384 brainpoolP384r1': ['sha384', 'sha384', 'ecdsa_sha384_brainpoolP384r1_384'], + 'sha384 secp384r1': ['sha384', 'sha384', 'ecdsa_sha384_secp384r1_384'], + 'sha256 rsa 65537 2048': ['sha256', 'sha256', 'rsa_sha256_65537_2048'], + 'sha256 rsa 3 2048': ['sha256', 'sha256', 'rsa_sha256_3_2048'], + 'sha256 rsa 65537 3072': ['sha256', 'sha256', 'rsa_sha256_65537_3072'], + 'sha256 rsa 3 4096': ['sha256', 'sha256', 'rsa_sha256_3_4096'], + 'sha384 rsa 65537 4096': ['sha384', 'sha384', 'rsa_sha384_65537_4096'], + 'sha512 rsa 65537 2048': ['sha512', 'sha512', 'rsa_sha512_65537_2048'], + 'sha512 rsa 65537 4096': ['sha512', 'sha512', 'rsa_sha512_65537_4096'], + 'sha1 rsa 65537 4096': ['sha1', 'sha1', 'rsa_sha1_65537_4096'], + 'sha256 rsapss 3 2048': ['sha256', 'sha256', 'rsapss_sha256_3_2048'], + 'sha256 rsapss 3 3072': ['sha256', 'sha256', 'rsapss_sha256_3_3072'], + 'sha256 rsapss 65537 3072': ['sha256', 'sha256', 'rsapss_sha256_65537_3072'], + 'sha256 rsapss 65537 4096': ['sha256', 'sha256', 'rsapss_sha256_65537_4096'], + 'sha384 rsapss 65537 2048': ['sha384', 'sha384', 'rsapss_sha384_65537_2048'], + 'sha384 rsapss 65537 3072': ['sha384', 'sha384', 'rsapss_sha384_65537_3072'], + 'sha512 rsapss 65537 2048': ['sha512', 'sha512', 'rsapss_sha512_65537_2048'], + 'sha512 rsapss 65537 4096': ['sha512', 'sha512', 'rsapss_sha512_65537_4096'], + 'sha1 secp256r1': ['sha1', 'sha1', 'ecdsa_sha1_secp256r1_256'], + 'sha224 secp224r1': ['sha224', 'sha224', 'ecdsa_sha224_secp224r1_224'], + 'sha256 secp256r1': ['sha256', 'sha256', 'ecdsa_sha256_secp256r1_256'], + 'sha256 secp384r1': ['sha256', 'sha256', 'ecdsa_sha256_secp384r1_384'], + 'sha1 brainpoolP224r1': ['sha1', 'sha1', 'ecdsa_sha1_brainpoolP224r1_224'], + 'sha1 brainpoolP256r1': ['sha1', 'sha1', 'ecdsa_sha1_brainpoolP256r1_256'], + 'sha224 brainpoolP224r1': ['sha224', 'sha224', 'ecdsa_sha224_brainpoolP224r1_224'], + 'sha256 brainpoolP224r1': ['sha256', 'sha256', 'ecdsa_sha256_brainpoolP224r1_224'], + 'sha384 brainpoolP256r1': ['sha384', 'sha384', 'ecdsa_sha384_brainpoolP256r1_256'], + 'sha512 brainpoolP256r1': ['sha512', 'sha512', 'ecdsa_sha512_brainpoolP256r1_256'], + 'sha512 brainpoolP384r1': ['sha512', 'sha512', 'ecdsa_sha512_brainpoolP384r1_384'], + 'sha512 poland': ['sha512', 'sha512', 'rsa_sha256_65537_4096'], + 'not existing': ['sha512', 'sha384', 'rsa_sha256_65537_4096'], +} as const; diff --git a/packages/mobile-sdk-alpha/tests/mock/generator.test.ts b/packages/mobile-sdk-alpha/tests/mock/generator.test.ts new file mode 100644 index 000000000..6f88a8223 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/mock/generator.test.ts @@ -0,0 +1,547 @@ +// 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 { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from '../../src/mock/generator'; + +// Mock the external dependencies +vi.mock('@selfxyz/common/utils/csca', () => ({ + getSKIPEM: vi.fn(), +})); + +vi.mock('@selfxyz/common/utils/passports', () => ({ + generateMockDSC: vi.fn(), + genMockIdDoc: vi.fn(), + initPassportDataParsing: vi.fn(), +})); + +// Import the mocked functions after the mocks are defined +let getSKIPEM: any; +let generateMockDSC: any; +let genMockIdDoc: any; +let initPassportDataParsing: any; + +// These will be imported after the mocks are set up + +describe('Mock Generator Helper Functions', () => { + describe('Date formatting and calculation', () => { + it('should format date to YYMMDD correctly', () => { + // Test with a known date + const testDate = new Date('2024-03-15T10:30:00.000Z'); + const expected = '240315'; // YY=24, MM=03, DD=15 + + // Since the helper function is not exported, we'll test through the main function + // and verify the format in the results + expect( + testDate.toISOString().slice(2, 4) + testDate.toISOString().slice(5, 7) + testDate.toISOString().slice(8, 10), + ).toBe(expected); + }); + + it('should calculate birth date from age correctly', () => { + const currentYear = new Date().getFullYear(); + const testAge = 25; + const expectedBirthYear = currentYear - testAge; + + // We'll verify this through the main function since the helper is not exported + const birthDate = new Date(); + birthDate.setFullYear(birthDate.getFullYear() - testAge); + const expectedFormat = ( + birthDate.toISOString().slice(2, 4) + + birthDate.toISOString().slice(5, 7) + + birthDate.toISOString().slice(8, 10) + ).toString(); + + expect(expectedFormat).toMatch(/^\d{6}$/); + expect(parseInt(expectedFormat.slice(0, 2)) + 2000).toBeCloseTo(expectedBirthYear, 0); + }); + + it('should calculate expiry date from years correctly', () => { + const currentYear = new Date().getFullYear(); + const testYears = 10; + const expectedExpiryYear = currentYear + testYears; + + const expiryDate = new Date(); + expiryDate.setFullYear(expiryDate.getFullYear() + testYears); + const expectedFormat = ( + expiryDate.toISOString().slice(2, 4) + + expiryDate.toISOString().slice(5, 7) + + expiryDate.toISOString().slice(8, 10) + ).toString(); + + expect(expectedFormat).toMatch(/^\d{6}$/); + expect(parseInt(expectedFormat.slice(0, 2)) + 2000).toBeCloseTo(expectedExpiryYear, 0); + }); + }); +}); + +describe('generateMockDocument', () => { + beforeEach(async () => { + vi.clearAllMocks(); + + // Import the mocked functions + const csca = await import('@selfxyz/common/utils/csca'); + const passports = await import('@selfxyz/common/utils/passports'); + getSKIPEM = csca.getSKIPEM; + generateMockDSC = passports.generateMockDSC; + genMockIdDoc = passports.genMockIdDoc; + initPassportDataParsing = passports.initPassportDataParsing; + + // Setup default mocks with proper types + vi.mocked(getSKIPEM).mockResolvedValue({ 'mock-key': 'mock-ski-pem' }); + vi.mocked(generateMockDSC).mockResolvedValue({ privateKeyPem: 'mock-private-key', dsc: 'mock-dsc' }); + vi.mocked(genMockIdDoc).mockReturnValue({ + dataGroupHashes: {}, + eContent: new Uint8Array(), + encryptedDigest: new Uint8Array(), + } as any); + vi.mocked(initPassportDataParsing).mockResolvedValue({ + dataGroupHashes: {}, + eContent: new Uint8Array(), + encryptedDigest: new Uint8Array(), + } as any); + }); + + const defaultOptions = { + age: 30, + expiryYears: 10, + isInOfacList: false, + selectedAlgorithm: 'sha256 rsa 65537 2048', + selectedCountry: 'US', + selectedDocumentType: 'mock_passport' as const, + }; + + it('should generate mock passport with default options', async () => { + const result = await generateMockDocument(defaultOptions); + + expect(result).toBeDefined(); + expect(getSKIPEM).toHaveBeenCalledWith('staging'); + expect(generateMockDSC).toHaveBeenCalledWith('rsa_sha256_65537_2048'); + expect(genMockIdDoc).toHaveBeenCalledWith( + expect.objectContaining({ + nationality: 'US', + idType: 'mock_passport', + dgHashAlgo: 'sha256', + eContentHashAlgo: 'sha256', + signatureType: 'rsa_sha256_65537_2048', + }), + expect.objectContaining({ privateKeyPem: expect.any(String), dsc: expect.any(String) }), + ); + expect(initPassportDataParsing).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ 'mock-key': expect.any(String) }), + ); + }); + + it('should generate mock ID card when document type is mock_id_card', async () => { + const options = { ...defaultOptions, selectedDocumentType: 'mock_id_card' as const }; + + await generateMockDocument(options); + + expect(genMockIdDoc).toHaveBeenCalledWith( + expect.objectContaining({ + idType: 'mock_id_card', + }), + expect.objectContaining({ privateKeyPem: expect.any(String), dsc: expect.any(String) }), + ); + }); + + it('should use OFAC-listed person data when isInOfacList is true', async () => { + const options = { ...defaultOptions, isInOfacList: true }; + + await generateMockDocument(options); + + expect(genMockIdDoc).toHaveBeenCalledWith( + expect.objectContaining({ + lastName: 'HENAO MONTOYA', + firstName: 'ARCANGEL DE JESUS', + birthDate: '541007', // Fixed OFAC DOB + }), + expect.objectContaining({ privateKeyPem: expect.any(String), dsc: expect.any(String) }), + ); + }); + + it('should calculate age-based birth date when not in OFAC list', async () => { + const options = { ...defaultOptions, age: 25, isInOfacList: false }; + + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[0][0]; + expect(callArgs.birthDate).toMatch(/^\d{6}$/); + expect(callArgs.birthDate).not.toBe('541007'); // Should not be the OFAC DOB + }); + + it('should generate random passport numbers', async () => { + await generateMockDocument(defaultOptions); + await generateMockDocument(defaultOptions); + + const call1Args = vi.mocked(genMockIdDoc).mock.calls[0]?.[0]; + const call2Args = vi.mocked(genMockIdDoc).mock.calls[1]?.[0]; + + expect(call1Args).toBeDefined(); + expect(call2Args).toBeDefined(); + expect(call1Args!.passportNumber).toMatch(/^[A-Z0-9]{9}$/); + expect(call2Args!.passportNumber).toMatch(/^[A-Z0-9]{9}$/); + // Random numbers should be different + expect(call1Args!.passportNumber).not.toBe(call2Args!.passportNumber); + }); + + it('should handle different signature algorithms', async () => { + const algorithms = ['sha256 rsa 65537 4096', 'sha1 rsa 65537 2048', 'sha256 brainpoolP256r1', 'sha384 secp384r1']; + + for (const algorithm of algorithms) { + const options = { ...defaultOptions, selectedAlgorithm: algorithm }; + await generateMockDocument(options); + + const expectedMapping = + signatureAlgorithmToStrictSignatureAlgorithm[ + algorithm as keyof typeof signatureAlgorithmToStrictSignatureAlgorithm + ]; + expect(generateMockDSC).toHaveBeenCalledWith(expectedMapping[2]); + } + }); + + it('should set expiry date based on years parameter', async () => { + const options = { ...defaultOptions, expiryYears: 5 }; + + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[0]?.[0]; + expect(callArgs).toBeDefined(); + expect(callArgs!.expiryDate).toMatch(/^\d{6}$/); + + // Verify the year is approximately correct (within 1 year due to timing) + const currentYear = new Date().getFullYear(); + const expiryYear = 2000 + parseInt(callArgs!.expiryDate!.slice(0, 2)); + expect(expiryYear).toBeGreaterThanOrEqual(currentYear + 4); + expect(expiryYear).toBeLessThanOrEqual(currentYear + 6); + }); + + it('should fall back to default DSC when generateMockDSC fails', async () => { + vi.mocked(generateMockDSC).mockRejectedValueOnce(new Error('DSC generation failed')); + + const result = await generateMockDocument(defaultOptions); + + expect(result).toBeDefined(); + expect(genMockIdDoc).toHaveBeenCalledWith( + expect.objectContaining({ + signatureType: 'rsa_sha256_65537_2048', + }), + // Note: no second argument (mock DSC) when falling back + ); + }); + + it('should handle various countries', async () => { + const countries = ['US', 'GB', 'DE', 'FR', 'JP']; + + for (const country of countries) { + const options = { ...defaultOptions, selectedCountry: country }; + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[vi.mocked(genMockIdDoc).mock.calls.length - 1]?.[0]; + expect(callArgs).toBeDefined(); + expect(callArgs!.nationality).toBe(country); + } + }); + + it('should preserve all required ID document fields', async () => { + await generateMockDocument(defaultOptions); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[0][0]; + + expect(callArgs).toEqual( + expect.objectContaining({ + nationality: expect.any(String), + idType: expect.any(String), + dgHashAlgo: expect.any(String), + eContentHashAlgo: expect.any(String), + signatureType: expect.any(String), + expiryDate: expect.any(String), + passportNumber: expect.any(String), + birthDate: expect.any(String), + }), + ); + }); +}); + +describe('signatureAlgorithmToStrictSignatureAlgorithm', () => { + it('should contain all expected signature algorithms', () => { + const expectedAlgorithms = [ + 'sha256 rsa 65537 4096', + 'sha1 rsa 65537 2048', + 'sha256 brainpoolP256r1', + 'sha384 brainpoolP384r1', + 'sha384 secp384r1', + 'sha256 rsa 65537 2048', + 'sha256 rsa 3 2048', + 'sha256 rsa 65537 3072', + 'sha256 rsa 3 4096', + 'sha384 rsa 65537 4096', + 'sha512 rsa 65537 2048', + 'sha512 rsa 65537 4096', + 'sha1 rsa 65537 4096', + 'sha256 rsapss 3 2048', + 'sha256 rsapss 3 3072', + 'sha256 rsapss 65537 3072', + 'sha256 rsapss 65537 4096', + 'sha384 rsapss 65537 2048', + 'sha384 rsapss 65537 3072', + 'sha512 rsapss 65537 2048', + 'sha512 rsapss 65537 4096', + 'sha1 secp256r1', + 'sha224 secp224r1', + 'sha256 secp256r1', + 'sha256 secp384r1', + 'sha1 brainpoolP224r1', + 'sha1 brainpoolP256r1', + 'sha224 brainpoolP224r1', + 'sha256 brainpoolP224r1', + 'sha384 brainpoolP256r1', + 'sha512 brainpoolP256r1', + 'sha512 brainpoolP384r1', + 'sha512 poland', + 'not existing', + ]; + + expectedAlgorithms.forEach(algorithm => { + expect(signatureAlgorithmToStrictSignatureAlgorithm).toHaveProperty(algorithm); + }); + }); + + it('should map algorithms to correct tuple format', () => { + const testCases = [ + { + input: 'sha256 rsa 65537 2048', + expected: ['sha256', 'sha256', 'rsa_sha256_65537_2048'], + }, + { + input: 'sha384 brainpoolP384r1', + expected: ['sha384', 'sha384', 'ecdsa_sha384_brainpoolP384r1_384'], + }, + { + input: 'sha256 rsapss 65537 4096', + expected: ['sha256', 'sha256', 'rsapss_sha256_65537_4096'], + }, + { + input: 'sha1 secp256r1', + expected: ['sha1', 'sha1', 'ecdsa_sha1_secp256r1_256'], + }, + ]; + + testCases.forEach(({ input, expected }) => { + expect( + signatureAlgorithmToStrictSignatureAlgorithm[ + input as keyof typeof signatureAlgorithmToStrictSignatureAlgorithm + ], + ).toEqual(expected); + }); + }); + + it('should have consistent hash algorithms in tuples', () => { + Object.entries(signatureAlgorithmToStrictSignatureAlgorithm).forEach(([key, [dgHashAlgo, eContentHashAlgo]]) => { + // For most algorithms, dgHashAlgo and eContentHashAlgo should be the same + if (key !== 'not existing') { + expect(dgHashAlgo).toBe(eContentHashAlgo); + } + }); + }); + + it('should have valid signature types in tuples', () => { + const validSignatureTypes = [ + 'rsa_sha256_65537_4096', + 'rsa_sha1_65537_2048', + 'ecdsa_sha256_brainpoolP256r1_256', + 'ecdsa_sha384_brainpoolP384r1_384', + 'ecdsa_sha384_secp384r1_384', + 'rsa_sha256_65537_2048', + 'rsa_sha256_3_2048', + 'rsa_sha256_65537_3072', + 'rsa_sha256_3_4096', + 'rsa_sha384_65537_4096', + 'rsa_sha512_65537_2048', + 'rsa_sha512_65537_4096', + 'rsa_sha1_65537_4096', + 'rsapss_sha256_3_2048', + 'rsapss_sha256_3_3072', + 'rsapss_sha256_65537_3072', + 'rsapss_sha256_65537_4096', + 'rsapss_sha384_65537_2048', + 'rsapss_sha384_65537_3072', + 'rsapss_sha512_65537_2048', + 'rsapss_sha512_65537_4096', + 'ecdsa_sha1_secp256r1_256', + 'ecdsa_sha224_secp224r1_224', + 'ecdsa_sha256_secp256r1_256', + 'ecdsa_sha256_secp384r1_384', + 'ecdsa_sha1_brainpoolP224r1_224', + 'ecdsa_sha1_brainpoolP256r1_256', + 'ecdsa_sha224_brainpoolP224r1_224', + 'ecdsa_sha256_brainpoolP224r1_224', + 'ecdsa_sha384_brainpoolP256r1_256', + 'ecdsa_sha512_brainpoolP256r1_256', + 'ecdsa_sha512_brainpoolP384r1_384', + ]; + + Object.values(signatureAlgorithmToStrictSignatureAlgorithm).forEach(([, , signatureType]) => { + expect(validSignatureTypes).toContain(signatureType); + }); + }); +}); + +describe('generateMockDocument integration', () => { + const defaultOptions = { + age: 30, + expiryYears: 10, + isInOfacList: false, + selectedAlgorithm: 'sha256 rsa 65537 2048', + selectedCountry: 'US', + selectedDocumentType: 'mock_passport' as const, + }; + + beforeEach(async () => { + vi.clearAllMocks(); + + // Import the mocked functions + const csca = await import('@selfxyz/common/utils/csca'); + const passports = await import('@selfxyz/common/utils/passports'); + getSKIPEM = csca.getSKIPEM; + generateMockDSC = passports.generateMockDSC; + genMockIdDoc = passports.genMockIdDoc; + initPassportDataParsing = passports.initPassportDataParsing; + + // Setup default mocks with proper types + vi.mocked(getSKIPEM).mockResolvedValue({ 'mock-key': 'mock-ski-pem' }); + vi.mocked(generateMockDSC).mockResolvedValue({ privateKeyPem: 'mock-private-key', dsc: 'mock-dsc' }); + vi.mocked(genMockIdDoc).mockReturnValue({ + dataGroupHashes: {}, + eContent: new Uint8Array(), + encryptedDigest: new Uint8Array(), + } as any); + vi.mocked(initPassportDataParsing).mockResolvedValue({ + dataGroupHashes: {}, + eContent: new Uint8Array(), + encryptedDigest: new Uint8Array(), + } as any); + }); + + it('should handle successful DSC generation path', async () => { + const result = await generateMockDocument(defaultOptions); + + expect(generateMockDSC).toHaveBeenCalledWith('rsa_sha256_65537_2048'); + expect(genMockIdDoc).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ privateKeyPem: expect.any(String), dsc: expect.any(String) }), + ); + expect(result).toBeDefined(); + }); + + it('should handle DSC generation failure and fall back gracefully', async () => { + const mockError = new Error('DSC generation failed'); + vi.mocked(generateMockDSC).mockRejectedValueOnce(mockError); + + const result = await generateMockDocument(defaultOptions); + + expect(generateMockDSC).toHaveBeenCalledWith('rsa_sha256_65537_2048'); + // Should call genMockIdDoc without DSC (fallback mode) + expect(genMockIdDoc).toHaveBeenCalledWith(expect.any(Object)); + expect(result).toBeDefined(); + }); + + it('should generate different passport numbers on subsequent calls', async () => { + await generateMockDocument(defaultOptions); + await generateMockDocument(defaultOptions); + + const call1Args = vi.mocked(genMockIdDoc).mock.calls[0]?.[0]; + const call2Args = vi.mocked(genMockIdDoc).mock.calls[1]?.[0]; + + expect(call1Args).toBeDefined(); + expect(call2Args).toBeDefined(); + expect(call1Args!.passportNumber).not.toBe(call2Args!.passportNumber); + expect(call1Args!.passportNumber).toMatch(/^[A-Z0-9]+$/); + expect(call2Args!.passportNumber).toMatch(/^[A-Z0-9]+$/); + }); + + it('should handle various edge cases for age', async () => { + const edgeCases = [0, 1, 18, 100, 120]; + + for (const age of edgeCases) { + const options = { ...defaultOptions, age }; + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[vi.mocked(genMockIdDoc).mock.calls.length - 1]?.[0]; + expect(callArgs).toBeDefined(); + expect(callArgs!.birthDate).toMatch(/^\d{6}$/); + } + }); + + it('should handle various expiry years', async () => { + const expiryYears = [1, 5, 10, 15, 30]; + + for (const years of expiryYears) { + const options = { ...defaultOptions, expiryYears: years }; + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[vi.mocked(genMockIdDoc).mock.calls.length - 1]?.[0]; + expect(callArgs).toBeDefined(); + expect(callArgs!.expiryDate).toMatch(/^\d{6}$/); + } + }); + + it('should use correct hash algorithms from mapping', async () => { + const testAlgorithm = 'sha384 brainpoolP384r1'; + const options = { ...defaultOptions, selectedAlgorithm: testAlgorithm }; + + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[0]?.[0]; + expect(callArgs).toBeDefined(); + expect(callArgs!.dgHashAlgo).toBe('sha384'); + expect(callArgs!.eContentHashAlgo).toBe('sha384'); + expect(callArgs!.signatureType).toBe('ecdsa_sha384_brainpoolP384r1_384'); + }); + + it('should always call getSKIPEM with staging parameter', async () => { + await generateMockDocument(defaultOptions); + + expect(getSKIPEM).toHaveBeenCalledWith('staging'); + expect(getSKIPEM).toHaveBeenCalledTimes(1); + }); + + it('should maintain document field completeness', async () => { + const options = { + ...defaultOptions, + age: 42, + expiryYears: 7, + selectedCountry: 'CA', + selectedDocumentType: 'mock_id_card' as const, + }; + + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[0]?.[0]; + + // Verify all required fields are present + expect(callArgs).toBeDefined(); + expect(callArgs!.nationality).toBe('CA'); + expect(callArgs!.idType).toBe('mock_id_card'); + expect(callArgs!.dgHashAlgo).toBeDefined(); + expect(callArgs!.eContentHashAlgo).toBeDefined(); + expect(callArgs!.signatureType).toBeDefined(); + expect(callArgs!.expiryDate).toBeDefined(); + expect(callArgs!.passportNumber).toBeDefined(); + expect(callArgs!.birthDate).toBeDefined(); + }); + + it('should handle special algorithm edge case', async () => { + const options = { ...defaultOptions, selectedAlgorithm: 'not existing' }; + + await generateMockDocument(options); + + const callArgs = vi.mocked(genMockIdDoc).mock.calls[0]?.[0]; + expect(callArgs).toBeDefined(); + expect(callArgs!.dgHashAlgo).toBe('sha512'); + expect(callArgs!.eContentHashAlgo).toBe('sha384'); + expect(callArgs!.signatureType).toBe('rsa_sha256_65537_4096'); + }); +});