@onekeyfe/hd-transport-react-native 1.2.3-alpha.1 → 1.2.3-alpha.10

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.
@@ -0,0 +1,307 @@
1
+ import EventEmitter from 'events';
2
+
3
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
4
+
5
+ import ReactNativeBleTransport, {
6
+ ANDROID_KEY_MISSING_GRACE_MS,
7
+ ANDROID_KEY_MISSING_LINK_WINDOW_MS,
8
+ } from '../index';
9
+ import { SYSTEM_BONDING_RESTART_WINDOW_MS, onDeviceBondState } from '../BleManager';
10
+ import { stopBleKeyMissingTracking } from '../bleKeyMissing';
11
+ import protocolV1Schema from './protocolV1SchemaFixture';
12
+
13
+ let mockKeyMissingSupported = true;
14
+ let mockEmitKeyMissing: ((event: { id?: string }) => void) | undefined;
15
+ let mockEmitBondState: ((peripheral: unknown) => void) | undefined;
16
+
17
+ jest.mock(
18
+ 'react-native',
19
+ () => ({
20
+ Platform: { OS: 'android', select: (spec: Record<string, unknown>) => spec.android },
21
+ PermissionsAndroid: {
22
+ PERMISSIONS: {},
23
+ RESULTS: {},
24
+ request: jest.fn(),
25
+ requestMultiple: jest.fn(),
26
+ },
27
+ }),
28
+ { virtual: true }
29
+ );
30
+
31
+ jest.mock('react-native-ble-plx', () => ({
32
+ BleATTErrorCode: { InvalidHandle: 1 },
33
+ BleError: Error,
34
+ BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
35
+ BleManager: jest.fn(),
36
+ ScanMode: { LowLatency: 2 },
37
+ }));
38
+
39
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
40
+ __esModule: true,
41
+ default: {
42
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
43
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
44
+ pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
45
+ supportsDeviceKeyMissing: jest.fn(() => mockKeyMissingSupported),
46
+ onDeviceKeyMissing: jest.fn((callback: (event: { id?: string }) => void) => {
47
+ mockEmitKeyMissing = callback;
48
+ return () => {
49
+ mockEmitKeyMissing = undefined;
50
+ };
51
+ }),
52
+ onDeviceBondState: jest.fn((callback: (peripheral: unknown) => void) => {
53
+ mockEmitBondState = callback;
54
+ return () => {
55
+ mockEmitBondState = undefined;
56
+ };
57
+ }),
58
+ },
59
+ }));
60
+
61
+ const UUID = 'CC:06:1C:FD:48:E9';
62
+
63
+ const flush = () =>
64
+ new Promise(resolve => {
65
+ setImmediate(resolve);
66
+ });
67
+
68
+ const advance = async (ms: number) => {
69
+ jest.advanceTimersByTime(ms);
70
+ await flush();
71
+ };
72
+
73
+ // What ble-plx reports when the device drops the link while the first write is queued.
74
+ const peerTerminated = () =>
75
+ Promise.reject(
76
+ Object.assign(new Error(`Device ${UUID} was disconnected`), {
77
+ errorCode: 201,
78
+ attErrorCode: 19,
79
+ reason: 'Disconnected with status 19 (GATT_CONN_TERMINATE_PEER_USER)',
80
+ })
81
+ );
82
+
83
+ function createConnectedTransport(linkAgeMs = 0) {
84
+ const t = new ReactNativeBleTransport({});
85
+ t.configure(protocolV1Schema);
86
+ (t as any).deviceProtocol.set(UUID, 'V1');
87
+ (t as any).getCachedTransport = () => ({
88
+ writeCharacteristic: { writeWithoutResponse: jest.fn(peerTerminated) },
89
+ });
90
+ (t as any).androidLinkStartedAt.set(UUID, Date.now() - linkAgeMs);
91
+ return t;
92
+ }
93
+
94
+ const settle = (promise: Promise<unknown>) => {
95
+ const outcome: { error?: { errorCode?: unknown }; done: boolean } = { done: false };
96
+ promise.then(
97
+ () => {
98
+ outcome.done = true;
99
+ },
100
+ error => {
101
+ outcome.error = error;
102
+ outcome.done = true;
103
+ }
104
+ );
105
+ return outcome;
106
+ };
107
+
108
+ describe('Android key missing', () => {
109
+ beforeEach(() => {
110
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
111
+ mockKeyMissingSupported = true;
112
+ });
113
+
114
+ afterEach(() => {
115
+ stopBleKeyMissingTracking();
116
+ jest.clearAllTimers();
117
+ jest.useRealTimers();
118
+ });
119
+
120
+ describe('link dropped right after connect', () => {
121
+ test('reports an invalid bond when the system says the keys are missing', async () => {
122
+ const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
123
+ await flush();
124
+ // The broadcast is delivered separately and can trail the GATT disconnect.
125
+ mockEmitKeyMissing?.({ id: UUID.toLowerCase() });
126
+ await flush();
127
+
128
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
129
+ });
130
+
131
+ test('keeps the original error when no key missing follows', async () => {
132
+ const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
133
+ await flush();
134
+ expect(outcome.done).toBe(false);
135
+
136
+ await advance(ANDROID_KEY_MISSING_GRACE_MS);
137
+
138
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
139
+ });
140
+
141
+ test('ignores key missing reported for an earlier link', async () => {
142
+ const transport = createConnectedTransport();
143
+ const earlier = settle(transport.call(UUID, 'GetFeatures', {}));
144
+ await flush();
145
+ mockEmitKeyMissing?.({ id: UUID });
146
+ await flush();
147
+ expect(earlier.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
148
+
149
+ // The user re-paired; a later link that drops is an ordinary disconnect.
150
+ await advance(1000);
151
+ (transport as any).androidLinkStartedAt.set(UUID, Date.now());
152
+ const later = settle(transport.call(UUID, 'GetFeatures', {}));
153
+ await flush();
154
+ await advance(ANDROID_KEY_MISSING_GRACE_MS);
155
+
156
+ expect(later.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
157
+ });
158
+
159
+ test('ignores key missing for another device', async () => {
160
+ const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
161
+ await flush();
162
+ mockEmitKeyMissing?.({ id: 'E9:B9:F4:DD:DE:CD' });
163
+ await advance(ANDROID_KEY_MISSING_GRACE_MS);
164
+
165
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
166
+ });
167
+
168
+ test('does not wait on a link that has been up for a while', async () => {
169
+ const transport = createConnectedTransport(ANDROID_KEY_MISSING_LINK_WINDOW_MS + 1);
170
+ const outcome = settle(transport.call(UUID, 'GetFeatures', {}));
171
+ await flush();
172
+
173
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
174
+ });
175
+
176
+ test('does not wait when the OS or native build never reports key missing', async () => {
177
+ mockKeyMissingSupported = false;
178
+ const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
179
+ await flush();
180
+
181
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
182
+ });
183
+ });
184
+
185
+ describe('acquire over a bond the device no longer holds', () => {
186
+ // Android still reports BONDED, so connect, MTU and GATT setup all succeed and the
187
+ // protocol probe is the first operation to meet the dropped link.
188
+ function createStaleBondHarness() {
189
+ const writeCharacteristic = {
190
+ uuid: '00000002-0000-1000-8000-00805f9b34fb',
191
+ isWritableWithoutResponse: true,
192
+ writeWithoutResponse: jest.fn(peerTerminated),
193
+ };
194
+ const notifyCharacteristic = {
195
+ uuid: '00000003-0000-1000-8000-00805f9b34fb',
196
+ isNotifiable: true,
197
+ monitor: jest.fn(() => ({ remove: jest.fn() })),
198
+ };
199
+ const device = {
200
+ id: UUID,
201
+ name: 'Pro 2 48E9',
202
+ localName: 'Pro 2 48E9',
203
+ mtu: 247,
204
+ serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
205
+ isConnected: jest.fn(() => Promise.resolve(true)),
206
+ cancelConnection: jest.fn(() => Promise.resolve()),
207
+ discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
208
+ characteristicsForService: jest.fn(() =>
209
+ Promise.resolve([writeCharacteristic, notifyCharacteristic])
210
+ ),
211
+ services: jest.fn(() => Promise.resolve([])),
212
+ onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
213
+ };
214
+ const transport = new ReactNativeBleTransport({});
215
+ (transport as any).blePlxManager = {
216
+ devices: jest.fn(() => Promise.resolve([device])),
217
+ connectedDevices: jest.fn(() => Promise.resolve([])),
218
+ cancelTransaction: jest.fn(() => Promise.resolve()),
219
+ cancelDeviceConnection: jest.fn(() => Promise.resolve()),
220
+ onStateChange: jest.fn((listener: (state: string) => void) => {
221
+ setImmediate(() => listener('PoweredOn'));
222
+ return { remove: jest.fn() };
223
+ }),
224
+ state: jest.fn(() => Promise.resolve('PoweredOn')),
225
+ };
226
+ transport.init(
227
+ { debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
228
+ new EventEmitter()
229
+ );
230
+ transport.configure(protocolV1Schema);
231
+ return { transport, write: writeCharacteristic.writeWithoutResponse };
232
+ }
233
+
234
+ async function acquireUntilSettled(keyMissing: boolean) {
235
+ const { transport, write } = createStaleBondHarness();
236
+ const outcome = settle(transport.acquire({ uuid: UUID }));
237
+ for (let i = 0; i < 100 && !outcome.done; i += 1) {
238
+ // eslint-disable-next-line no-await-in-loop
239
+ await advance(100);
240
+ // The broadcast follows the dropped link, which the probe write is first to meet.
241
+ if (keyMissing && write.mock.calls.length > 0) mockEmitKeyMissing?.({ id: UUID });
242
+ }
243
+ expect(write).toHaveBeenCalled();
244
+ return JSON.parse(JSON.stringify(ERRORS.serializeError({ error: outcome.error })));
245
+ }
246
+
247
+ test('ends as an invalid bond that survives the Core response path', async () => {
248
+ await expect(acquireUntilSettled(true)).resolves.toEqual({
249
+ code: HardwareErrorCode.BleBondInvalid,
250
+ error: expect.stringContaining('Forget the device'),
251
+ params: { phase: 'connect', reason: 'key_missing' },
252
+ });
253
+ });
254
+
255
+ test('keeps the legacy error when the system reports nothing', async () => {
256
+ await expect(acquireUntilSettled(false)).resolves.toMatchObject({
257
+ code: HardwareErrorCode.BleDeviceNotBonded,
258
+ });
259
+ });
260
+ });
261
+
262
+ describe('waiting for bonding', () => {
263
+ const bondState = (preState: string, state: string) =>
264
+ mockEmitBondState?.({ id: UUID, bondState: { preState, state } });
265
+
266
+ test('fails as an invalid bond when the system re-pair ends in key missing', async () => {
267
+ const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
268
+ mockEmitKeyMissing?.({ id: UUID });
269
+ await flush();
270
+
271
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
272
+ });
273
+
274
+ test('follows a system re-pair that replaces the bond', async () => {
275
+ const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
276
+ bondState('BOND_BONDING', 'BOND_NONE');
277
+ bondState('BOND_NONE', 'BOND_BONDING');
278
+ await advance(SYSTEM_BONDING_RESTART_WINDOW_MS);
279
+ expect(outcome.done).toBe(false);
280
+
281
+ bondState('BOND_BONDING', 'BOND_BONDED');
282
+ await flush();
283
+
284
+ expect(outcome.done).toBe(true);
285
+ expect(outcome.error).toBeUndefined();
286
+ });
287
+
288
+ test('fails a system re-pair that ends without a bond', async () => {
289
+ const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
290
+ bondState('BOND_BONDING', 'BOND_NONE');
291
+ await flush();
292
+ expect(outcome.done).toBe(false);
293
+
294
+ await advance(SYSTEM_BONDING_RESTART_WINDOW_MS);
295
+
296
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
297
+ });
298
+
299
+ test('fails a bonding this transport started as soon as it ends without a bond', async () => {
300
+ const outcome = settle(onDeviceBondState(UUID));
301
+ bondState('BOND_BONDING', 'BOND_NONE');
302
+ await flush();
303
+
304
+ expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
305
+ });
306
+ });
307
+ });
@@ -0,0 +1,33 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import { isNativeBleDisconnectError, toBleDisconnectHardwareError } from '../bleNativeDisconnect';
4
+
5
+ describe('native BLE disconnect mapping', () => {
6
+ test.each([
7
+ [{ errorCode: 201, reason: 'The specified device has disconnected from us.' }],
8
+ [{ errorCode: 201, iosErrorCode: 7, reason: 'The specified device has disconnected from us.' }],
9
+ [{ iosErrorCode: 7, reason: 'Peripheral disconnected' }],
10
+ ])('recognizes %j as a native disconnect', nativeError => {
11
+ expect(isNativeBleDisconnectError(nativeError)).toBe(true);
12
+ expect(toBleDisconnectHardwareError(nativeError)).toMatchObject({
13
+ errorCode: HardwareErrorCode.BleDeviceDisconnected,
14
+ });
15
+ });
16
+
17
+ test('does not treat a stale-bond iOS code as a disconnect', () => {
18
+ expect(
19
+ isNativeBleDisconnectError({
20
+ iosErrorCode: 14,
21
+ reason: 'Peer removed pairing information',
22
+ })
23
+ ).toBe(false);
24
+ });
25
+
26
+ test('does not treat an already-canonical unpaired error as a native disconnect', () => {
27
+ expect(
28
+ isNativeBleDisconnectError({
29
+ errorCode: HardwareErrorCode.BleDeviceNotBonded,
30
+ })
31
+ ).toBe(false);
32
+ });
33
+ });