@onekeyfe/hd-transport-react-native 1.2.3-alpha.9 → 1.2.3
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/bleEncryption.d.ts +17 -0
- package/dist/bleEncryption.d.ts.map +1 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +229 -17
- package/package.json +6 -6
- package/src/__tests__/androidEncryption.test.ts +482 -0
- package/src/__tests__/enumerate.test.ts +8 -1
- package/src/__tests__/protocolV2Link.test.ts +8 -1
- package/src/bleEncryption.ts +267 -0
- package/src/index.ts +71 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
|
+
|
|
4
|
+
import ReactNativeBleTransport, {
|
|
5
|
+
ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS,
|
|
6
|
+
ANDROID_KEY_MISSING_GRACE_MS,
|
|
7
|
+
ANDROID_SYSTEM_REPAIR_TIMEOUT_MS,
|
|
8
|
+
} from '../index';
|
|
9
|
+
import {
|
|
10
|
+
HCI_PIN_OR_KEY_MISSING,
|
|
11
|
+
stopBleEncryptionTracking,
|
|
12
|
+
waitForAndroidLinkEncryption,
|
|
13
|
+
} from '../bleEncryption';
|
|
14
|
+
import { stopBleKeyMissingTracking } from '../bleKeyMissing';
|
|
15
|
+
import protocolV1Schema from './protocolV1SchemaFixture';
|
|
16
|
+
|
|
17
|
+
type EncryptionEvent = { id: string; status: number; enabled: boolean };
|
|
18
|
+
|
|
19
|
+
let mockSupported = true;
|
|
20
|
+
let mockPairResult: { bonded: boolean; bonding: boolean; initiated?: boolean } = {
|
|
21
|
+
bonded: true,
|
|
22
|
+
bonding: false,
|
|
23
|
+
initiated: false,
|
|
24
|
+
};
|
|
25
|
+
let mockEmitEncryption: ((event: EncryptionEvent) => void) | undefined;
|
|
26
|
+
let mockEmitAclDisconnected: ((event: { id: string }) => void) | undefined;
|
|
27
|
+
let mockEmitKeyMissing: ((event: { id: string }) => void) | undefined;
|
|
28
|
+
let mockEmitBondState: ((peripheral: unknown) => void) | undefined;
|
|
29
|
+
|
|
30
|
+
jest.mock(
|
|
31
|
+
'react-native',
|
|
32
|
+
() => ({
|
|
33
|
+
Platform: { OS: 'android', select: (spec: Record<string, unknown>) => spec.android },
|
|
34
|
+
PermissionsAndroid: {
|
|
35
|
+
PERMISSIONS: {},
|
|
36
|
+
RESULTS: {},
|
|
37
|
+
request: jest.fn(),
|
|
38
|
+
requestMultiple: jest.fn(),
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
{ virtual: true }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
45
|
+
BleATTErrorCode: { InvalidHandle: 1 },
|
|
46
|
+
BleError: Error,
|
|
47
|
+
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
48
|
+
BleManager: jest.fn(),
|
|
49
|
+
ScanMode: { LowLatency: 2 },
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
53
|
+
__esModule: true,
|
|
54
|
+
default: {
|
|
55
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
56
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
57
|
+
pairDevice: jest.fn(() => Promise.resolve(mockPairResult)),
|
|
58
|
+
supportsDeviceKeyMissing: jest.fn(() => mockSupported),
|
|
59
|
+
onDeviceKeyMissing: jest.fn((callback: (event: { id: string }) => void) => {
|
|
60
|
+
mockEmitKeyMissing = callback;
|
|
61
|
+
return () => {
|
|
62
|
+
mockEmitKeyMissing = undefined;
|
|
63
|
+
};
|
|
64
|
+
}),
|
|
65
|
+
supportsDeviceEncryptionChange: jest.fn(() => mockSupported),
|
|
66
|
+
onDeviceEncryptionChange: jest.fn((callback: (event: EncryptionEvent) => void) => {
|
|
67
|
+
mockEmitEncryption = callback;
|
|
68
|
+
return () => {
|
|
69
|
+
mockEmitEncryption = undefined;
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
72
|
+
onDeviceAclDisconnected: jest.fn((callback: (event: { id: string }) => void) => {
|
|
73
|
+
mockEmitAclDisconnected = callback;
|
|
74
|
+
return () => {
|
|
75
|
+
mockEmitAclDisconnected = undefined;
|
|
76
|
+
};
|
|
77
|
+
}),
|
|
78
|
+
onDeviceBondState: jest.fn((callback: (peripheral: unknown) => void) => {
|
|
79
|
+
mockEmitBondState = callback;
|
|
80
|
+
return () => {
|
|
81
|
+
mockEmitBondState = undefined;
|
|
82
|
+
};
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const UUID = 'CC:06:1C:FD:48:E9';
|
|
88
|
+
const OTHER_UUID = 'E9:B9:F4:DD:DE:CD';
|
|
89
|
+
|
|
90
|
+
const flush = () =>
|
|
91
|
+
new Promise(resolve => {
|
|
92
|
+
setImmediate(resolve);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const advance = async (ms: number) => {
|
|
96
|
+
jest.advanceTimersByTime(ms);
|
|
97
|
+
await flush();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const encrypted = (id = UUID) => mockEmitEncryption?.({ id, status: 0, enabled: true });
|
|
101
|
+
const keyMissing = (id = UUID) =>
|
|
102
|
+
mockEmitEncryption?.({ id, status: HCI_PIN_OR_KEY_MISSING, enabled: false });
|
|
103
|
+
|
|
104
|
+
const settle = <T>(promise: Promise<T>) => {
|
|
105
|
+
const outcome: { value?: T; error?: { errorCode?: unknown }; done: boolean } = { done: false };
|
|
106
|
+
promise.then(
|
|
107
|
+
value => {
|
|
108
|
+
outcome.value = value;
|
|
109
|
+
outcome.done = true;
|
|
110
|
+
},
|
|
111
|
+
error => {
|
|
112
|
+
outcome.error = error;
|
|
113
|
+
outcome.done = true;
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
return outcome;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const waitFor = ({
|
|
120
|
+
linkStartedAt = Date.now(),
|
|
121
|
+
signal,
|
|
122
|
+
}: { linkStartedAt?: number; signal?: AbortSignal } = {}) =>
|
|
123
|
+
settle(
|
|
124
|
+
waitForAndroidLinkEncryption({
|
|
125
|
+
deviceId: UUID,
|
|
126
|
+
linkStartedAt,
|
|
127
|
+
resultTimeoutMs: ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS,
|
|
128
|
+
repairTimeoutMs: ANDROID_SYSTEM_REPAIR_TIMEOUT_MS,
|
|
129
|
+
keyMissingGraceMs: ANDROID_KEY_MISSING_GRACE_MS,
|
|
130
|
+
signal,
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
describe('Android link encryption', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
137
|
+
mockSupported = true;
|
|
138
|
+
mockPairResult = { bonded: true, bonding: false, initiated: false };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
stopBleEncryptionTracking();
|
|
143
|
+
stopBleKeyMissingTracking();
|
|
144
|
+
jest.clearAllTimers();
|
|
145
|
+
jest.useRealTimers();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('first result of a new link', () => {
|
|
149
|
+
test('resolves when the stored bond encrypts the link', async () => {
|
|
150
|
+
const outcome = waitFor();
|
|
151
|
+
await flush();
|
|
152
|
+
expect(outcome.done).toBe(false);
|
|
153
|
+
|
|
154
|
+
encrypted();
|
|
155
|
+
await flush();
|
|
156
|
+
|
|
157
|
+
expect(outcome.value).toBe('encrypted');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('uses a result that arrived before the wait started', async () => {
|
|
161
|
+
const linkStartedAt = Date.now();
|
|
162
|
+
waitFor({ linkStartedAt });
|
|
163
|
+
await flush();
|
|
164
|
+
encrypted();
|
|
165
|
+
await flush();
|
|
166
|
+
|
|
167
|
+
const outcome = waitFor({ linkStartedAt });
|
|
168
|
+
await flush();
|
|
169
|
+
|
|
170
|
+
expect(outcome.value).toBe('encrypted');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('proceeds as before when no result arrives in time', async () => {
|
|
174
|
+
const outcome = waitFor();
|
|
175
|
+
await advance(ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS - 1);
|
|
176
|
+
expect(outcome.done).toBe(false);
|
|
177
|
+
|
|
178
|
+
await advance(1);
|
|
179
|
+
|
|
180
|
+
expect(outcome.value).toBe('unresolved');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('does not wait when the platform cannot report the result', async () => {
|
|
184
|
+
mockSupported = false;
|
|
185
|
+
const outcome = waitFor();
|
|
186
|
+
await flush();
|
|
187
|
+
|
|
188
|
+
expect(outcome.value).toBe('unresolved');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('proceeds when encryption fails for a reason other than a lost bond', async () => {
|
|
192
|
+
const outcome = waitFor();
|
|
193
|
+
await flush();
|
|
194
|
+
mockEmitEncryption?.({ id: UUID, status: 0x08, enabled: false });
|
|
195
|
+
await flush();
|
|
196
|
+
|
|
197
|
+
expect(outcome.value).toBe('unresolved');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('proceeds when the link drops before any result', async () => {
|
|
201
|
+
const outcome = waitFor();
|
|
202
|
+
await flush();
|
|
203
|
+
mockEmitAclDisconnected?.({ id: UUID });
|
|
204
|
+
await flush();
|
|
205
|
+
|
|
206
|
+
expect(outcome.value).toBe('unresolved');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('ignores results for another device', async () => {
|
|
210
|
+
const outcome = waitFor();
|
|
211
|
+
await flush();
|
|
212
|
+
encrypted(OTHER_UUID);
|
|
213
|
+
await flush();
|
|
214
|
+
expect(outcome.done).toBe(false);
|
|
215
|
+
|
|
216
|
+
encrypted(UUID.toLowerCase());
|
|
217
|
+
await flush();
|
|
218
|
+
|
|
219
|
+
expect(outcome.value).toBe('encrypted');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('reused link', () => {
|
|
224
|
+
test('does not wait on a link that is still up and was encrypted earlier', async () => {
|
|
225
|
+
waitFor();
|
|
226
|
+
await flush();
|
|
227
|
+
encrypted();
|
|
228
|
+
await advance(1000);
|
|
229
|
+
|
|
230
|
+
const outcome = waitFor();
|
|
231
|
+
await flush();
|
|
232
|
+
|
|
233
|
+
expect(outcome.value).toBe('already-encrypted');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('waits again once the earlier link has dropped', async () => {
|
|
237
|
+
waitFor();
|
|
238
|
+
await flush();
|
|
239
|
+
encrypted();
|
|
240
|
+
await advance(1000);
|
|
241
|
+
mockEmitAclDisconnected?.({ id: UUID });
|
|
242
|
+
await advance(1000);
|
|
243
|
+
|
|
244
|
+
const outcome = waitFor();
|
|
245
|
+
await flush();
|
|
246
|
+
expect(outcome.done).toBe(false);
|
|
247
|
+
|
|
248
|
+
keyMissing();
|
|
249
|
+
await flush();
|
|
250
|
+
|
|
251
|
+
expect(outcome.done).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('lost bond', () => {
|
|
256
|
+
test('holds until the system re-pairs on the same link', async () => {
|
|
257
|
+
const outcome = waitFor();
|
|
258
|
+
await flush();
|
|
259
|
+
keyMissing();
|
|
260
|
+
await advance(10_000);
|
|
261
|
+
expect(outcome.done).toBe(false);
|
|
262
|
+
|
|
263
|
+
encrypted();
|
|
264
|
+
await flush();
|
|
265
|
+
|
|
266
|
+
expect(outcome.value).toBe('re-paired');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('reports an invalid bond when the system says re-pairing failed', async () => {
|
|
270
|
+
const outcome = waitFor();
|
|
271
|
+
await flush();
|
|
272
|
+
keyMissing();
|
|
273
|
+
await advance(5000);
|
|
274
|
+
mockEmitKeyMissing?.({ id: UUID });
|
|
275
|
+
await flush();
|
|
276
|
+
|
|
277
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('reports an invalid bond when the drop is followed by key missing', async () => {
|
|
281
|
+
const outcome = waitFor();
|
|
282
|
+
await flush();
|
|
283
|
+
keyMissing();
|
|
284
|
+
await advance(5000);
|
|
285
|
+
mockEmitAclDisconnected?.({ id: UUID });
|
|
286
|
+
await advance(ANDROID_KEY_MISSING_GRACE_MS - 100);
|
|
287
|
+
mockEmitKeyMissing?.({ id: UUID });
|
|
288
|
+
await flush();
|
|
289
|
+
|
|
290
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('reports a disconnect when the link drops without key missing', async () => {
|
|
294
|
+
const outcome = waitFor();
|
|
295
|
+
await flush();
|
|
296
|
+
keyMissing();
|
|
297
|
+
await advance(5000);
|
|
298
|
+
mockEmitAclDisconnected?.({ id: UUID });
|
|
299
|
+
await advance(ANDROID_KEY_MISSING_GRACE_MS);
|
|
300
|
+
|
|
301
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceDisconnected);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('gives up when re-pairing never finishes', async () => {
|
|
305
|
+
const outcome = waitFor();
|
|
306
|
+
await flush();
|
|
307
|
+
keyMissing();
|
|
308
|
+
await advance(ANDROID_SYSTEM_REPAIR_TIMEOUT_MS);
|
|
309
|
+
|
|
310
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('stops waiting when the transport is stopped', async () => {
|
|
314
|
+
const controller = new AbortController();
|
|
315
|
+
const outcome = waitFor({ signal: controller.signal });
|
|
316
|
+
await flush();
|
|
317
|
+
keyMissing();
|
|
318
|
+
await flush();
|
|
319
|
+
|
|
320
|
+
controller.abort();
|
|
321
|
+
await flush();
|
|
322
|
+
|
|
323
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceDisconnected);
|
|
324
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('acquire', () => {
|
|
329
|
+
function createHarness() {
|
|
330
|
+
const monitor = jest.fn(() => ({ remove: jest.fn() }));
|
|
331
|
+
const writeCharacteristic = {
|
|
332
|
+
uuid: '00000002-0000-1000-8000-00805f9b34fb',
|
|
333
|
+
isWritableWithoutResponse: true,
|
|
334
|
+
writeWithoutResponse: jest.fn(() => Promise.resolve()),
|
|
335
|
+
};
|
|
336
|
+
const notifyCharacteristic = {
|
|
337
|
+
uuid: '00000003-0000-1000-8000-00805f9b34fb',
|
|
338
|
+
isNotifiable: true,
|
|
339
|
+
monitor,
|
|
340
|
+
};
|
|
341
|
+
const device = {
|
|
342
|
+
id: UUID,
|
|
343
|
+
name: 'Pro 2 48E9',
|
|
344
|
+
localName: 'Pro 2 48E9',
|
|
345
|
+
mtu: 247,
|
|
346
|
+
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
347
|
+
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
348
|
+
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
349
|
+
discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
|
|
350
|
+
characteristicsForService: jest.fn(() =>
|
|
351
|
+
Promise.resolve([writeCharacteristic, notifyCharacteristic])
|
|
352
|
+
),
|
|
353
|
+
services: jest.fn(() => Promise.resolve([])),
|
|
354
|
+
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
355
|
+
};
|
|
356
|
+
const transport = new ReactNativeBleTransport({});
|
|
357
|
+
(transport as any).blePlxManager = {
|
|
358
|
+
devices: jest.fn(() => Promise.resolve([device])),
|
|
359
|
+
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
360
|
+
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
361
|
+
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
362
|
+
onDeviceDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
363
|
+
onStateChange: jest.fn((listener: (state: string) => void) => {
|
|
364
|
+
setImmediate(() => listener('PoweredOn'));
|
|
365
|
+
return { remove: jest.fn() };
|
|
366
|
+
}),
|
|
367
|
+
state: jest.fn(() => Promise.resolve('PoweredOn')),
|
|
368
|
+
};
|
|
369
|
+
transport.init(
|
|
370
|
+
{ debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
|
|
371
|
+
new EventEmitter()
|
|
372
|
+
);
|
|
373
|
+
transport.configure(protocolV1Schema);
|
|
374
|
+
return { transport, monitor, device };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Drives acquire up to the point where it waits for the link's encryption result. */
|
|
378
|
+
async function reachGate(device: { characteristicsForService: jest.Mock }) {
|
|
379
|
+
for (let i = 0; i < 100 && device.characteristicsForService.mock.calls.length === 0; i += 1) {
|
|
380
|
+
// eslint-disable-next-line no-await-in-loop
|
|
381
|
+
await advance(10);
|
|
382
|
+
}
|
|
383
|
+
expect(device.characteristicsForService).toHaveBeenCalled();
|
|
384
|
+
await advance(10);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function runUntil(done: () => boolean, maxMs = 2000) {
|
|
388
|
+
for (let elapsed = 0; elapsed < maxMs && !done(); elapsed += 50) {
|
|
389
|
+
// eslint-disable-next-line no-await-in-loop
|
|
390
|
+
await advance(50);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
test('subscribes to notifications only after a bonded link is encrypted', async () => {
|
|
395
|
+
const { transport, monitor, device } = createHarness();
|
|
396
|
+
const outcome = settle(transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }));
|
|
397
|
+
await reachGate(device);
|
|
398
|
+
await advance(ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS / 2);
|
|
399
|
+
expect(monitor).not.toHaveBeenCalled();
|
|
400
|
+
|
|
401
|
+
encrypted();
|
|
402
|
+
await runUntil(() => outcome.done);
|
|
403
|
+
|
|
404
|
+
expect(outcome.error).toBeUndefined();
|
|
405
|
+
expect(outcome.value).toEqual({ uuid: UUID, protocolType: 'V1' });
|
|
406
|
+
expect(monitor).toHaveBeenCalledTimes(1);
|
|
407
|
+
await transport.release(UUID, true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('keeps GATT idle while the system re-pairs a lost bond', async () => {
|
|
411
|
+
const { transport, monitor, device } = createHarness();
|
|
412
|
+
const outcome = settle(transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }));
|
|
413
|
+
await reachGate(device);
|
|
414
|
+
keyMissing();
|
|
415
|
+
await advance(8000);
|
|
416
|
+
expect(monitor).not.toHaveBeenCalled();
|
|
417
|
+
expect(outcome.done).toBe(false);
|
|
418
|
+
|
|
419
|
+
encrypted();
|
|
420
|
+
await runUntil(() => outcome.done);
|
|
421
|
+
|
|
422
|
+
expect(outcome.value).toEqual({ uuid: UUID, protocolType: 'V1' });
|
|
423
|
+
expect(monitor).toHaveBeenCalledTimes(1);
|
|
424
|
+
await transport.release(UUID, true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('fails as an invalid bond when the system re-pairing fails', async () => {
|
|
428
|
+
const { transport, monitor, device } = createHarness();
|
|
429
|
+
const outcome = settle(transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }));
|
|
430
|
+
await reachGate(device);
|
|
431
|
+
keyMissing();
|
|
432
|
+
await advance(5000);
|
|
433
|
+
mockEmitKeyMissing?.({ id: UUID });
|
|
434
|
+
await runUntil(() => outcome.done);
|
|
435
|
+
|
|
436
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
437
|
+
expect(monitor).not.toHaveBeenCalled();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('does not wait for a bond created by this acquire', async () => {
|
|
441
|
+
mockPairResult = { bonded: false, bonding: true, initiated: true };
|
|
442
|
+
const { transport, monitor } = createHarness();
|
|
443
|
+
const outcome = settle(transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }));
|
|
444
|
+
for (let i = 0; i < 100 && !mockEmitBondState; i += 1) {
|
|
445
|
+
// eslint-disable-next-line no-await-in-loop
|
|
446
|
+
await advance(10);
|
|
447
|
+
}
|
|
448
|
+
mockEmitBondState?.({
|
|
449
|
+
id: UUID,
|
|
450
|
+
advertising: {},
|
|
451
|
+
bondState: { preState: 'BOND_BONDING', state: 'BOND_BONDED' },
|
|
452
|
+
});
|
|
453
|
+
await runUntil(() => outcome.done);
|
|
454
|
+
|
|
455
|
+
expect(outcome.value).toEqual({ uuid: UUID, protocolType: 'V1' });
|
|
456
|
+
expect(monitor).toHaveBeenCalledTimes(1);
|
|
457
|
+
await transport.release(UUID, true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('reuses a link that notifications already worked on without waiting', async () => {
|
|
461
|
+
const { transport, monitor, device } = createHarness();
|
|
462
|
+
// No result arrives: the link was already up before tracking could observe it.
|
|
463
|
+
const first = settle(transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }));
|
|
464
|
+
await reachGate(device);
|
|
465
|
+
await advance(ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS - 100);
|
|
466
|
+
expect(monitor).not.toHaveBeenCalled();
|
|
467
|
+
await runUntil(() => first.done);
|
|
468
|
+
expect(first.value).toEqual({ uuid: UUID, protocolType: 'V1' });
|
|
469
|
+
await transport.release(UUID, true);
|
|
470
|
+
monitor.mockClear();
|
|
471
|
+
device.characteristicsForService.mockClear();
|
|
472
|
+
|
|
473
|
+
const second = settle(transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }));
|
|
474
|
+
await reachGate(device);
|
|
475
|
+
|
|
476
|
+
expect(monitor).toHaveBeenCalledTimes(1);
|
|
477
|
+
await runUntil(() => second.done);
|
|
478
|
+
expect(second.value).toEqual({ uuid: UUID, protocolType: 'V1' });
|
|
479
|
+
await transport.release(UUID, true);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -25,13 +25,20 @@ jest.mock('../BleManager', () => ({
|
|
|
25
25
|
pairDevice: jest.fn(),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
-
// The native facade is replaced wholesale above; key missing
|
|
28
|
+
// The native facade is replaced wholesale above; key missing and link encryption are covered
|
|
29
|
+
// by their own suites.
|
|
29
30
|
jest.mock('../bleKeyMissing', () => ({
|
|
30
31
|
isBleKeyMissingSupported: jest.fn(() => false),
|
|
31
32
|
startBleKeyMissingTracking: jest.fn(() => false),
|
|
32
33
|
stopBleKeyMissingTracking: jest.fn(),
|
|
33
34
|
waitForBleKeyMissing: jest.fn(() => Promise.resolve(false)),
|
|
34
35
|
}));
|
|
36
|
+
jest.mock('../bleEncryption', () => ({
|
|
37
|
+
markBleLinkEncrypted: jest.fn(),
|
|
38
|
+
startBleEncryptionTracking: jest.fn(() => false),
|
|
39
|
+
stopBleEncryptionTracking: jest.fn(),
|
|
40
|
+
waitForAndroidLinkEncryption: jest.fn(() => Promise.resolve('unresolved')),
|
|
41
|
+
}));
|
|
35
42
|
|
|
36
43
|
jest.mock('../subscribeBleOn', () => ({
|
|
37
44
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
@@ -45,13 +45,20 @@ jest.mock('../BleManager', () => ({
|
|
|
45
45
|
pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
-
// The native facade is replaced wholesale above; key missing
|
|
48
|
+
// The native facade is replaced wholesale above; key missing and link encryption are covered
|
|
49
|
+
// by their own suites.
|
|
49
50
|
jest.mock('../bleKeyMissing', () => ({
|
|
50
51
|
isBleKeyMissingSupported: jest.fn(() => false),
|
|
51
52
|
startBleKeyMissingTracking: jest.fn(() => false),
|
|
52
53
|
stopBleKeyMissingTracking: jest.fn(),
|
|
53
54
|
waitForBleKeyMissing: jest.fn(() => Promise.resolve(false)),
|
|
54
55
|
}));
|
|
56
|
+
jest.mock('../bleEncryption', () => ({
|
|
57
|
+
markBleLinkEncrypted: jest.fn(),
|
|
58
|
+
startBleEncryptionTracking: jest.fn(() => false),
|
|
59
|
+
stopBleEncryptionTracking: jest.fn(),
|
|
60
|
+
waitForAndroidLinkEncryption: jest.fn(() => Promise.resolve('unresolved')),
|
|
61
|
+
}));
|
|
55
62
|
|
|
56
63
|
jest.mock('../subscribeBleOn', () => ({
|
|
57
64
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|