Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/utils/deeplinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const decodeUrl = (encodedUrl: string): string => {
}
};

const handleUrl = (uri: string) => {
export const handleUrl = (uri: string) => {
const decodedUri = decodeUrl(uri);
const encodedData = queryString.parseUrl(decodedUri).query;
const sessionId = encodedData.sessionId;
Expand Down
59 changes: 59 additions & 0 deletions app/tests/src/hooks/useAppUpdates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 { useNavigation } from '@react-navigation/native';
import { act, renderHook } from '@testing-library/react-native';

jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));

jest.mock('react-native-check-version', () => ({
checkVersion: jest.fn(),
}));

jest.mock('../../../src/utils/modalCallbackRegistry', () => ({
registerModalCallbacks: jest.fn().mockReturnValue(1),
}));

jest.mock('../../../src/utils/analytics', () => () => ({
trackEvent: jest.fn(),
}));

import { checkVersion } from 'react-native-check-version';

import { useAppUpdates } from '../../../src/hooks/useAppUpdates';
import { registerModalCallbacks } from '../../../src/utils/modalCallbackRegistry';

const navigate = jest.fn();
(useNavigation as jest.Mock).mockReturnValue({ navigate });

describe('useAppUpdates', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('indicates update available', async () => {
(checkVersion as jest.Mock).mockResolvedValue({
needsUpdate: true,
url: 'u',
});
const { result } = renderHook(() => useAppUpdates());
await act(async () => {
await Promise.resolve();
});
expect(result.current[0]).toBe(true);
});

it('shows modal when triggered', () => {
(checkVersion as jest.Mock).mockResolvedValue({
needsUpdate: true,
url: 'u',
});
const { result } = renderHook(() => useAppUpdates());
act(() => {
result.current[1]();
});
expect(registerModalCallbacks).toHaveBeenCalled();
expect(navigate).toHaveBeenCalledWith('Modal', expect.any(Object));
});
});
42 changes: 42 additions & 0 deletions app/tests/src/hooks/useConnectionModal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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 { act, renderHook } from '@testing-library/react-native';

jest.useFakeTimers();

jest.mock('../../../src/navigation', () => ({
navigationRef: { isReady: jest.fn(() => true), navigate: jest.fn() },
}));

jest.mock('../../../src/hooks/useModal');
jest.mock('@react-native-community/netinfo', () => ({
useNetInfo: jest
.fn()
.mockReturnValue({ isConnected: false, isInternetReachable: false }),
}));

import useConnectionModal from '../../../src/hooks/useConnectionModal';
import { useModal } from '../../../src/hooks/useModal';

const showModal = jest.fn();
const dismissModal = jest.fn();
(useModal as jest.Mock).mockReturnValue({
showModal,
dismissModal,
visible: false,
});

describe('useConnectionModal', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('shows modal when no connection', () => {
const { result } = renderHook(() => useConnectionModal());
act(() => {
jest.advanceTimersByTime(2000);
});
expect(showModal).toHaveBeenCalled();
expect(result.current.visible).toBe(false);
});
});
59 changes: 59 additions & 0 deletions app/tests/src/hooks/useHapticNavigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 { useNavigation } from '@react-navigation/native';
import { act, renderHook } from '@testing-library/react-native';

import {
impactLight,
impactMedium,
selectionChange,
} from '../../../src/utils/haptic';

jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));

jest.mock('../../../src/utils/haptic', () => ({
impactLight: jest.fn(),
impactMedium: jest.fn(),
selectionChange: jest.fn(),
}));

const navigate = jest.fn();
const popTo = jest.fn();
(useNavigation as jest.Mock).mockReturnValue({ navigate, popTo });

import useHapticNavigation from '../../../src/hooks/useHapticNavigation';

describe('useHapticNavigation', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('navigates with light impact by default', () => {
const { result } = renderHook(() => useHapticNavigation('Home'));
act(() => {
result.current();
});
expect(impactLight).toHaveBeenCalled();
expect(navigate).toHaveBeenCalledWith('Home');
});

it('uses cancel action', () => {
const { result } = renderHook(() =>
useHapticNavigation('Home', { action: 'cancel' }),
);
act(() => result.current());
expect(selectionChange).toHaveBeenCalled();
expect(popTo).toHaveBeenCalledWith('Home');
});

it('uses confirm action', () => {
const { result } = renderHook(() =>
useHapticNavigation('Home', { action: 'confirm' }),
);
act(() => result.current());
expect(impactMedium).toHaveBeenCalled();
expect(navigate).toHaveBeenCalledWith('Home');
});
});
45 changes: 45 additions & 0 deletions app/tests/src/hooks/useMnemonic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 { act, renderHook } from '@testing-library/react-native';

jest.mock('../../../src/providers/authProvider', () => ({
useAuth: jest.fn(),
}));

import useMnemonic from '../../../src/hooks/useMnemonic';
import { useAuth } from '../../../src/providers/authProvider';

jest.mock('ethers', () => ({
ethers: {
Mnemonic: {
fromEntropy: jest.fn().mockReturnValue({ phrase: 'one two three four' }),
},
},
}));

const getOrCreateMnemonic = jest.fn();
(useAuth as jest.Mock).mockReturnValue({ getOrCreateMnemonic });

describe('useMnemonic', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('loads mnemonic', async () => {
getOrCreateMnemonic.mockResolvedValue({ data: { entropy: '0x00' } });
const { result } = renderHook(() => useMnemonic());
await act(async () => {
await result.current.loadMnemonic();
});
expect(result.current.mnemonic).toEqual(['one', 'two', 'three', 'four']);
});

it('handles missing mnemonic', async () => {
getOrCreateMnemonic.mockResolvedValue(null);
const { result } = renderHook(() => useMnemonic());
await act(async () => {
await result.current.loadMnemonic();
});
expect(result.current.mnemonic).toBeUndefined();
});
});
104 changes: 104 additions & 0 deletions app/tests/utils/deeplinks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 { Linking } from 'react-native';

jest.mock('../../src/navigation', () => ({
navigationRef: { navigate: jest.fn(), isReady: jest.fn(() => true) },
}));

const mockSelfAppStore = { useSelfAppStore: { getState: jest.fn() } };
jest.mock('../../src/stores/selfAppStore', () => mockSelfAppStore);

const mockUserStore = { default: { getState: jest.fn() } };
jest.mock('../../src/stores/userStore', () => ({
__esModule: true,
...mockUserStore,
}));

let setSelfApp: jest.Mock, startAppListener: jest.Mock, cleanSelfApp: jest.Mock;
let setDeepLinkUserDetails: jest.Mock;

let handleUrl: (url: string) => void;
let setupUniversalLinkListenerInNavigation: () => () => void;

describe('deeplinks', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
({
handleUrl,
setupUniversalLinkListenerInNavigation,
} = require('../../src/utils/deeplinks'));
setSelfApp = jest.fn();
startAppListener = jest.fn();
cleanSelfApp = jest.fn();
setDeepLinkUserDetails = jest.fn();
jest.spyOn(Linking, 'getInitialURL').mockResolvedValue(null as any);
jest
.spyOn(Linking, 'addEventListener')
.mockReturnValue({ remove: jest.fn() } as any);
mockSelfAppStore.useSelfAppStore.getState.mockReturnValue({
setSelfApp,
startAppListener,
cleanSelfApp,
});
mockUserStore.default.getState.mockReturnValue({
setDeepLinkUserDetails,
});
});

it('handles selfApp parameter', () => {
const selfApp = { sessionId: 'abc' };
const url = `scheme://open?selfApp=${encodeURIComponent(JSON.stringify(selfApp))}`;
handleUrl(url);

expect(setSelfApp).toHaveBeenCalledWith(selfApp);
expect(startAppListener).toHaveBeenCalledWith('abc');
const { navigationRef } = require('../../src/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('ProveScreen');
});

it('handles sessionId parameter', () => {
const url = 'scheme://open?sessionId=123';
handleUrl(url);

expect(cleanSelfApp).toHaveBeenCalled();
expect(startAppListener).toHaveBeenCalledWith('123');
const { navigationRef } = require('../../src/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('ProveScreen');
});

it('handles mock_passport parameter', () => {
const mockData = { name: 'John', surname: 'Doe' };
const url = `scheme://open?mock_passport=${encodeURIComponent(JSON.stringify(mockData))}`;
handleUrl(url);

expect(setDeepLinkUserDetails).toHaveBeenCalledWith({
name: 'John',
surname: 'Doe',
nationality: undefined,
birthDate: undefined,
gender: undefined,
});
const { navigationRef } = require('../../src/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('MockDataDeepLink');
});

it('navigates to QRCodeTrouble for invalid data', () => {
const url = 'scheme://open?selfApp=%7Binvalid';
handleUrl(url);
const { navigationRef } = require('../../src/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('QRCodeTrouble');
});

it('setup listener registers and cleans up', () => {
const remove = jest.fn();
(Linking.getInitialURL as jest.Mock).mockResolvedValue(undefined);
(Linking.addEventListener as jest.Mock).mockReturnValue({ remove });

const cleanup = setupUniversalLinkListenerInNavigation();
expect(Linking.addEventListener).toHaveBeenCalled();
cleanup();
expect(remove).toHaveBeenCalled();
});
});
56 changes: 56 additions & 0 deletions app/tests/utils/nfcScanner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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 { Platform } from 'react-native';

import { parseScanResponse } from '../../src/utils/nfcScanner';

describe('parseScanResponse', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('parses iOS response', () => {
Platform.OS = 'ios';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid direct Platform.OS mutation to prevent test pollution.

Directly mutating Platform.OS can cause test pollution and unpredictable test behavior. Use a more robust approach to mock platform-specific behavior.

-    Platform.OS = 'ios';
+    jest.spyOn(Platform, 'OS', 'get').mockReturnValue('ios');

Apply the same pattern for the Android test case.

🤖 Prompt for AI Agents
In app/tests/utils/nfcScanner.test.ts at line 13, avoid directly mutating
Platform.OS as it can cause test pollution. Instead, use a mocking library or
jest.spyOn to mock Platform.OS for the duration of the test, ensuring it is
restored after the test completes. Apply this pattern consistently for both iOS
and Android test cases to isolate platform-specific behavior safely.

const mrz =
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
const response = JSON.stringify({
dataGroupHashes: JSON.stringify({
DG1: { sodHash: 'abcd' },
DG2: { sodHash: '1234' },
}),
eContentBase64: Buffer.from('ec').toString('base64'),
signedAttributes: Buffer.from('sa').toString('base64'),
passportMRZ: mrz,
signatureBase64: Buffer.from([1, 2]).toString('base64'),
dataGroupsPresent: [1, 2],
passportPhoto: 'photo',
documentSigningCertificate: JSON.stringify({ PEM: 'CERT' }),
});

const result = parseScanResponse(response);
expect(result.mrz).toBe(mrz);
expect(result.documentType).toBe('passport');
expect(result.dg1Hash).toEqual([171, 205]);
expect(result.dg2Hash).toEqual([18, 52]);
});

it('parses Android response', () => {
Platform.OS = 'android';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid direct Platform.OS mutation to prevent test pollution.

Same issue as the iOS test - use jest.spyOn instead of direct mutation.

-    Platform.OS = 'android';
+    jest.spyOn(Platform, 'OS', 'get').mockReturnValue('android');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('parses Android response', () => {
Platform.OS = 'android';
it('parses Android response', () => {
jest.spyOn(Platform, 'OS', 'get').mockReturnValue('android');
🤖 Prompt for AI Agents
In app/tests/utils/nfcScanner.test.ts around lines 37 to 38, avoid directly
mutating Platform.OS as it can cause test pollution. Instead, use jest.spyOn to
mock Platform.OS for the duration of the test. This involves spying on the
Platform module's OS property and providing a mock implementation that returns
'android', ensuring the original value is restored after the test.

const mrz =
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
const response = {
mrz,
eContent: JSON.stringify([4, 5]),
encryptedDigest: JSON.stringify([6, 7]),
encapContent: JSON.stringify([8, 9]),
documentSigningCertificate: 'CERT',
dataGroupHashes: JSON.stringify({ '1': 'abcd', '2': [1, 2, 3] }),
} as any;

const result = parseScanResponse(response);
expect(result.documentType).toBe('passport');
expect(result.mrz).toBe(mrz);
expect(result.dg1Hash).toEqual([171, 205]);
expect(result.dgPresents).toEqual([1, 2]);
});
});
Loading
Loading