diff --git a/.cursorignore b/.cursorignore index 00b1b1008..4f4289339 100644 --- a/.cursorignore +++ b/.cursorignore @@ -40,10 +40,6 @@ app/android/dev-keystore circuits/scripts/server/*.sh !node_modules/**/*.sh -# Fastlane configuration (may contain secrets) -app/fastlane/Fastfile -app/fastlane/helpers.rb - # Test wallets and mock data app/ios/passport.json app/ios/OpenPassport/passport.json @@ -100,7 +96,6 @@ contracts/ignition/deployments/ **/.pnp.* # Mobile specific -app/ios/Podfile.lock app/android/link-assets-manifest.json app/ios/link-assets-manifest.json @@ -162,7 +157,6 @@ app/android/android-passport-reader/app/src/main/assets/tessdata/ # IDE & Editor Files # ======================================== -.vscode/ .idea/ *.swp *.swo diff --git a/.vscode/settings.json b/.vscode/settings.json index 00ff1b893..57f322553 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,49 @@ { + // Performance Optimizations + "files.watcherExclude": { + "**/node_modules/**": true, + "**/.git/**": true, + "**/dist/**": true, + "**/build/**": true, + "**/vendor/**": true, + "**/coverage/**": true, + "**/.nyc_output/**": true, + "**/android/app/build/**": true, + "**/ios/build/**": true, + "**/circuits/build/**": true, + "**/Pods/**": true, + "**/.gradle/**": true, + "**/DerivedData/**": true + }, + "search.exclude": { + "**/node_modules": true, + "**/dist": true, + "**/build": true, + "**/vendor": true, + "**/coverage": true, + "**/.nyc_output": true, + "**/android/app/build": true, + "**/ios/build": true, + "**/circuits/build": true + }, + "files.exclude": { + "**/node_modules": false, + "**/.git": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + + // TypeScript Performance (Keep the good stuff) + "typescript.preferences.includePackageJsonAutoImports": "on", + "typescript.suggest.autoImports": true, + "typescript.disableAutomaticTypeAcquisition": true, + "typescript.updateImportsOnFileMove.enabled": "always", + "typescript.suggestionActions.enabled": true, + + // Editor Performance (Sensible optimizations only) + "editor.minimap.enabled": false, + "editor.hover.delay": 500, + // Formatting & Linting "editor.formatOnSave": false, "editor.formatOnPaste": false, @@ -8,8 +53,8 @@ "editor.formatOnSave": true }, - // ESLint Configuration - "eslint.run": "onType", + // ESLint Configuration - Optimized for Performance + "eslint.run": "onSave", "eslint.format.enable": true, "eslint.lintTask.enable": true, "eslint.quiet": false, diff --git a/app/src/RemoteConfig.shared.ts b/app/src/RemoteConfig.shared.ts index 85736b0f8..42f49ee66 100644 --- a/app/src/RemoteConfig.shared.ts +++ b/app/src/RemoteConfig.shared.ts @@ -37,6 +37,9 @@ export interface StorageBackend { export const LOCAL_OVERRIDES_KEY = 'feature_flag_overrides'; +// Default feature flags - this should be defined by the consuming application +const defaultFlags: Record = {}; + export const clearAllLocalOverrides = async ( storage: StorageBackend, ): Promise => { @@ -60,11 +63,6 @@ export const clearLocalOverride = async ( } }; -// Shared interfaces for platform-specific implementations -export const defaultFlags: Record = { - aesop: false, -}; - export const getAllFeatureFlags = async ( remoteConfig: RemoteConfigBackend, storage: StorageBackend, @@ -237,7 +235,7 @@ export const initRemoteConfig = async ( try { await remoteConfig.fetchAndActivate(); } catch (err) { - console.log('Remote config fetch failed', err); + console.error('Remote config fetch failed', err); } }; @@ -247,7 +245,7 @@ export const refreshRemoteConfig = async ( try { await remoteConfig.fetchAndActivate(); } catch (err) { - console.log('Remote config refresh failed', err); + console.error('Remote config refresh failed', err); } }; diff --git a/app/src/components/ErrorBoundary.tsx b/app/src/components/ErrorBoundary.tsx index ae6545908..b5f8e52e4 100644 --- a/app/src/components/ErrorBoundary.tsx +++ b/app/src/components/ErrorBoundary.tsx @@ -1,8 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 +import type { ErrorInfo } from 'react'; import React, { Component } from 'react'; import { Text, View } from 'react-native'; +import { captureException } from '../Sentry'; import analytics from '../utils/analytics'; const { flush: flushAnalytics } = analytics(); @@ -25,12 +27,13 @@ class ErrorBoundary extends Component { return { hasError: true }; } - componentDidCatch() { + componentDidCatch(error: Error, info: ErrorInfo) { // Flush analytics before the app crashes flushAnalytics(); - // TODO Sentry React docs recommend Sentry.captureReactException(error, info); - // https://docs.sentry.io/platforms/javascript/guides/react/features/error-boundary/ - // but ill wait so as to have few changes on native app + captureException(error, { + componentStack: info.componentStack, + errorBoundary: true, + }); } render() { diff --git a/app/src/components/native/QRCodeScanner.tsx b/app/src/components/native/QRCodeScanner.tsx index a82811132..17cc062ac 100644 --- a/app/src/components/native/QRCodeScanner.tsx +++ b/app/src/components/native/QRCodeScanner.tsx @@ -65,7 +65,6 @@ export const QRCodeScannerView: React.FC = ({ if (!isMounted) { return; } - console.log(event.nativeEvent.data); onQRData(null, event.nativeEvent.data); }, [onQRData, isMounted], diff --git a/app/src/components/native/RCTFragment.tsx b/app/src/components/native/RCTFragment.tsx index 60502c4bd..9162d9796 100644 --- a/app/src/components/native/RCTFragment.tsx +++ b/app/src/components/native/RCTFragment.tsx @@ -44,7 +44,7 @@ function dispatchCommand( } catch (e) { // Error creatingthe fragment // TODO: assert this only happens in dev mode when the fragment is already mounted - console.log(e); + console.warn(e); if (command === 'create') { dispatchCommand(fragmentComponentName, viewId, 'destroy'); } diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index 442a43b44..69f4fcfb5 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -78,7 +78,7 @@ const NavigationWithTracking = () => { const trackScreen = () => { const currentRoute = navigationRef.getCurrentRoute(); if (currentRoute) { - console.log(`Screen View: ${currentRoute.name}`); + if (__DEV__) console.log(`Screen View: ${currentRoute.name}`); trackScreenView(`${currentRoute.name}`, { screenName: currentRoute.name, }); diff --git a/app/src/providers/authProvider.tsx b/app/src/providers/authProvider.tsx index b1f016763..aed35ee5b 100644 --- a/app/src/providers/authProvider.tsx +++ b/app/src/providers/authProvider.tsx @@ -26,13 +26,8 @@ const _getSecurely = async function ( fn: () => Promise, formatter: (dataString: string) => T, ): Promise | null> { - console.log('Starting _getSecurely'); - const dataString = await fn(); - console.log('Got data string:', dataString ? 'exists' : 'not found'); - if (dataString === false) { - console.log('No data string available'); return null; } @@ -111,23 +106,19 @@ async function loadOrCreateMnemonic(): Promise { if (storedMnemonic) { try { JSON.parse(storedMnemonic.password); - console.log('Stored mnemonic parsed successfully'); trackEvent(AuthEvents.MNEMONIC_LOADED); return storedMnemonic.password; } catch (e: any) { - console.log( + console.error( 'Error parsing stored mnemonic, old secret format was used', e, ); - console.log('Creating a new one'); trackEvent(AuthEvents.MNEMONIC_RESTORE_FAILED, { reason: 'unknown_error', error: e.message, }); } } - - console.log('No secret found, creating one'); try { const { mnemonic } = ethers.HDNodeWallet.fromMnemonic( ethers.Mnemonic.fromEntropy(ethers.randomBytes(32)), diff --git a/app/src/providers/passportDataProvider.tsx b/app/src/providers/passportDataProvider.tsx index f508afa84..fa40b8d04 100644 --- a/app/src/providers/passportDataProvider.tsx +++ b/app/src/providers/passportDataProvider.tsx @@ -496,7 +496,12 @@ export async function loadDocumentCatalog(): Promise { service: 'documentCatalog', }); if (catalogCreds !== false) { - return JSON.parse(catalogCreds.password); + const parsed = JSON.parse(catalogCreds.password); + // Handle case where JSON.parse(null) returns null + if (parsed === null) { + throw new TypeError('Cannot parse null password'); + } + return parsed; } } catch (error) { console.log('Error loading document catalog:', error); diff --git a/app/src/screens/dev/MockDataScreen.tsx b/app/src/screens/dev/MockDataScreen.tsx index 88f9c5caf..ce1102073 100644 --- a/app/src/screens/dev/MockDataScreen.tsx +++ b/app/src/screens/dev/MockDataScreen.tsx @@ -289,7 +289,6 @@ const MockDataScreen: React.FC = ({}) => { }; const handleGenerate = useCallback(async () => { - console.log('selectedDocumentType', selectedDocumentType); setIsGenerating(true); try { const randomPassportNumber = Math.random() diff --git a/app/src/screens/misc/LoadingScreen.tsx b/app/src/screens/misc/LoadingScreen.tsx index cb2af7640..ef6b5396d 100644 --- a/app/src/screens/misc/LoadingScreen.tsx +++ b/app/src/screens/misc/LoadingScreen.tsx @@ -111,9 +111,6 @@ const LoadingScreen: React.FC = ({}) => { return; } - console.log('[LoadingScreen] Current proving state:', currentState); - console.log('[LoadingScreen] FCM token available:', !!fcmToken); - // Update UI if passport data is available if (passportData?.passportMetadata) { // Update loading text based on current state diff --git a/app/src/screens/misc/SplashScreen.tsx b/app/src/screens/misc/SplashScreen.tsx index 353d676ec..7835a17d2 100644 --- a/app/src/screens/misc/SplashScreen.tsx +++ b/app/src/screens/misc/SplashScreen.tsx @@ -30,7 +30,6 @@ const SplashScreen: React.FC = ({}) => { useEffect(() => { if (!dataLoadInitiatedRef.current) { dataLoadInitiatedRef.current = true; - console.log('Starting data loading and validation...'); checkBiometricsAvailable() .then(setBiometricsAvailable) @@ -41,7 +40,6 @@ const SplashScreen: React.FC = ({}) => { const loadDataAndDetermineNextScreen = async () => { try { // Initialize native modules first, before any data operations - console.log('Initializing native modules...'); const modulesReady = await initializeNativeModules(); if (!modulesReady) { console.warn( @@ -53,14 +51,7 @@ const SplashScreen: React.FC = ({}) => { const needsMigration = await checkIfAnyDocumentsNeedMigration(); if (needsMigration) { - console.log( - 'Documents need registration state migration, running...', - ); await checkAndUpdateRegistrationStates(); - } else { - console.log( - 'No documents need registration state migration, skipping...', - ); } const hasValid = await hasAnyValidRegisteredDocument(); @@ -82,7 +73,6 @@ const SplashScreen: React.FC = ({}) => { useEffect(() => { if (isAnimationFinished && nextScreen) { - console.log(`Navigating to ${nextScreen}`); requestAnimationFrame(() => { navigation.navigate(nextScreen as any); }); diff --git a/app/src/screens/prove/ConfirmBelongingScreen.tsx b/app/src/screens/prove/ConfirmBelongingScreen.tsx index bea285c73..55c024dec 100644 --- a/app/src/screens/prove/ConfirmBelongingScreen.tsx +++ b/app/src/screens/prove/ConfirmBelongingScreen.tsx @@ -56,7 +56,6 @@ const ConfirmBelongingScreen: React.FC = ({}) => { if (token) { setFcmToken(token); trackEvent(ProofEvents.FCM_TOKEN_STORED); - console.log('FCM token stored in proving store'); } } diff --git a/app/src/screens/prove/ProofRequestStatusScreen.tsx b/app/src/screens/prove/ProofRequestStatusScreen.tsx index 908dd5c0d..c494fd423 100644 --- a/app/src/screens/prove/ProofRequestStatusScreen.tsx +++ b/app/src/screens/prove/ProofRequestStatusScreen.tsx @@ -65,7 +65,6 @@ const SuccessScreen: React.FC = () => { } function cancelCountdown() { - console.log('[ProofRequestStatusScreen] Cancelling countdown'); if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; @@ -75,10 +74,6 @@ const SuccessScreen: React.FC = () => { useEffect(() => { if (isFocused) { - console.log( - '[ProofRequestStatusScreen] State update while focused:', - currentState, - ); } if (currentState === 'completed') { notificationSuccess(); @@ -95,10 +90,6 @@ const SuccessScreen: React.FC = () => { new URL(selfApp.deeplinkCallback); setCountdown(5); setCountdownStarted(true); - console.log( - '[ProofRequestStatusScreen] Countdown started:', - countdown, - ); } catch (error) { console.warn( 'Invalid deep link URL provided:', diff --git a/app/src/screens/prove/ProveScreen.tsx b/app/src/screens/prove/ProveScreen.tsx index 3be999461..7306afa61 100644 --- a/app/src/screens/prove/ProveScreen.tsx +++ b/app/src/screens/prove/ProveScreen.tsx @@ -95,7 +95,6 @@ const ProveScreen: React.FC = () => { setDefaultDocumentTypeIfNeeded(); if (selectedAppRef.current?.sessionId !== selectedApp.sessionId) { - console.log('[ProveScreen] Selected app updated:', selectedApp); provingStore.init('disclose'); } selectedAppRef.current = selectedApp; diff --git a/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx b/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx index 3fb654a17..2f93cd283 100644 --- a/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx +++ b/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx @@ -64,9 +64,8 @@ const AccountRecoveryChoiceScreen: React.FC< passportData, secret, ); - console.log('User is registered:', isRegistered); if (!isRegistered) { - console.log( + console.warn( 'Secret provided did not match a registered ID. Please try again.', ); trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED); diff --git a/app/src/screens/recovery/RecoverWithPhraseScreen.tsx b/app/src/screens/recovery/RecoverWithPhraseScreen.tsx index acefe7b62..e7fad462d 100644 --- a/app/src/screens/recovery/RecoverWithPhraseScreen.tsx +++ b/app/src/screens/recovery/RecoverWithPhraseScreen.tsx @@ -50,7 +50,6 @@ const RecoverWithPhraseScreen: React.FC< setRestoring(true); const slimMnemonic = mnemonic?.trim(); if (!slimMnemonic || !ethers.Mnemonic.isValidMnemonic(slimMnemonic)) { - console.log('Invalid mnemonic'); setRestoring(false); return; } @@ -69,9 +68,8 @@ const RecoverWithPhraseScreen: React.FC< passportData, secret as string, ); - console.log('User is registered:', isRegistered); if (!isRegistered) { - console.log( + console.warn( 'Secret provided did not match a registered passport. Please try again.', ); reStorePassportDataWithRightCSCA(passportData, csca as string); diff --git a/app/src/stores/database.ts b/app/src/stores/database.ts index 59d447627..781cee195 100644 --- a/app/src/stores/database.ts +++ b/app/src/stores/database.ts @@ -31,29 +31,19 @@ export const database: ProofDB = { ); // Improved error handling - wrap each setProofStatus call in try-catch - let successfulUpdates = 0; - let failedUpdates = 0; for (let i = 0; i < stalePending.rows.length; i++) { const { sessionId } = stalePending.rows.item(i); try { await setProofStatus(sessionId, ProofStatus.FAILURE); - successfulUpdates++; } catch (error) { console.error( `Failed to update proof status for session ${sessionId}:`, error, ); - failedUpdates++; // Continue with the next iteration instead of stopping the entire loop } } - - if (stalePending.rows.length > 0) { - console.log( - `Stale proof cleanup: ${successfulUpdates} successful, ${failedUpdates} failed`, - ); - } }, getPendingProofs: async (): Promise => { const db = await openDatabase(); diff --git a/app/src/stores/proofHistoryStore.ts b/app/src/stores/proofHistoryStore.ts index c9d834cd0..db677c4f6 100644 --- a/app/src/stores/proofHistoryStore.ts +++ b/app/src/stores/proofHistoryStore.ts @@ -38,7 +38,6 @@ export const useProofHistoryStore = create()((set, get) => { // Throttling mechanism - prevent sync if called too frequently const now = Date.now(); if (now - lastSyncTime < SYNC_THROTTLE_MS) { - console.log('Sync throttled - too soon since last sync'); return; } lastSyncTime = now; @@ -50,7 +49,6 @@ export const useProofHistoryStore = create()((set, get) => { const pendingProofs = await database.getPendingProofs(); if (pendingProofs.rows.length === 0) { - console.log('No pending proofs to sync'); return; } @@ -60,7 +58,6 @@ export const useProofHistoryStore = create()((set, get) => { }); setTimeout(() => { websocket.connected && websocket.disconnect(); - console.log('WebSocket disconnected after timeout'); // disconnect after 2 minutes }, SYNC_THROTTLE_MS * 4); @@ -74,13 +71,10 @@ export const useProofHistoryStore = create()((set, get) => { typeof message === 'string' ? JSON.parse(message) : message; if (data.status === 3) { - console.log('Failed to generate proof'); get().updateProofStatus(data.request_id, ProofStatus.FAILURE); } else if (data.status === 4) { - console.log('Proof verified'); get().updateProofStatus(data.request_id, ProofStatus.SUCCESS); } else if (data.status === 5) { - console.log('Failed to verify proof'); get().updateProofStatus(data.request_id, ProofStatus.FAILURE); } websocket.emit('unsubscribe', data.request_id); diff --git a/app/src/stores/selfAppStore.tsx b/app/src/stores/selfAppStore.tsx index 92c75520a..906a6edf3 100644 --- a/app/src/stores/selfAppStore.tsx +++ b/app/src/stores/selfAppStore.tsx @@ -51,20 +51,13 @@ export const useSelfAppStore = create((set, get) => ({ }, startAppListener: (sessionId: string) => { - console.log( - `[SelfAppStore] Initializing WS connection with sessionId: ${sessionId}`, - ); const currentSocket = get().socket; // If a socket connection exists for a different session, disconnect it. if (currentSocket && get().sessionId !== sessionId) { - console.log( - '[SelfAppStore] Disconnecting existing socket for old session.', - ); currentSocket.disconnect(); set({ socket: null, sessionId: null, selfApp: null }); } else if (currentSocket && get().sessionId === sessionId) { - console.log('[SelfAppStore] Already connected with the same session ID.'); return; // Avoid reconnecting if already connected with the same session } @@ -72,15 +65,10 @@ export const useSelfAppStore = create((set, get) => ({ const socket = get()._initSocket(sessionId); set({ socket, sessionId }); - socket.on('connect', () => { - console.log( - `[SelfAppStore] Mobile WS connected (id: ${socket.id}) with sessionId: ${sessionId}`, - ); - }); + socket.on('connect', () => {}); // Listen for the event only once per connection attempt socket.once('self_app', (data: any) => { - console.log('[SelfAppStore] Received self_app event with data:', data); try { const appData: SelfApp = typeof data === 'string' ? JSON.parse(data) : data; @@ -101,10 +89,6 @@ export const useSelfAppStore = create((set, get) => ({ return; } - console.log( - '[SelfAppStore] Processing valid app data:', - JSON.stringify(appData), - ); set({ selfApp: appData }); } catch (error) { console.error('[SelfAppStore] Error processing app data:', error); @@ -123,11 +107,9 @@ export const useSelfAppStore = create((set, get) => ({ // Consider if cleanup is needed here as well }); - socket.on('disconnect', (reason: string) => { - console.log('[SelfAppStore] Mobile WS disconnected:', reason); + socket.on('disconnect', (_reason: string) => { // Prevent cleaning up if disconnect was initiated by cleanSelfApp if (get().socket === socket) { - console.log('[SelfAppStore] Cleaning up state on disconnect.'); set({ socket: null, sessionId: null, selfApp: null }); } }); @@ -138,7 +120,6 @@ export const useSelfAppStore = create((set, get) => ({ }, cleanSelfApp: () => { - console.log('[SelfAppStore] Cleaning up SelfApp state and WS connection.'); const socket = get().socket; if (socket) { socket.disconnect(); @@ -162,26 +143,11 @@ export const useSelfAppStore = create((set, get) => ({ return; } - console.log( - `[SelfAppStore] handleProofResult called for sessionId: ${sessionId}, verified: ${proof_verified}`, - ); - if (proof_verified) { - console.log('[SelfAppStore] Emitting proof_verified event with data:', { - session_id: sessionId, - }); socket.emit('proof_verified', { session_id: sessionId, }); } else { - console.log( - '[SelfAppStore] Emitting proof_generation_failed event with data:', - { - session_id: sessionId, - error_code, - reason, - }, - ); socket.emit('proof_generation_failed', { session_id: sessionId, error_code, diff --git a/app/src/stores/settingStore.ts b/app/src/stores/settingStore.ts index 8dcb2bc6d..81dc1b481 100644 --- a/app/src/stores/settingStore.ts +++ b/app/src/stores/settingStore.ts @@ -77,7 +77,7 @@ export const useSettingStore = create()( { name: 'setting-storage', storage: createJSONStorage(() => AsyncStorage), - onRehydrateStorage: () => console.log('Rehydrated settings'), + onRehydrateStorage: () => undefined, partialize: state => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { hideNetworkModal, setHideNetworkModal, ...persistedState } = diff --git a/app/src/utils/jsonUtils.ts b/app/src/utils/jsonUtils.ts new file mode 100644 index 000000000..510888392 --- /dev/null +++ b/app/src/utils/jsonUtils.ts @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +/** + * Safely parses a JSON string with error handling. + * Returns a default value if parsing fails. + * + * @param jsonString - The JSON string to parse + * @param defaultValue - The default value to return if parsing fails + * @returns The parsed object or the default value + */ +export function safeJsonParse( + jsonString: string | null | undefined, + defaultValue: T, +): T { + if (jsonString == null) { + return defaultValue; + } + + try { + return JSON.parse(jsonString); + } catch (error) { + console.warn('Failed to parse JSON, using default value:', error); + return defaultValue; + } +} + +/** + * Safely stringifies an object with error handling. + * Returns a default string if stringification fails. + * + * @param obj - The object to stringify + * @param defaultValue - The default string to return if stringification fails + * @returns The JSON string or the default string + */ +export function safeJsonStringify( + obj: T, + defaultValue: string = '{}', +): string { + if (obj == null) { + return defaultValue; + } + + try { + return JSON.stringify(obj); + } catch (error) { + console.warn('Failed to stringify JSON, using default value:', error); + return defaultValue; + } +} diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index 714b2ef2a..54c436ef8 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -204,7 +204,6 @@ export const useProvingStore = create((set, get) => { function setupActorSubscriptions(newActor: AnyActorRef) { newActor.subscribe((state: any) => { - console.log(`State transition: ${state.value}`); trackEvent(ProofEvents.PROVING_STATE_CHANGE, { state: state.value }); set({ currentState: state.value as ProvingStateType }); @@ -246,7 +245,6 @@ export const useProvingStore = create((set, get) => { (async () => { try { await markCurrentDocumentAsRegistered(); - console.log('Document marked as registered on-chain'); } catch (error) { //This will be checked and updated when the app launches the next time console.error('Error marking document as registered:', error); @@ -360,7 +358,7 @@ export const useProvingStore = create((set, get) => { !result.error ) { trackEvent(ProofEvents.WS_HELLO_ACK); - console.log('Received message with status:', result.id); + // Received status from TEE const statusUuid = result.result; if (get().uuid !== statusUuid) { console.warn( @@ -465,8 +463,7 @@ export const useProvingStore = create((set, get) => { set({ socketConnection: null }); }); - socket.on('disconnect', (reason: string) => { - console.log(`SocketIO disconnected. Reason: ${reason}`); + socket.on('disconnect', (_reason: string) => { const currentActor = actor; if (get().currentState === 'ready_to_prove' && currentActor) { @@ -486,7 +483,6 @@ export const useProvingStore = create((set, get) => { socket.on('status', (message: any) => { const data = typeof message === 'string' ? JSON.parse(message) : message; - console.log('Received status update with status:', data.status); trackEvent(ProofEvents.SOCKETIO_STATUS_RECEIVED, { status: data.status, }); @@ -563,9 +559,6 @@ export const useProvingStore = create((set, get) => { }, _handleWsClose: (event: CloseEvent) => { - console.log( - `TEE WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`, - ); trackEvent(ProofEvents.TEE_WS_CLOSED, { code: event.code, reason: event.reason, @@ -711,7 +704,6 @@ export const useProvingStore = create((set, get) => { actor!.send({ type: 'VALIDATION_SUCCESS' }); return; } else { - console.log('Passport is not registered with local CSCA'); actor!.send({ type: 'PASSPORT_DATA_NOT_FOUND' }); return; } @@ -731,7 +723,6 @@ export const useProvingStore = create((set, get) => { (async () => { try { await markCurrentDocumentAsRegistered(); - console.log('Document marked as registered (already on-chain)'); } catch (error) { //it will be checked and marked as registered during next app launch console.error('Error marking document as registered:', error); @@ -744,7 +735,7 @@ export const useProvingStore = create((set, get) => { } const isNullifierOnchain = await isDocumentNullified(passportData); if (isNullifierOnchain) { - console.log( + console.warn( 'Passport is nullified, but not registered with this secret. Navigating to AccountRecoveryChoice', ); trackEvent(ProofEvents.PASSPORT_NULLIFIER_ONCHAIN); @@ -756,7 +747,6 @@ export const useProvingStore = create((set, get) => { passportData, useProtocolStore.getState()[document].dsc_tree, ); - console.log('isDscRegistered: ', isDscRegistered); if (isDscRegistered) { trackEvent(ProofEvents.DSC_IN_TREE); set({ circuitType: 'register' }); diff --git a/app/src/utils/proving/validateDocument.ts b/app/src/utils/proving/validateDocument.ts index 4ba1ed413..58c25a683 100644 --- a/app/src/utils/proving/validateDocument.ts +++ b/app/src/utils/proving/validateDocument.ts @@ -43,6 +43,7 @@ export type PassportSupportStatus = | 'registration_circuit_not_supported' | 'dsc_circuit_not_supported' | 'passport_supported'; + /** * This function checks and updates registration states for all documents and updates the `isRegistered`. */ @@ -59,7 +60,7 @@ export async function checkAndUpdateRegistrationStates(): Promise { error: 'Passport data is not valid', documentId, }); - console.log(`Skipping invalid document ${documentId}`); + console.warn(`Skipping invalid document ${documentId}`); continue; } const migratedPassportData = migratePassportData(passportData); @@ -78,7 +79,7 @@ export async function checkAndUpdateRegistrationStates(): Promise { documentCategory, mock: migratedPassportData.mock, }); - console.log( + console.warn( `Skipping document ${documentId} - no authority key identifier`, ); continue; @@ -88,7 +89,7 @@ export async function checkAndUpdateRegistrationStates(): Promise { [documentCategory].fetch_all(environment, authorityKeyIdentifier); const passportDataAndSecret = await loadPassportDataAndSecret(); if (!passportDataAndSecret) { - console.log( + console.warn( `Skipping document ${documentId} - no passport data and secret`, ); continue; @@ -108,9 +109,10 @@ export async function checkAndUpdateRegistrationStates(): Promise { }); } - console.log( - `Updated registration state for document ${documentId}: ${isRegistered}`, - ); + if (__DEV__) + console.log( + `Updated registration state for document ${documentId}: ${isRegistered}`, + ); } catch (error) { console.error( `Error checking registration state for document ${documentId}: ${error}`, @@ -122,7 +124,7 @@ export async function checkAndUpdateRegistrationStates(): Promise { } } - console.log('Registration state check and update completed'); + if (__DEV__) console.log('Registration state check and update completed'); } export async function checkIfPassportDscIsInTree( @@ -137,12 +139,10 @@ export async function checkIfPassportDscIsInTree( ); const index = tree.indexOf(BigInt(leaf)); if (index === -1) { - console.log('DSC not found in the tree'); + console.warn('DSC not found in the tree'); return false; - } else { - console.log('DSC found in the tree'); - return true; } + return true; } export async function checkPassportSupported( @@ -154,11 +154,11 @@ export async function checkPassportSupported( const passportMetadata = passportData.passportMetadata; const document: DocumentCategory = passportData.documentCategory; if (!passportMetadata) { - console.log('Passport metadata is null'); + console.warn('Passport metadata is null'); return { status: 'passport_metadata_missing', details: passportData.dsc }; } if (!passportMetadata.cscaFound) { - console.log('CSCA not found'); + console.warn('CSCA not found'); return { status: 'csca_not_found', details: passportData.dsc }; } const circuitNameRegister = getCircuitNameFromPassportData( @@ -187,10 +187,9 @@ export async function checkPassportSupported( deployedCircuits.DSC_ID.includes(circuitNameDsc) ) ) { - console.log('DSC circuit not supported:', circuitNameDsc); + console.warn('DSC circuit not supported:', circuitNameDsc); return { status: 'dsc_circuit_not_supported', details: circuitNameDsc }; } - console.log('Passport supported'); return { status: 'passport_supported', details: 'null' }; } @@ -246,6 +245,21 @@ export function generateCommitmentInApp( return { commitment_list, csca_list }; } +function formatCSCAPem(cscaPem: string): string { + let cleanedPem = cscaPem.trim(); + + if (!cleanedPem.includes('-----BEGIN CERTIFICATE-----')) { + cleanedPem = cleanedPem.replace(/[^A-Za-z0-9+/=]/g, ''); + try { + Buffer.from(cleanedPem, 'base64'); + } catch (error) { + throw new Error(`Invalid base64 certificate data: ${error}`); + } + cleanedPem = `-----BEGIN CERTIFICATE-----\n${cleanedPem}\n-----END CERTIFICATE-----`; + } + return cleanedPem; +} + export async function hasAnyValidRegisteredDocument(): Promise { try { const catalog = await loadDocumentCatalog(); @@ -283,21 +297,6 @@ export async function isDocumentNullified(passportData: PassportData) { return data.data; } -function formatCSCAPem(cscaPem: string): string { - let cleanedPem = cscaPem.trim(); - - if (!cleanedPem.includes('-----BEGIN CERTIFICATE-----')) { - cleanedPem = cleanedPem.replace(/[^A-Za-z0-9+/=]/g, ''); - try { - Buffer.from(cleanedPem, 'base64'); - } catch (error) { - throw new Error(`Invalid base64 certificate data: ${error}`); - } - cleanedPem = `-----BEGIN CERTIFICATE-----\n${cleanedPem}\n-----END CERTIFICATE-----`; - } - return cleanedPem; -} - export function isPassportDataValid(passportData: PassportData) { if (!passportData) { trackEvent(DocumentEvents.VALIDATE_DOCUMENT_FAILED, { @@ -370,7 +369,6 @@ export async function isUserRegisteredWithAlternativeCSCA( const document: DocumentCategory = passportData.documentCategory; const alternativeCSCA = useProtocolStore.getState()[document].alternative_csca; - console.log('alternativeCSCA: ', alternativeCSCA); const { commitment_list, csca_list } = generateCommitmentInApp( secret, document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID, diff --git a/app/src/utils/testingUtils.ts b/app/src/utils/testingUtils.ts new file mode 100644 index 000000000..e2073b9f1 --- /dev/null +++ b/app/src/utils/testingUtils.ts @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import Keychain from 'react-native-keychain'; + +import { loadDocumentCatalog } from '../providers/passportDataProvider'; + +/** + * Testing utility function to clear the document catalog for migration testing. + * This function is only available in development/testing environments. + * + * @returns Promise + */ +export async function clearDocumentCatalogForMigrationTesting(): Promise { + // Only allow this function in development/testing environments + if (__DEV__ === false && process.env.NODE_ENV === 'production') { + throw new Error( + 'clearDocumentCatalogForMigrationTesting is not available in production', + ); + } + + console.log('Clearing document catalog for migration testing...'); + const catalog = await loadDocumentCatalog(); + + // Delete all new-style documents + for (const doc of catalog.documents) { + try { + await Keychain.resetGenericPassword({ service: `document-${doc.id}` }); + console.log(`Cleared document: ${doc.id}`); + } catch (error) { + console.log(`Document ${doc.id} not found or already cleared`); + } + } + + // Clear the catalog itself + try { + await Keychain.resetGenericPassword({ service: 'documentCatalog' }); + console.log('Cleared document catalog'); + } catch (error) { + console.log('Document catalog not found or already cleared'); + } + + // Note: We intentionally do NOT clear legacy storage entries + // (passportData, mockPassportData, etc.) so migration can be tested + console.log( + 'Document catalog cleared. Legacy storage preserved for migration testing.', + ); +} diff --git a/app/tests/src/components/ErrorBoundary.test.tsx b/app/tests/src/components/ErrorBoundary.test.tsx new file mode 100644 index 000000000..14c3fd1ec --- /dev/null +++ b/app/tests/src/components/ErrorBoundary.test.tsx @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React from 'react'; +import { Text } from 'react-native'; + +import { render } from '@testing-library/react-native'; + +const mockFlush = jest.fn(); +const mockAnalytics = jest.fn(() => ({ + flush: mockFlush, +})); + +jest.doMock('../../../src/utils/analytics', () => mockAnalytics); +jest.mock('../../../src/Sentry', () => ({ + captureException: jest.fn(), +})); + +// Import after mocks are set up +const ErrorBoundary = require('../../../src/components/ErrorBoundary').default; +const { captureException } = require('../../../src/Sentry'); + +const ProblemChild = () => { + throw new Error('boom'); +}; + +const GoodChild = () => Good child; + +describe('ErrorBoundary', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('logs errors to Sentry with correct parameters', () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + render( + + + , + ); + + consoleError.mockRestore(); + expect(captureException).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ + componentStack: expect.any(String), + errorBoundary: true, + }), + ); + }); + + it('renders error UI when child component throws', () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const { getByText } = render( + + + , + ); + + consoleError.mockRestore(); + expect( + getByText('Something went wrong. Please restart the app.'), + ).toBeTruthy(); + }); + + it('calls analytics flush before logging to Sentry', () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + render( + + + , + ); + + consoleError.mockRestore(); + expect(mockFlush).toHaveBeenCalled(); + }); + + it('renders children normally when no error occurs', () => { + const { getByText } = render( + + + , + ); + + expect(getByText('Good child')).toBeTruthy(); + }); + + it('captures error details correctly', () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const testError = new Error('Test error message'); + const ProblemChildWithSpecificError = () => { + throw testError; + }; + + render( + + + , + ); + + consoleError.mockRestore(); + expect(captureException).toHaveBeenCalledWith( + testError, + expect.objectContaining({ + componentStack: expect.any(String), + errorBoundary: true, + }), + ); + }); + + it('handles multiple error boundaries correctly', () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const { getByText } = render( + + + + + , + ); + + consoleError.mockRestore(); + // Should show the error UI from the inner error boundary + expect( + getByText('Something went wrong. Please restart the app.'), + ).toBeTruthy(); + expect(captureException).toHaveBeenCalledTimes(1); + }); + + it('maintains error state after catching an error', () => { + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const { getByText, rerender } = render( + + + , + ); + + consoleError.mockRestore(); + + // Verify error UI is shown + expect( + getByText('Something went wrong. Please restart the app.'), + ).toBeTruthy(); + + // Rerender with a good child - should still show error UI + rerender( + + + , + ); + + // Should still show error UI, not the good child + expect( + getByText('Something went wrong. Please restart the app.'), + ).toBeTruthy(); + expect(() => getByText('Good child')).toThrow(); + }); +}); diff --git a/app/tests/src/providers/passportDataProvider.test.tsx b/app/tests/src/providers/passportDataProvider.test.tsx new file mode 100644 index 000000000..dab1b4843 --- /dev/null +++ b/app/tests/src/providers/passportDataProvider.test.tsx @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React, { useEffect, useState } from 'react'; +import { Text } from 'react-native'; + +// Import after mocking +import { + PassportProvider, + usePassport, +} from '../../../src/providers/passportDataProvider'; + +import { render, waitFor } from '@testing-library/react-native'; + +// Mock react-native-keychain before importing the module +const mockKeychain = { + getGenericPassword: jest.fn(), + setGenericPassword: jest.fn(), + resetGenericPassword: jest.fn(), +}; + +jest.mock('react-native-keychain', () => mockKeychain); + +// Mock the auth provider +const mockAuthProvider = { + _getSecurely: jest.fn(), +}; + +jest.mock('../../../src/providers/authProvider', () => ({ + useAuth: () => mockAuthProvider, +})); + +// Test component that uses the passport hook and extracts context values +const TestComponent = () => { + const passportContext = usePassport(); + const [contextValues, setContextValues] = useState([]); + + useEffect(() => { + // Extract function names from context to verify they exist + const functionNames = Object.keys(passportContext).filter( + key => + typeof passportContext[key as keyof typeof passportContext] === + 'function', + ); + setContextValues(functionNames); + }, [passportContext]); + + return ( + <> + + {contextValues.length} functions available + + {contextValues.join(',')} + getData available + setData available + + loadDocumentCatalog available + + + ); +}; + +// Component to test multiple consumers +const MultipleConsumersTest = () => { + const context1 = usePassport(); + const context2 = usePassport(); + + return ( + <> + + { + Object.keys(context1).filter( + key => typeof context1[key as keyof typeof context1] === 'function', + ).length + } + + + { + Object.keys(context2).filter( + key => typeof context2[key as keyof typeof context2] === 'function', + ).length + } + + + ); +}; + +// Component to test error boundaries +const ErrorBoundaryTest = () => { + // Simulate calling a context function that might throw + const testContextFunction = () => { + try { + // This would normally call a context function + return 'success'; + } catch (error) { + return 'error'; + } + }; + + return {testContextFunction()}; +}; + +// Component to test context updates +const ContextUpdateTest = () => { + const [updateCount, setUpdateCount] = useState(0); + + useEffect(() => { + // Simulate context updates + const interval = setInterval(() => { + setUpdateCount(prev => prev + 1); + }, 100); + return () => clearInterval(interval); + }, []); + + return {updateCount}; +}; + +describe('PassportDataProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + console.log = jest.fn(); + console.warn = jest.fn(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should provide context values to children', () => { + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('getData-available')).toBeTruthy(); + expect(getByTestId('setData-available')).toBeTruthy(); + expect(getByTestId('loadDocumentCatalog-available')).toBeTruthy(); + }); + + it('should provide all required context functions', () => { + const { getByTestId } = render( + + + , + ); + + const functionsCount = getByTestId('context-functions-count'); + expect(functionsCount.props.children[0]).toBeGreaterThan(15); // Should have many functions + + const functionsList = getByTestId('context-functions-list'); + expect(functionsList.props.children).toContain('getData'); + expect(functionsList.props.children).toContain('setData'); + expect(functionsList.props.children).toContain('loadDocumentCatalog'); + expect(functionsList.props.children).toContain('getAllDocuments'); + expect(functionsList.props.children).toContain('setSelectedDocument'); + expect(functionsList.props.children).toContain('deleteDocument'); + }); + + it('should support multiple consumers accessing the same context', () => { + const { getByTestId } = render( + + + , + ); + + const consumer1Functions = getByTestId('consumer1-functions'); + const consumer2Functions = getByTestId('consumer2-functions'); + + expect(consumer1Functions.props.children).toBeGreaterThan(0); + expect(consumer2Functions.props.children).toBeGreaterThan(0); + expect(consumer1Functions.props.children).toBe( + consumer2Functions.props.children, + ); + }); + + it('should handle context updates and trigger re-renders', async () => { + const { getByTestId } = render( + + + , + ); + + const updateCount = getByTestId('update-count'); + const initialCount = parseInt(updateCount.props.children); + + // Wait for updates to occur + await waitFor( + () => { + const newCount = parseInt(getByTestId('update-count').props.children); + expect(newCount).toBeGreaterThan(initialCount); + }, + { timeout: 1000 }, + ); + }); + + it('should handle errors gracefully in context consumers', () => { + const { getByTestId } = render( + + + , + ); + + const errorTestResult = getByTestId('error-test-result'); + expect(errorTestResult.props.children).toBe('success'); + }); + + it('should render without children gracefully', () => { + expect(() => { + render(); + }).not.toThrow(); + }); + + it('should provide consistent context values across re-renders', () => { + const { getByTestId, rerender } = render( + + + , + ); + + const initialFunctionsCount = getByTestId('context-functions-count').props + .children[0]; + + // Re-render the component + rerender( + + + , + ); + + const newFunctionsCount = getByTestId('context-functions-count').props + .children[0]; + expect(newFunctionsCount).toBe(initialFunctionsCount); + }); + + it('should maintain context stability across provider re-renders', () => { + const { getByTestId, rerender } = render( + + + , + ); + + const initialFunctionsList = getByTestId('context-functions-list').props + .children; + + // Re-render with different props + rerender( + + + , + ); + + const newFunctionsList = getByTestId('context-functions-list').props + .children; + expect(newFunctionsList).toBe(initialFunctionsList); + }); + + describe('initializeNativeModules', () => { + let initializeNativeModulesLocal: any; + + beforeEach(() => { + jest.clearAllMocks(); + // Reset module state for each test by re-importing + jest.resetModules(); + jest.doMock('react-native-keychain', () => mockKeychain); + + const passportModule = require('../../../src/providers/passportDataProvider'); + initializeNativeModulesLocal = passportModule.initializeNativeModules; + }); + + it('should return true immediately if native modules are already ready', async () => { + // Mock successful keychain response + mockKeychain.getGenericPassword = jest.fn().mockResolvedValue({ + password: 'test', + }); + + // First call should initialize + const firstResult = await initializeNativeModulesLocal(); + expect(firstResult).toBe(true); + expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(1); + + // Clear mock calls to verify subsequent calls don't hit keychain + jest.clearAllMocks(); + + // Subsequent calls should return immediately without hitting keychain + const secondResult = await initializeNativeModulesLocal(); + expect(secondResult).toBe(true); + expect(mockKeychain.getGenericPassword).not.toHaveBeenCalled(); + }); + + it('should handle "requiring unknown module" errors by retrying', async () => { + // Mock the error that occurs when native modules aren't ready + const moduleError = new Error( + 'Requiring unknown module "react-native-keychain"', + ); + mockKeychain.getGenericPassword = jest + .fn() + .mockRejectedValueOnce(moduleError) + .mockRejectedValueOnce(moduleError) + .mockResolvedValue({ password: 'test' }); + + const result = await initializeNativeModulesLocal(3, 10); // 3 retries, 10ms delay + + expect(result).toBe(true); + expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(3); + }); + + it('should return false after max retries if modules never become ready', async () => { + // Mock persistent module error + const moduleError = new Error( + 'Requiring unknown module "react-native-keychain"', + ); + mockKeychain.getGenericPassword = jest + .fn() + .mockRejectedValue(moduleError); + + const result = await initializeNativeModulesLocal(2, 10); // 2 retries, 10ms delay + + expect(result).toBe(false); + expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(2); + }); + + it('should handle other errors by assuming Keychain is available', async () => { + // Mock a different type of error (like service not found) + const otherError = new Error('Service not found'); + mockKeychain.getGenericPassword = jest.fn().mockRejectedValue(otherError); + + const result = await initializeNativeModulesLocal(); + + expect(result).toBe(true); + expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(1); + }); + }); + + describe('migrateFromLegacyStorage', () => { + let migrateFromLegacyStorageLocal: any; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + jest.doMock('react-native-keychain', () => mockKeychain); + + const passportModule = require('../../../src/providers/passportDataProvider'); + migrateFromLegacyStorageLocal = passportModule.migrateFromLegacyStorage; + }); + + it('should skip migration if catalog already has documents', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: JSON.stringify({ documents: [{ id: 'existing' }] }), + }); // For loadDocumentCatalog + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + + await migrateFromLegacyStorageLocal(); + + // Should log that migration is already completed + expect(consoleSpy).toHaveBeenCalledWith('Migration already completed'); + + consoleSpy.mockRestore(); + }); + + it('should migrate legacy documents when catalog is empty', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: JSON.stringify({ documents: [] }), + }) // For loadDocumentCatalog + .mockResolvedValueOnce({ + password: JSON.stringify({ documentType: 'passport', mrz: 'test' }), + }) // For legacy document + .mockResolvedValue(false); // No more legacy documents + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + + await migrateFromLegacyStorageLocal(); + + // Should log migration start and completion + expect(consoleSpy).toHaveBeenCalledWith( + 'Migrating from legacy storage to new architecture...', + ); + expect(consoleSpy).toHaveBeenCalledWith('Migration completed'); + + consoleSpy.mockRestore(); + }); + + it('should handle errors during migration gracefully', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: JSON.stringify({ documents: [] }), + }) // For loadDocumentCatalog + .mockRejectedValue(new Error('Keychain error')); // Error on legacy service + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + + await migrateFromLegacyStorageLocal(); + + // Should log error for each service that fails + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Could not migrate from service passportData:'), + expect.any(Error), + ); + + consoleSpy.mockRestore(); + }); + }); + + describe('loadDocumentCatalog', () => { + let loadDocumentCatalogLocal: any; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + jest.doMock('react-native-keychain', () => mockKeychain); + + const passportModule = require('../../../src/providers/passportDataProvider'); + loadDocumentCatalogLocal = passportModule.loadDocumentCatalog; + }); + + it('should return empty catalog when Keychain is undefined', async () => { + // Reset module registry to ensure mock takes effect + jest.resetModules(); + // Mock that Keychain is undefined + jest.doMock('react-native-keychain', () => undefined); + + // Re-import the module after mocking to ensure mock is applied + const passportModule = require('../../../src/providers/passportDataProvider'); + const loadDocumentCatalogLocal = passportModule.loadDocumentCatalog; + + const result = await loadDocumentCatalogLocal(); + + expect(result).toEqual({ documents: [] }); + }); + + it('should return empty catalog when no catalog exists', async () => { + mockKeychain.getGenericPassword = jest.fn().mockResolvedValue(false); + + const result = await loadDocumentCatalogLocal(); + + expect(result).toEqual({ documents: [] }); + }); + + it('should return empty catalog when native modules are not ready', async () => { + // Since nativeModulesReady is a module-level variable, we can't easily mock it + // The function will return empty catalog when native modules are not ready + mockKeychain.getGenericPassword = jest.fn().mockResolvedValue({ + password: JSON.stringify({ documents: [{ id: 'test' }] }), + }); + + const result = await loadDocumentCatalogLocal(); + + // The function should return empty catalog due to nativeModulesReady check + expect(result).toEqual({ documents: [] }); + }); + + it('should return parsed catalog when it exists and native modules are ready', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: JSON.stringify({ documents: [{ id: 'test' }] }), + }); // For loadDocumentCatalog + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + // Now test loadDocumentCatalog + const result = await loadDocumentCatalogLocal(); + + expect(result).toEqual({ documents: [{ id: 'test' }] }); + }); + + it('should handle malformed JSON and return empty documents array', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: '{"documents": [{"id": "test"}]', // Missing closing brace + }); // For loadDocumentCatalog + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + + const result = await loadDocumentCatalogLocal(); + + expect(result).toEqual({ documents: [] }); + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Error loading document catalog:', + expect.any(SyntaxError), + ); + + consoleLogSpy.mockRestore(); + }); + + it('should handle invalid catalog structure and return the parsed structure', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: JSON.stringify({ invalidField: 'test' }), // Missing documents array + }); // For loadDocumentCatalog + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const result = await loadDocumentCatalogLocal(); + + // The function returns the parsed JSON as-is, even if it doesn't have the expected structure + expect(result).toEqual({ invalidField: 'test' }); + }); + + it('should handle JSON parsing exceptions and return empty documents array', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: 'invalid json string', + }); // For loadDocumentCatalog + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + + const result = await loadDocumentCatalogLocal(); + + expect(result).toEqual({ documents: [] }); + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Error loading document catalog:', + expect.any(SyntaxError), + ); + + consoleLogSpy.mockRestore(); + }); + + it('should handle null/undefined password and return empty documents array', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: null, + }); // For loadDocumentCatalog + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + + console.log('About to call loadDocumentCatalogLocal'); + const result = await loadDocumentCatalogLocal(); + console.log('Called loadDocumentCatalogLocal'); + + console.log('Actual result:', result); + console.log('Result type:', typeof result); + console.log('Is null?', result === null); + console.log('Function name:', loadDocumentCatalogLocal.name); + + // When password is null, JSON.parse(null) throws TypeError, which is caught + // and the function returns empty catalog + expect(result).toEqual({ documents: [] }); + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Error loading document catalog:', + expect.any(TypeError), + ); + + consoleLogSpy.mockRestore(); + }); + + it('should handle empty string password and return empty documents array', async () => { + // First initialize native modules to set the flag + const passportModule = require('../../../src/providers/passportDataProvider'); + mockKeychain.getGenericPassword = jest + .fn() + .mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules + .mockResolvedValueOnce({ + password: '', + }); // For loadDocumentCatalog + + // Initialize native modules first + await passportModule.initializeNativeModules(); + + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + + const result = await loadDocumentCatalogLocal(); + + expect(result).toEqual({ documents: [] }); + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Error loading document catalog:', + expect.any(SyntaxError), + ); + + consoleLogSpy.mockRestore(); + }); + }); +}); diff --git a/app/tests/src/utils/jsonUtils.test.ts b/app/tests/src/utils/jsonUtils.test.ts new file mode 100644 index 000000000..7907b571e --- /dev/null +++ b/app/tests/src/utils/jsonUtils.test.ts @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { safeJsonParse, safeJsonStringify } from '../../../src/utils/jsonUtils'; + +describe('JSON Utils', () => { + describe('safeJsonParse', () => { + it('should parse valid JSON correctly', () => { + const validJson = '{"name": "test", "value": 123}'; + const result = safeJsonParse(validJson, null); + + expect(result).toEqual({ name: 'test', value: 123 }); + }); + + it('should return default value for invalid JSON', () => { + const invalidJson = '{"name": "test", "value": 123'; // Missing closing brace + const defaultValue = { error: 'parsing failed' }; + const result = safeJsonParse(invalidJson, defaultValue); + + expect(result).toEqual(defaultValue); + }); + + it('should return default value for malformed JSON', () => { + const malformedJson = 'not json at all'; + const defaultValue = null; + const result = safeJsonParse(malformedJson, defaultValue); + + expect(result).toBe(defaultValue); + }); + + it('should handle empty string', () => { + const emptyString = ''; + const defaultValue = {}; + const result = safeJsonParse(emptyString, defaultValue); + + expect(result).toEqual(defaultValue); + }); + + it('should handle null input', () => { + const nullInput = null as any; + const defaultValue = {}; + const result = safeJsonParse(nullInput, defaultValue); + + expect(result).toEqual(defaultValue); + }); + }); + + describe('safeJsonStringify', () => { + it('should stringify valid objects correctly', () => { + const obj = { name: 'test', value: 123 }; + const result = safeJsonStringify(obj); + + expect(result).toBe('{"name":"test","value":123}'); + }); + + it('should return default value for objects with circular references', () => { + const obj: any = { name: 'test' }; + obj.self = obj; // Create circular reference + const defaultValue = '{"error": "circular reference"}'; + const result = safeJsonStringify(obj, defaultValue); + + expect(result).toBe(defaultValue); + }); + + it('should handle functions gracefully', () => { + const obj = { + name: 'test', + func: () => 'test', + }; + const result = safeJsonStringify(obj); + + // JSON.stringify omits functions, so we should get the object without the function + expect(result).toBe('{"name":"test"}'); + }); + + it('should handle undefined input', () => { + const undefinedInput = undefined as any; + const defaultValue = '{}'; + const result = safeJsonStringify(undefinedInput, defaultValue); + + expect(result).toBe(defaultValue); + }); + + it('should use default default value when not provided', () => { + const obj: any = { func: () => 'test' }; + const result = safeJsonStringify(obj); + + expect(result).toBe('{}'); + }); + }); +});