Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[submodule "submodules/firmware"]
path = submodules/firmware
url = https://github.com/OneKeyHQ/firmware.git
[submodule "submodules/firmware-pro2"]
path = submodules/firmware-pro2
url = https://github.com/OneKeyHQ/firmware-pro2.git
branch = dev
25 changes: 25 additions & 0 deletions packages/core/__tests__/DeviceCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ const createCommands = () => {
};

describe('DeviceCommands failure mapping', () => {
it('maps Protocol V2 DeviceLocked failure to a structured hardware error', async () => {
const commands = createCommands();

await expect(
commands._filterCommonTypes(
{
type: 'Failure',
message: {
code: 'Failure_ProcessError',
subcode: 9,
message: 'Device locked',
},
} as any,
'DeviceSettingsPageShow'
)
).rejects.toMatchObject({
errorCode: HardwareErrorCode.DeviceLocked,
params: {
failureCode: 'Failure_ProcessError',
subcode: 9,
firmwareMessage: 'Device locked',
},
});
});

it.each([
['ButtonAck', 'Not in Ethereum signing mode'],
['PinMatrixAck', 'Not in Conflux signing mode'],
Expand Down
34 changes: 34 additions & 0 deletions packages/core/__tests__/RequestQueue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import RequestQueue from '../src/core/RequestQueue';

import type { BaseMethod } from '../src/api/BaseMethod';

jest.mock('../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
}));

function buildMethod(responseID: number, operationId?: string) {
return {
payload: { operationId },
responseID,
} as unknown as BaseMethod;
}

describe('RequestQueue operation cancellation', () => {
test('aborts only the request matching the operation id', () => {
const queue = new RequestQueue();
const portfolio = queue.createTask(buildMethod(1, 'portfolio-1'));
const userAction = queue.createTask(buildMethod(2, 'user-action'));

expect(queue.abortOperation('portfolio-1')).toBe(true);
expect(portfolio.abortController?.signal.aborted).toBe(true);
expect(userAction.abortController?.signal.aborted).toBe(false);
});

test('returns false for an unknown operation id', () => {
const queue = new RequestQueue();
queue.createTask(buildMethod(1, 'portfolio-1'));

expect(queue.abortOperation('missing')).toBe(false);
});
});
19 changes: 19 additions & 0 deletions packages/core/__tests__/connectSettings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { parseConnectSettings } from '../src/data-manager/connectSettings';

jest.mock('../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
}));

describe('parseConnectSettings', () => {
it('keeps custom configFetcher in parsed settings', () => {
const configFetcher = jest.fn();

const settings = parseConnectSettings({
fetchConfig: true,
configFetcher,
});

expect(settings.configFetcher).toBe(configFetcher);
});
});
131 changes: 131 additions & 0 deletions packages/core/__tests__/device-wallet-session-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import ClearSessionCache from '../src/api/ClearSessionCache';
import {
DeviceWalletSessionStore,
deviceWalletSessionStore,
} from '../src/device/DeviceWalletSessionStore';
import { createCoreApi } from '../src/inject';

jest.mock('../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
}));

describe('DeviceWalletSessionStore', () => {
test('requires passphraseState for wallet session lookup', () => {
const store = new DeviceWalletSessionStore();
store.set('device-1', 'hidden-a', 'session-a');

expect(store.get('device-1', undefined)).toBeUndefined();
expect(store.get('device-1', 'hidden-a')).toBe('session-a');
});

test('isolates wallets and devices', () => {
const store = new DeviceWalletSessionStore();
store.set('device-1', 'hidden-a', 'session-a');
store.set('device-1', 'hidden-b', 'session-b');
store.set('device-2', 'hidden-a', 'session-c');

expect(store.get('device-1', 'hidden-a')).toBe('session-a');
expect(store.get('device-1', 'hidden-b')).toBe('session-b');
expect(store.get('device-2', 'hidden-a')).toBe('session-c');
});

test('keeps pending sessions unreadable until wallet binding', () => {
const store = new DeviceWalletSessionStore();
store.setPending('device-1', 'pending-session');

expect(store.get('device-1', undefined)).toBeUndefined();
expect(store.getPending('device-1')).toBe('pending-session');
});

test('migrates descriptor keys to stable device ids', () => {
const store = new DeviceWalletSessionStore();
store.set('ble-path', 'hidden-a', 'session-a');
store.setPending('ble-path', 'pending-session');

store.migrateDeviceKey('ble-path', 'stable-device-id');

expect(store.get('ble-path', 'hidden-a')).toBeUndefined();
expect(store.get('stable-device-id', 'hidden-a')).toBe('session-a');
expect(store.getPending('ble-path')).toBeUndefined();
expect(store.getPending('stable-device-id')).toBe('pending-session');
});

test('clears one wallet, one device, or all sessions', () => {
const store = new DeviceWalletSessionStore();
store.set('device-1', 'hidden-a', 'session-a');
store.set('device-1', 'hidden-b', 'session-b');
store.set('device-2', 'hidden-a', 'session-c');

store.delete('device-1', 'hidden-a');
expect(store.get('device-1', 'hidden-a')).toBeUndefined();
expect(store.get('device-1', 'hidden-b')).toBe('session-b');

store.deleteDevice('device-1');
expect(store.get('device-1', 'hidden-b')).toBeUndefined();
expect(store.get('device-2', 'hidden-a')).toBe('session-c');

store.clear();
expect(store.get('device-2', 'hidden-a')).toBeUndefined();
});
});

describe('ClearSessionCache', () => {
beforeEach(() => {
deviceWalletSessionStore.clear();
});

test('clears one wallet without using a device', async () => {
deviceWalletSessionStore.set('device-1', 'hidden-a', 'session-a');
deviceWalletSessionStore.set('device-1', 'hidden-b', 'session-b');
const method = new ClearSessionCache({
payload: {
method: 'clearSessionCache',
deviceId: 'device-1',
passphraseState: 'hidden-a',
},
});

method.init();

expect(method.useDevice).toBe(false);
await expect(method.run()).resolves.toEqual({ cleared: true });
expect(deviceWalletSessionStore.get('device-1', 'hidden-a')).toBeUndefined();
expect(deviceWalletSessionStore.get('device-1', 'hidden-b')).toBe('session-b');
});

test('clears one device or the full runtime store', async () => {
deviceWalletSessionStore.set('device-1', 'hidden-a', 'session-a');
deviceWalletSessionStore.set('device-2', 'hidden-a', 'session-b');

const clearDevice = new ClearSessionCache({
payload: { method: 'clearSessionCache', deviceId: 'device-1' },
});
clearDevice.init();
await clearDevice.run();

expect(deviceWalletSessionStore.get('device-1', 'hidden-a')).toBeUndefined();
expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBe('session-b');

const clearAll = new ClearSessionCache({
payload: { method: 'clearSessionCache' },
});
clearAll.init();
await clearAll.run();

expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBeUndefined();
});

test('routes cache clearing through the active CoreApi call channel', async () => {
const call = jest.fn().mockResolvedValue({ success: true, payload: { cleared: true } });
const api = createCoreApi(call as any);

await api.clearSessionCache({ deviceId: 'device-1', passphraseState: 'hidden-a' });

expect(call).toHaveBeenCalledWith({
method: 'clearSessionCache',
deviceId: 'device-1',
passphraseState: 'hidden-a',
});
});
});
19 changes: 15 additions & 4 deletions packages/core/__tests__/evmLedgerLegacySafety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import AllNetworkGetAddressBase from '../src/api/allnetwork/AllNetworkGetAddress
import EvmGetAddress from '../src/api/evm/EVMGetAddress';
import EVMGetPublicKey from '../src/api/evm/EVMGetPublicKey';
import { findMethod } from '../src/api/utils';
import { getDeviceType, getFirmwareType, getMethodVersionRange } from '../src/utils';
import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';

jest.mock('../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
Expand All @@ -14,16 +16,25 @@ jest.mock('../src/api/utils', () => ({

const createDevice = (onekeyDeviceType: string) => {
const typedCall = jest.fn();
const features = {
deviceType: onekeyDeviceType.toLowerCase(),
safety_checks: 'Strict',
};
return {
typedCall,
device: {
features: {
onekey_device_type: onekeyDeviceType,
safety_checks: 'Strict',
},
features,
commands: {
typedCall,
},
// BaseMethod 通过 Device accessor 读取设备状态,stub 复用真实工具函数保持映射一致
isProtocolV2: () => false,
getCurrentDeviceType: () => getDeviceType(features as any),
getCurrentSafetyChecks: () => features.safety_checks,
getCurrentFirmwareType: () => getFirmwareType(features as any),
getCurrentFirmwareVersionString: () => getDeviceFirmwareVersion(features as any)?.join('.'),
getCurrentMethodVersionRange: (fn: (model: any) => any) =>
getMethodVersionRange(features as any, fn),
},
};
};
Expand Down
2 changes: 1 addition & 1 deletion packages/core/__tests__/evmSignTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jest.mock('../src/data/config', () => ({

// Mock the device and transport manager
jest.mock('../src/data-manager/TransportManager', () => ({
getMessageVersion: jest.fn(() => 'v2'),
getProtocolV1MessageSchema: jest.fn(() => 'v1CurrentSchema'),
}));

jest.mock('../src/device/Device', () => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/core/__tests__/evmSignTypedData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jest.mock('../src/data/config', () => ({
}));

jest.mock('../src/data-manager/TransportManager', () => ({
getMessageVersion: jest.fn(() => 'v2'),
getProtocolV1MessageSchema: jest.fn(() => 'v1CurrentSchema'),
}));

jest.mock('../src/device/Device', () => ({
Expand Down
37 changes: 37 additions & 0 deletions packages/core/__tests__/logBlockEvent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { UI_RESPONSE, getLogBlockLabel } from '../src/events';

describe('getLogBlockLabel', () => {
it('blocks evmSignTypedData params before logging large typed data', () => {
expect(
getLogBlockLabel({
method: 'evmSignTypedData',
data: {
message: {
data: `0x${'ab'.repeat(4096)}`,
},
},
})
).toBe('evmSignTypedData');
});

it('blocks evmSignTypedData iframe call payload before bridge logging', () => {
expect(
getLogBlockLabel({
event: 'iframe-call',
type: 'iframe-call',
payload: {
method: 'evmSignTypedData',
data: {
message: {
data: `0x${'ab'.repeat(4096)}`,
},
},
},
})
).toBe('evmSignTypedData');
});

it('keeps existing sensitive UI response blocking', () => {
expect(getLogBlockLabel({ type: UI_RESPONSE.RECEIVE_PIN })).toBe(UI_RESPONSE.RECEIVE_PIN);
});
});
33 changes: 33 additions & 0 deletions packages/core/__tests__/preInitialize.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Device } from '../src/device/Device';
import { initCore } from '../src/core';
import PreInitialize from '../src/api/device/PreInitialize';
import { IFRAME } from '../src/events';

import type Core from '../src/core';

jest.mock('../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
}));

describe('preInitialize', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('matches pre-initialized state by passphraseState only', () => {
const device = Object.create(Device.prototype) as Device;

Expand All @@ -19,4 +28,28 @@ describe('preInitialize', () => {
})
).toBe(true);
});

it('cleans request lifecycle when preInitialize is acknowledged without connectId', async () => {
const core: Core = initCore();
const disposeSpy = jest.spyOn(PreInitialize.prototype, 'dispose');

try {
const response = await core.handleMessage({
id: 1001,
type: IFRAME.CALL,
payload: {
method: 'preInitialize',
},
} as any);

expect(response).toMatchObject({
success: true,
payload: true,
});
expect((core as any).tracingContext.activeRequests.has(1001)).toBe(false);
expect(disposeSpy).toHaveBeenCalledTimes(1);
} finally {
core.dispose();
}
});
});
Loading
Loading