@onekeyfe/hardware-cli 1.2.0-alpha.4 → 1.2.0-alpha.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +86 -1
- package/dist/cli.js +364 -273
- package/dist/deviceSelection.d.ts +3 -0
- package/dist/deviceSelection.js +11 -0
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/sdk.d.ts +2 -2
- package/dist/sdk.js +5 -5
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.js +113 -70
- package/package.json +7 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-selection.test.ts +29 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +18 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +100 -0
- package/src/__tests__/noble-ble-plugin.test.ts +370 -0
- package/src/__tests__/wallet-session.test.ts +55 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +459 -321
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/sdk.ts +6 -6
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +148 -76
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { getCanonicalDeviceState, getCompatibleFeatures } from '../deviceStateCommands';
|
|
2
|
+
|
|
3
|
+
const createSdkMock = () => ({
|
|
4
|
+
searchDevices: jest.fn(),
|
|
5
|
+
getDeviceState: jest.fn(),
|
|
6
|
+
getFeatures: jest.fn(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('设备状态 CLI 兼容层', () => {
|
|
10
|
+
test('Protocol V2 直接返回 SDK 搜索结果中的兼容 features', async () => {
|
|
11
|
+
const sdk = createSdkMock();
|
|
12
|
+
const features = {
|
|
13
|
+
protocol: 'V2',
|
|
14
|
+
deviceType: 'pro2',
|
|
15
|
+
deviceId: 'device-id',
|
|
16
|
+
};
|
|
17
|
+
sdk.searchDevices.mockResolvedValue({
|
|
18
|
+
success: true,
|
|
19
|
+
payload: [
|
|
20
|
+
{
|
|
21
|
+
connectId: 'pro2-connect-id',
|
|
22
|
+
state: { protocol: 'V2' },
|
|
23
|
+
features,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await expect(getCompatibleFeatures(sdk as never, 'pro2-connect-id')).resolves.toEqual({
|
|
29
|
+
success: true,
|
|
30
|
+
payload: features,
|
|
31
|
+
});
|
|
32
|
+
expect(sdk.getFeatures).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('Protocol V1 继续调用公共 getFeatures 保持旧行为', async () => {
|
|
36
|
+
const sdk = createSdkMock();
|
|
37
|
+
const response = {
|
|
38
|
+
success: true,
|
|
39
|
+
payload: { protocol: 'V1', label: 'Classic' },
|
|
40
|
+
};
|
|
41
|
+
sdk.searchDevices.mockResolvedValue({
|
|
42
|
+
success: true,
|
|
43
|
+
payload: [
|
|
44
|
+
{
|
|
45
|
+
connectId: 'classic-connect-id',
|
|
46
|
+
state: { protocol: 'V1' },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
sdk.getFeatures.mockResolvedValue(response);
|
|
51
|
+
|
|
52
|
+
await expect(getCompatibleFeatures(sdk as never, 'classic-connect-id')).resolves.toBe(response);
|
|
53
|
+
expect(sdk.getFeatures).toHaveBeenCalledWith('classic-connect-id');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('未显式指定设备时选择搜索结果中的第一台设备', async () => {
|
|
57
|
+
const sdk = createSdkMock();
|
|
58
|
+
const response = { success: true, payload: { protocol: 'V1' } };
|
|
59
|
+
sdk.searchDevices.mockResolvedValue({
|
|
60
|
+
success: true,
|
|
61
|
+
payload: [{ connectId: 'first-device', state: { protocol: 'V1' } }],
|
|
62
|
+
});
|
|
63
|
+
sdk.getFeatures.mockResolvedValue(response);
|
|
64
|
+
|
|
65
|
+
await expect(getCompatibleFeatures(sdk as never)).resolves.toBe(response);
|
|
66
|
+
expect(sdk.getFeatures).toHaveBeenCalledWith('first-device');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('显式 connectId 不在搜索结果时返回结构化错误', async () => {
|
|
70
|
+
const sdk = createSdkMock();
|
|
71
|
+
sdk.searchDevices.mockResolvedValue({
|
|
72
|
+
success: true,
|
|
73
|
+
payload: [{ connectId: 'another-device', state: { protocol: 'V1' } }],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await expect(getCompatibleFeatures(sdk as never, 'missing-device')).resolves.toEqual({
|
|
77
|
+
success: false,
|
|
78
|
+
payload: {
|
|
79
|
+
code: 'DEVICE_NOT_FOUND',
|
|
80
|
+
error: 'Device not found: missing-device',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
expect(sdk.getFeatures).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('get-state 将 firmware scope 传给 SDK', async () => {
|
|
87
|
+
const sdk = createSdkMock();
|
|
88
|
+
const response = { success: true, payload: { protocol: 'V2' } };
|
|
89
|
+
sdk.searchDevices.mockResolvedValue({
|
|
90
|
+
success: true,
|
|
91
|
+
payload: [{ connectId: 'pro2-connect-id', state: { protocol: 'V2' } }],
|
|
92
|
+
});
|
|
93
|
+
sdk.getDeviceState.mockResolvedValue(response);
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
getCanonicalDeviceState(sdk as never, 'pro2-connect-id', 'firmware')
|
|
97
|
+
).resolves.toBe(response);
|
|
98
|
+
expect(sdk.getDeviceState).toHaveBeenCalledWith('pro2-connect-id', {
|
|
99
|
+
scope: 'firmware',
|
|
100
|
+
});
|
|
101
|
+
expect(sdk.searchDevices).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('get-state 未指定 connectId 时搜索并选择第一台设备', async () => {
|
|
105
|
+
const sdk = createSdkMock();
|
|
106
|
+
const state = { protocol: 'V2', revision: 2 };
|
|
107
|
+
sdk.searchDevices.mockResolvedValue({
|
|
108
|
+
success: true,
|
|
109
|
+
payload: [{ connectId: 'pro2-connect-id', state }],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await expect(getCanonicalDeviceState(sdk as never, undefined, 'runtime')).resolves.toEqual({
|
|
113
|
+
success: true,
|
|
114
|
+
payload: state,
|
|
115
|
+
});
|
|
116
|
+
expect(sdk.getDeviceState).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getLegacyFirmwareConnectTimeout, program } from '../cli';
|
|
2
|
+
|
|
3
|
+
describe('firmware-update-legacy CLI command', () => {
|
|
4
|
+
test('提供 Classic/Pure 的本地固件升级命令', () => {
|
|
5
|
+
const command = program.commands.find(item => item.name() === 'firmware-update-legacy');
|
|
6
|
+
|
|
7
|
+
expect(command).toBeDefined();
|
|
8
|
+
expect(command?.description()).toBe('Update Classic/Pure firmware through the legacy protocol');
|
|
9
|
+
expect(command?.options.find(option => option.long === '--binary')?.mandatory).toBe(true);
|
|
10
|
+
expect(command?.options.some(option => option.long === '--device-name')).toBe(true);
|
|
11
|
+
expect(command?.options.some(option => option.long === '--update-type')).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('USB Classic 固件升级使用足够的设备探测超时', () => {
|
|
15
|
+
expect(getLegacyFirmwareConnectTimeout('usb')).toBe(90_000);
|
|
16
|
+
expect(getLegacyFirmwareConnectTimeout('ble')).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createSDK, disposeSDK } from '../sdk';
|
|
2
|
+
import { program, runFirmwareUpdateV4WithRetry } from '../cli';
|
|
3
|
+
|
|
4
|
+
jest.mock('../sdk', () => ({
|
|
5
|
+
createSDK: jest.fn(),
|
|
6
|
+
disposeSDK: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const transientProbeFailure = {
|
|
10
|
+
success: false,
|
|
11
|
+
payload: {
|
|
12
|
+
error: 'Device protocol mismatch: expected V2; device did not respond to expected protocol',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createSdkMock = () => ({
|
|
17
|
+
getDeviceState: jest.fn(),
|
|
18
|
+
firmwareUpdateV4: jest.fn(),
|
|
19
|
+
on: jest.fn(),
|
|
20
|
+
off: jest.fn(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('firmware-update-v4 CLI command', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('exposes firmware-update-v4 as the formal command', () => {
|
|
33
|
+
const command = program.commands.find(item => item.name() === 'firmware-update-v4');
|
|
34
|
+
|
|
35
|
+
expect(command).toBeDefined();
|
|
36
|
+
expect(command?.description()).toBe(
|
|
37
|
+
'Run Protocol V2 firmware update through sdk.firmwareUpdateV4'
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('does not expose the pre-release firmware-update-v4-debug command', () => {
|
|
42
|
+
expect(program.commands.some(item => item.name() === 'firmware-update-v4-debug')).toBe(false);
|
|
43
|
+
expect(program.commands.some(item => item.aliases().includes('firmware-update-v4-debug'))).toBe(
|
|
44
|
+
false
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('retries only the read-only USB probe before starting the firmware update', async () => {
|
|
49
|
+
const firstSdk = createSdkMock();
|
|
50
|
+
const retrySdk = createSdkMock();
|
|
51
|
+
firstSdk.getDeviceState.mockResolvedValue(transientProbeFailure);
|
|
52
|
+
retrySdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
|
|
53
|
+
retrySdk.firmwareUpdateV4.mockResolvedValue({ success: true, payload: {} });
|
|
54
|
+
jest.mocked(createSDK).mockResolvedValue(retrySdk as never);
|
|
55
|
+
jest.mocked(disposeSDK).mockResolvedValue(undefined);
|
|
56
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(callback => {
|
|
57
|
+
callback();
|
|
58
|
+
return 0 as never;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
62
|
+
sdk: firstSdk as never,
|
|
63
|
+
globalOpts: { transport: 'usb', connectId: 'stale-connect-id' },
|
|
64
|
+
params: { applicationP1Binary: new ArrayBuffer(1) } as never,
|
|
65
|
+
retries: 1,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(firstSdk.firmwareUpdateV4).not.toHaveBeenCalled();
|
|
69
|
+
expect(disposeSDK).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(retrySdk.getDeviceState).toHaveBeenCalledWith(undefined, {
|
|
71
|
+
scope: 'runtime',
|
|
72
|
+
connectProtocol: 'V2',
|
|
73
|
+
retryCount: 0,
|
|
74
|
+
});
|
|
75
|
+
expect(retrySdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(result).toMatchObject({ success: true, payload: { metrics: { attempt: 2 } } });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('does not replay firmwareUpdateV4 after the read-only probe succeeds', async () => {
|
|
80
|
+
const sdk = createSdkMock();
|
|
81
|
+
sdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
|
|
82
|
+
sdk.firmwareUpdateV4.mockResolvedValue(transientProbeFailure);
|
|
83
|
+
|
|
84
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
85
|
+
sdk: sdk as never,
|
|
86
|
+
globalOpts: { transport: 'usb', connectId: 'pro2-connect-id' },
|
|
87
|
+
params: { applicationP1Binary: new ArrayBuffer(1) } as never,
|
|
88
|
+
retries: 2,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(sdk.getDeviceState).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(sdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(disposeSDK).not.toHaveBeenCalled();
|
|
94
|
+
expect(createSDK).not.toHaveBeenCalled();
|
|
95
|
+
expect(result).toMatchObject({
|
|
96
|
+
success: false,
|
|
97
|
+
payload: { error: transientProbeFailure.payload.error, metrics: { attempt: 1 } },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
type MockCharacteristic = EventEmitter & {
|
|
4
|
+
uuid: string;
|
|
5
|
+
unsubscribe: jest.Mock;
|
|
6
|
+
subscribe: jest.Mock;
|
|
7
|
+
write: jest.Mock;
|
|
8
|
+
removeAllListeners: jest.Mock;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const createCharacteristic = (uuid: string): MockCharacteristic => {
|
|
12
|
+
const characteristic = new EventEmitter() as MockCharacteristic;
|
|
13
|
+
characteristic.uuid = uuid;
|
|
14
|
+
characteristic.unsubscribe = jest.fn(callback => callback());
|
|
15
|
+
characteristic.subscribe = jest.fn(callback => callback());
|
|
16
|
+
characteristic.write = jest.fn((_buffer, _withoutResponse, callback) => callback());
|
|
17
|
+
characteristic.removeAllListeners = jest.fn(
|
|
18
|
+
characteristic.removeAllListeners.bind(characteristic)
|
|
19
|
+
);
|
|
20
|
+
return characteristic;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createPeripheral = (id: string) => {
|
|
24
|
+
const write = createCharacteristic('0002');
|
|
25
|
+
const notify = createCharacteristic('0003');
|
|
26
|
+
const service = {
|
|
27
|
+
uuid: '0001',
|
|
28
|
+
discoverCharacteristics: jest.fn((_uuids, callback) => callback(null, [write, notify])),
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
peripheral: {
|
|
32
|
+
id,
|
|
33
|
+
state: 'connected',
|
|
34
|
+
advertisement: {
|
|
35
|
+
localName: `OneKey Pro 2 ${id}`,
|
|
36
|
+
serviceUuids: ['0001'],
|
|
37
|
+
},
|
|
38
|
+
discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
|
|
39
|
+
connect: jest.fn(callback => callback()),
|
|
40
|
+
disconnect: jest.fn(callback => callback()),
|
|
41
|
+
},
|
|
42
|
+
service,
|
|
43
|
+
write,
|
|
44
|
+
notify,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('Noble BLE plugin notification routing', () => {
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.useRealTimers();
|
|
51
|
+
jest.resetModules();
|
|
52
|
+
jest.clearAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('does not enumerate Find My advertisements that expose FFFD', async () => {
|
|
56
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
57
|
+
const oneKey = createPeripheral('onekey-device');
|
|
58
|
+
const findMy = createPeripheral('find-my-device');
|
|
59
|
+
findMy.peripheral.advertisement.localName = 'Find My';
|
|
60
|
+
findMy.peripheral.advertisement.serviceUuids = ['fffd'];
|
|
61
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
62
|
+
state: string;
|
|
63
|
+
startScanning: jest.Mock;
|
|
64
|
+
stopScanning: jest.Mock;
|
|
65
|
+
};
|
|
66
|
+
noble.state = 'poweredOn';
|
|
67
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
68
|
+
callback?.();
|
|
69
|
+
noble.emit('discover', findMy.peripheral);
|
|
70
|
+
noble.emit('discover', oneKey.peripheral);
|
|
71
|
+
});
|
|
72
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
73
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
74
|
+
|
|
75
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
76
|
+
const plugin = createNobleBlePlugin();
|
|
77
|
+
await plugin.init();
|
|
78
|
+
const devicesPromise = plugin.enumerate();
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
jest.runAllTimers();
|
|
81
|
+
await Promise.resolve();
|
|
82
|
+
|
|
83
|
+
await expect(devicesPromise).resolves.toEqual([
|
|
84
|
+
expect.objectContaining({ id: 'onekey-device' }),
|
|
85
|
+
]);
|
|
86
|
+
jest.useRealTimers();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('does not enumerate a name-only candidate without the communication service', async () => {
|
|
90
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
91
|
+
const nameOnly = createPeripheral('name-only-device');
|
|
92
|
+
nameOnly.peripheral.advertisement.serviceUuids = [];
|
|
93
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
94
|
+
state: string;
|
|
95
|
+
startScanning: jest.Mock;
|
|
96
|
+
stopScanning: jest.Mock;
|
|
97
|
+
};
|
|
98
|
+
noble.state = 'poweredOn';
|
|
99
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
100
|
+
callback?.();
|
|
101
|
+
noble.emit('discover', nameOnly.peripheral);
|
|
102
|
+
});
|
|
103
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
104
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
105
|
+
|
|
106
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
107
|
+
const plugin = createNobleBlePlugin();
|
|
108
|
+
await plugin.init();
|
|
109
|
+
const devicesPromise = plugin.enumerate();
|
|
110
|
+
await Promise.resolve();
|
|
111
|
+
jest.runAllTimers();
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
|
|
114
|
+
await expect(devicesPromise).resolves.toEqual([]);
|
|
115
|
+
jest.useRealTimers();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('routes notifications to the receiver waiting for the same device', async () => {
|
|
119
|
+
const deviceA = createPeripheral('device-a');
|
|
120
|
+
const deviceB = createPeripheral('device-b');
|
|
121
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
122
|
+
state: string;
|
|
123
|
+
startScanning: jest.Mock;
|
|
124
|
+
stopScanning: jest.Mock;
|
|
125
|
+
};
|
|
126
|
+
noble.state = 'poweredOn';
|
|
127
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
128
|
+
callback?.();
|
|
129
|
+
noble.emit('discover', deviceA.peripheral);
|
|
130
|
+
noble.emit('discover', deviceB.peripheral);
|
|
131
|
+
});
|
|
132
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
133
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
134
|
+
|
|
135
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
136
|
+
const plugin = createNobleBlePlugin();
|
|
137
|
+
await plugin.init();
|
|
138
|
+
await plugin.connect('device-a');
|
|
139
|
+
await plugin.connect('device-b');
|
|
140
|
+
|
|
141
|
+
const receiveA = plugin.receive('device-a');
|
|
142
|
+
const receiveB = plugin.receive('device-b');
|
|
143
|
+
deviceB.notify.emit('data', Buffer.from('bb', 'hex'));
|
|
144
|
+
deviceA.notify.emit('data', Buffer.from('aa', 'hex'));
|
|
145
|
+
|
|
146
|
+
await expect(Promise.all([receiveA, receiveB])).resolves.toEqual(['aa', 'bb']);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('finishes disconnect cleanup when Noble never calls unsubscribe back', async () => {
|
|
150
|
+
const device = createPeripheral('device-a');
|
|
151
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
152
|
+
state: string;
|
|
153
|
+
startScanning: jest.Mock;
|
|
154
|
+
stopScanning: jest.Mock;
|
|
155
|
+
};
|
|
156
|
+
noble.state = 'poweredOn';
|
|
157
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
158
|
+
callback?.();
|
|
159
|
+
noble.emit('discover', device.peripheral);
|
|
160
|
+
});
|
|
161
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
162
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
163
|
+
|
|
164
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
165
|
+
const plugin = createNobleBlePlugin();
|
|
166
|
+
await plugin.init();
|
|
167
|
+
await plugin.connect('device-a');
|
|
168
|
+
device.notify.unsubscribe.mockImplementation(() => undefined);
|
|
169
|
+
|
|
170
|
+
const result = await Promise.race([
|
|
171
|
+
plugin.disconnect('device-a').then(() => 'completed'),
|
|
172
|
+
new Promise(resolve => {
|
|
173
|
+
setTimeout(() => resolve('blocked'), 300);
|
|
174
|
+
}),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
expect(result).toBe('completed');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('disconnects an untracked peripheral when service discovery fails', async () => {
|
|
181
|
+
const device = createPeripheral('device-a');
|
|
182
|
+
device.peripheral.discoverServices.mockImplementation((_uuids, callback) =>
|
|
183
|
+
callback(new Error('service discovery failed'))
|
|
184
|
+
);
|
|
185
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
186
|
+
state: string;
|
|
187
|
+
startScanning: jest.Mock;
|
|
188
|
+
stopScanning: jest.Mock;
|
|
189
|
+
};
|
|
190
|
+
noble.state = 'poweredOn';
|
|
191
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
192
|
+
callback?.();
|
|
193
|
+
noble.emit('discover', device.peripheral);
|
|
194
|
+
});
|
|
195
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
196
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
197
|
+
|
|
198
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
199
|
+
const plugin = createNobleBlePlugin();
|
|
200
|
+
await plugin.init();
|
|
201
|
+
|
|
202
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('service discovery failed');
|
|
203
|
+
expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('rejects a vendor-specific service containing the OneKey short UUID', async () => {
|
|
207
|
+
const device = createPeripheral('device-a');
|
|
208
|
+
device.service.uuid = 'abcd0001-1234-5678-9012-abcdefabcdef';
|
|
209
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
210
|
+
state: string;
|
|
211
|
+
startScanning: jest.Mock;
|
|
212
|
+
stopScanning: jest.Mock;
|
|
213
|
+
};
|
|
214
|
+
noble.state = 'poweredOn';
|
|
215
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
216
|
+
callback?.();
|
|
217
|
+
noble.emit('discover', device.peripheral);
|
|
218
|
+
});
|
|
219
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
220
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
221
|
+
|
|
222
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
223
|
+
const plugin = createNobleBlePlugin();
|
|
224
|
+
await plugin.init();
|
|
225
|
+
|
|
226
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('No BLE service found');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('unsubscribes and disconnects an untracked peripheral when notification setup fails', async () => {
|
|
230
|
+
const device = createPeripheral('device-a');
|
|
231
|
+
device.notify.subscribe.mockImplementation(callback =>
|
|
232
|
+
callback(new Error('notification setup failed'))
|
|
233
|
+
);
|
|
234
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
235
|
+
state: string;
|
|
236
|
+
startScanning: jest.Mock;
|
|
237
|
+
stopScanning: jest.Mock;
|
|
238
|
+
};
|
|
239
|
+
noble.state = 'poweredOn';
|
|
240
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
241
|
+
callback?.();
|
|
242
|
+
noble.emit('discover', device.peripheral);
|
|
243
|
+
});
|
|
244
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
245
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
246
|
+
|
|
247
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
248
|
+
const plugin = createNobleBlePlugin();
|
|
249
|
+
await plugin.init();
|
|
250
|
+
|
|
251
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('notification setup failed');
|
|
252
|
+
expect(device.notify.unsubscribe).toHaveBeenCalled();
|
|
253
|
+
expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('uses withoutResponse for normal and high-volume writes', async () => {
|
|
257
|
+
const device = createPeripheral('device-a');
|
|
258
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
259
|
+
state: string;
|
|
260
|
+
startScanning: jest.Mock;
|
|
261
|
+
stopScanning: jest.Mock;
|
|
262
|
+
};
|
|
263
|
+
noble.state = 'poweredOn';
|
|
264
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
265
|
+
callback?.();
|
|
266
|
+
noble.emit('discover', device.peripheral);
|
|
267
|
+
});
|
|
268
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
269
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
270
|
+
|
|
271
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
272
|
+
const plugin = createNobleBlePlugin();
|
|
273
|
+
await plugin.init();
|
|
274
|
+
await plugin.connect('device-a');
|
|
275
|
+
|
|
276
|
+
await plugin.send('device-a', 'aa');
|
|
277
|
+
await plugin.send('device-a', 'bb');
|
|
278
|
+
|
|
279
|
+
expect(device.write.write.mock.calls.map(([, withoutResponse]) => withoutResponse)).toEqual([
|
|
280
|
+
true,
|
|
281
|
+
true,
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('uses acknowledged writes when requested by firmware upload', async () => {
|
|
286
|
+
const device = createPeripheral('device-a');
|
|
287
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
288
|
+
state: string;
|
|
289
|
+
startScanning: jest.Mock;
|
|
290
|
+
stopScanning: jest.Mock;
|
|
291
|
+
};
|
|
292
|
+
noble.state = 'poweredOn';
|
|
293
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
294
|
+
callback?.();
|
|
295
|
+
noble.emit('discover', device.peripheral);
|
|
296
|
+
});
|
|
297
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
298
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
299
|
+
|
|
300
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
301
|
+
const plugin = createNobleBlePlugin();
|
|
302
|
+
await plugin.init();
|
|
303
|
+
await plugin.connect('device-a');
|
|
304
|
+
|
|
305
|
+
await (plugin.send as any)('device-a', 'aa', { withoutResponse: false });
|
|
306
|
+
|
|
307
|
+
expect(device.write.write).toHaveBeenCalledWith(
|
|
308
|
+
expect.any(Buffer),
|
|
309
|
+
false,
|
|
310
|
+
expect.any(Function)
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('does not add a fixed delay between 192-byte writes', async () => {
|
|
315
|
+
const device = createPeripheral('device-a');
|
|
316
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
317
|
+
state: string;
|
|
318
|
+
startScanning: jest.Mock;
|
|
319
|
+
stopScanning: jest.Mock;
|
|
320
|
+
};
|
|
321
|
+
const wait = jest.fn(() => Promise.resolve());
|
|
322
|
+
noble.state = 'poweredOn';
|
|
323
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
324
|
+
callback?.();
|
|
325
|
+
noble.emit('discover', device.peripheral);
|
|
326
|
+
});
|
|
327
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
328
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
329
|
+
jest.doMock('@onekeyfe/hd-shared', () => ({
|
|
330
|
+
...jest.requireActual('@onekeyfe/hd-shared'),
|
|
331
|
+
wait,
|
|
332
|
+
}));
|
|
333
|
+
|
|
334
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
335
|
+
const plugin = createNobleBlePlugin();
|
|
336
|
+
await plugin.init();
|
|
337
|
+
await plugin.connect('device-a');
|
|
338
|
+
|
|
339
|
+
await plugin.send('device-a', 'aa'.repeat(193));
|
|
340
|
+
|
|
341
|
+
expect(device.write.write).toHaveBeenCalledTimes(2);
|
|
342
|
+
expect(wait).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('preserves a short final BLE packet without padding', async () => {
|
|
346
|
+
const device = createPeripheral('device-a');
|
|
347
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
348
|
+
state: string;
|
|
349
|
+
startScanning: jest.Mock;
|
|
350
|
+
stopScanning: jest.Mock;
|
|
351
|
+
};
|
|
352
|
+
noble.state = 'poweredOn';
|
|
353
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
354
|
+
callback?.();
|
|
355
|
+
noble.emit('discover', device.peripheral);
|
|
356
|
+
});
|
|
357
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
358
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
359
|
+
|
|
360
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
361
|
+
const plugin = createNobleBlePlugin();
|
|
362
|
+
await plugin.init();
|
|
363
|
+
await plugin.connect('device-a');
|
|
364
|
+
|
|
365
|
+
await plugin.send('device-a', 'aabb');
|
|
366
|
+
|
|
367
|
+
const packet = device.write.write.mock.calls[0][0] as Buffer;
|
|
368
|
+
expect(packet).toEqual(Buffer.from('aabb', 'hex'));
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { prepareSession } from '../cli';
|
|
2
|
+
import { preloadSessionFromKeychain } from '../session';
|
|
3
|
+
|
|
4
|
+
jest.mock('../session', () => ({
|
|
5
|
+
clearSessionFromKeychain: jest.fn(),
|
|
6
|
+
preloadSessionFromKeychain: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('CLI wallet session', () => {
|
|
10
|
+
test('uses the public wallet identity without persisting an internal session id', async () => {
|
|
11
|
+
const sdk = {
|
|
12
|
+
searchDevices: jest.fn().mockResolvedValue({
|
|
13
|
+
success: true,
|
|
14
|
+
payload: [
|
|
15
|
+
{
|
|
16
|
+
connectId: 'pro2-connect-id',
|
|
17
|
+
deviceId: 'device-id',
|
|
18
|
+
deviceType: 'pro2',
|
|
19
|
+
features: {
|
|
20
|
+
deviceId: 'device-id',
|
|
21
|
+
deviceType: 'pro2',
|
|
22
|
+
unlocked: true,
|
|
23
|
+
passphraseProtection: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
getFeatures: jest.fn(),
|
|
29
|
+
openWalletSession: jest.fn().mockResolvedValue({
|
|
30
|
+
success: true,
|
|
31
|
+
payload: {
|
|
32
|
+
protocol: 'V2',
|
|
33
|
+
walletType: 'hidden',
|
|
34
|
+
deviceId: 'device-id',
|
|
35
|
+
passphraseState: 'wallet-state',
|
|
36
|
+
resumed: false,
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
const globalOpts: Record<string, unknown> = {};
|
|
41
|
+
(preloadSessionFromKeychain as jest.Mock).mockResolvedValueOnce(undefined);
|
|
42
|
+
|
|
43
|
+
await expect(prepareSession(sdk as never, globalOpts)).resolves.toBe('wallet-state');
|
|
44
|
+
|
|
45
|
+
expect(sdk.openWalletSession).toHaveBeenCalledWith('pro2-connect-id', {
|
|
46
|
+
mode: 'select-hidden',
|
|
47
|
+
});
|
|
48
|
+
expect(sdk.getFeatures).not.toHaveBeenCalled();
|
|
49
|
+
expect(globalOpts).toMatchObject({
|
|
50
|
+
connectId: 'pro2-connect-id',
|
|
51
|
+
deviceId: 'device-id',
|
|
52
|
+
passphraseState: 'wallet-state',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|