@onekeyfe/hd-core 1.2.0-alpha.50 → 1.2.0-alpha.51
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/__tests__/AllNetworkGetAddressBase.tracing.test.ts +0 -78
- package/__tests__/DeviceEventRegistration.test.ts +0 -6
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +3 -111
- package/__tests__/connectSettings.test.ts +47 -5
- package/__tests__/device-initialize-timeout.test.ts +56 -0
- package/__tests__/device-lifecycle-events.test.ts +3 -105
- package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
- package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
- package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
- package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +213 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +58 -0
- package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
- package/__tests__/get-device-state.test.ts +12 -132
- package/__tests__/homescreen.test.ts +1 -41
- package/__tests__/logBlockEvent.test.ts +1 -1
- package/__tests__/method-protocol-support.test.ts +0 -19
- package/__tests__/open-wallet-session.test.ts +33 -446
- package/__tests__/protocol-v2-unlock-policy.test.ts +35 -196
- package/__tests__/protocol-v2.test.ts +1032 -1056
- package/__tests__/protocolV2UiInteraction.test.ts +0 -63
- package/__tests__/public-pro2-api-boundary.test.ts +0 -1
- package/__tests__/search-devices.test.ts +3 -12
- package/dist/api/BaseMethod.d.ts +1 -3
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +11 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts +7 -3
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +25 -9
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +9 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +30 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/api/firmware/getBinary.d.ts +7 -0
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/progressThrottle.d.ts +3 -0
- package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
- package/dist/api/firmware/updateBootloader.d.ts +2 -0
- package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts +9 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +0 -2
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +0 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
- package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
- package/dist/core/deviceEventRegistration.d.ts +0 -2
- package/dist/core/deviceEventRegistration.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +2 -4
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/connectSettings.d.ts.map +1 -1
- package/dist/device/Device.d.ts +6 -29
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/events/device.d.ts +0 -4
- package/dist/events/device.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-promise.d.ts +2 -4
- package/dist/events/ui-promise.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +2 -31
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/events/ui-response.d.ts +2 -10
- package/dist/events/ui-response.d.ts.map +1 -1
- package/dist/index.d.ts +230 -203
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3445 -2419
- package/dist/inject.d.ts +1 -6
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/lockedError.d.ts +2 -0
- package/dist/protocols/protocol-v2/lockedError.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/uiInteraction.d.ts +4 -7
- package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts +3 -1
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockRetry.d.ts +10 -0
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts +1 -4
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +4 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +70 -2
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
- package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +10 -7
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +0 -3
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/params.d.ts +0 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/types/settings.d.ts +13 -22
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +0 -2
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/homescreen.d.ts +0 -4
- package/dist/utils/homescreen.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2Wallpaper.d.ts +0 -10
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +10 -8
- package/src/api/CheckAllFirmwareRelease.ts +53 -41
- package/src/api/FirmwareUpdate.ts +15 -7
- package/src/api/FirmwareUpdateV2.ts +313 -126
- package/src/api/FirmwareUpdateV3.ts +265 -63
- package/src/api/FirmwareUpdateV4.ts +937 -430
- package/src/api/GetPassphraseState.ts +1 -7
- package/src/api/OpenWalletSession.ts +17 -54
- package/src/api/SearchDevices.ts +1 -3
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +11 -32
- package/src/api/device/DeviceUnlock.ts +1 -1
- package/src/api/device/DeviceUpdateBootloader.ts +122 -6
- package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
- package/src/api/firmware/FirmwareHostBinding.ts +100 -0
- package/src/api/firmware/FirmwarePreparationError.ts +12 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +83 -16
- package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +745 -0
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +392 -0
- package/src/api/firmware/progressThrottle.ts +44 -0
- package/src/api/firmware/updateBootloader.ts +19 -4
- package/src/api/firmware/uploadFirmware.ts +144 -39
- package/src/api/helpers/protocolV2FileWrite.ts +4 -11
- package/src/api/index.ts +0 -2
- package/src/api/protocol-v2/ProtocolInfoRequest.ts +1 -3
- package/src/core/deviceEventRegistration.ts +0 -4
- package/src/core/index.ts +83 -167
- package/src/data/messages/messages-protocol-v2.json +0 -25
- package/src/data-manager/DataManager.ts +38 -67
- package/src/data-manager/connectSettings.ts +11 -0
- package/src/device/Device.ts +42 -257
- package/src/device/DevicePool.ts +2 -7
- package/src/events/device.ts +0 -4
- package/src/events/logBlockEvent.ts +0 -1
- package/src/events/ui-promise.ts +2 -4
- package/src/events/ui-request.ts +2 -36
- package/src/events/ui-response.ts +2 -12
- package/src/index.ts +5 -0
- package/src/inject.ts +24 -60
- package/src/lowLevelInject.ts +8 -7
- package/src/protocols/protocol-v2/features.ts +2 -9
- package/src/protocols/protocol-v2/lockedError.ts +10 -0
- package/src/protocols/protocol-v2/uiInteraction.ts +6 -46
- package/src/protocols/protocol-v2/unlockPolicy.ts +105 -1
- package/src/protocols/protocol-v2/unlockRetry.ts +109 -0
- package/src/protocols/protocol-v2/walletSession.ts +30 -169
- package/src/topLevelInject.ts +8 -7
- package/src/types/api/checkAllFirmwareRelease.ts +4 -1
- package/src/types/api/deviceUpdateBootloader.ts +6 -0
- package/src/types/api/export.ts +18 -0
- package/src/types/api/firmwareUpdate.ts +76 -3
- package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
- package/src/types/api/firmwareUpdatePlan.ts +38 -0
- package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
- package/src/types/api/index.ts +14 -13
- package/src/types/api/protocolV2.ts +0 -13
- package/src/types/params.ts +1 -7
- package/src/types/settings.ts +13 -42
- package/src/utils/deviceFeaturesUtils.ts +0 -4
- package/src/utils/homescreen.ts +6 -32
- package/src/utils/index.ts +1 -6
- package/src/utils/pro2Wallpaper.ts +8 -35
- package/__tests__/deviceUploadNft.test.ts +0 -293
- package/__tests__/pro2Nft.test.ts +0 -108
- package/__tests__/protocol-binding.test.ts +0 -89
- package/__tests__/protocol-v2-resources.test.ts +0 -284
- package/__tests__/protocol-v2-ui-lifecycle.test.ts +0 -91
- package/__tests__/uiProgressThrottle.test.ts +0 -74
- package/__tests__/uiPromiseRegistry.test.ts +0 -86
- package/dist/api/DetectDeviceConnectProtocol.d.ts +0 -7
- package/dist/api/DetectDeviceConnectProtocol.d.ts.map +0 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +0 -31
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +0 -1
- package/dist/core/uiPromiseRegistry.d.ts +0 -4
- package/dist/core/uiPromiseRegistry.d.ts.map +0 -1
- package/dist/protocols/protocol-v2/resources.d.ts +0 -30
- package/dist/protocols/protocol-v2/resources.d.ts.map +0 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +0 -22
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +0 -1
- package/dist/types/api/detectDeviceConnectProtocol.d.ts +0 -4
- package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +0 -1
- package/dist/utils/pro2Nft.d.ts +0 -31
- package/dist/utils/pro2Nft.d.ts.map +0 -1
- package/dist/utils/uiProgressThrottle.d.ts +0 -3
- package/dist/utils/uiProgressThrottle.d.ts.map +0 -1
- package/src/api/DetectDeviceConnectProtocol.ts +0 -18
- package/src/api/protocol-v2/DeviceUploadNft.ts +0 -189
- package/src/core/uiPromiseRegistry.ts +0 -41
- package/src/protocols/protocol-v2/resources.ts +0 -359
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +0 -124
- package/src/types/api/detectDeviceConnectProtocol.ts +0 -7
- package/src/utils/pro2Nft.ts +0 -142
- package/src/utils/uiProgressThrottle.ts +0 -63
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
DeviceSettingsPage,
|
|
6
6
|
DeviceType,
|
|
7
7
|
} from '@onekeyfe/hd-transport';
|
|
8
|
-
import { sha256 } from '@noble/hashes/sha256';
|
|
9
8
|
|
|
10
9
|
import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
|
|
11
10
|
import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
|
|
@@ -62,7 +61,6 @@ import { createCoreApi } from '../src/inject';
|
|
|
62
61
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
63
62
|
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
64
63
|
import { DevicePool } from '../src/device/DevicePool';
|
|
65
|
-
import { DEVICE } from '../src/events';
|
|
66
64
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
67
65
|
import {
|
|
68
66
|
PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
@@ -76,15 +74,11 @@ import {
|
|
|
76
74
|
requestProtocolV2ProtocolInfo,
|
|
77
75
|
supportsProtocolV2Message,
|
|
78
76
|
} from '../src/protocols/protocol-v2/features';
|
|
79
|
-
import { PROTOCOL_V2_RESOURCE_DEVICE_PATHS } from '../src/protocols/protocol-v2/resources';
|
|
80
77
|
import {
|
|
81
78
|
getProtocolV2WalletSession,
|
|
82
79
|
refreshProtocolV2DeviceStatus,
|
|
83
80
|
} from '../src/protocols/protocol-v2/walletSession';
|
|
84
|
-
import {
|
|
85
|
-
createProtocolV2UnlockContext,
|
|
86
|
-
runMethodWithUnlockPolicy,
|
|
87
|
-
} from '../src/protocols/protocol-v2/unlockPolicyRunner';
|
|
81
|
+
import { runMethodWithUnlockRetry } from '../src/protocols/protocol-v2/unlockRetry';
|
|
88
82
|
import { BaseMethod } from '../src/api/BaseMethod';
|
|
89
83
|
import {
|
|
90
84
|
buildProtocolV1FeaturesPayload,
|
|
@@ -98,6 +92,7 @@ import {
|
|
|
98
92
|
getMethodVersionRange,
|
|
99
93
|
} from '../src/utils';
|
|
100
94
|
import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
|
|
95
|
+
import { openFirmwareByteSource } from '../src/api/firmware/FirmwareArtifactSource';
|
|
101
96
|
import {
|
|
102
97
|
getPassphraseState,
|
|
103
98
|
getPassphraseStateWithRefreshDeviceInfo,
|
|
@@ -246,7 +241,7 @@ describe('UploadPortfolio', () => {
|
|
|
246
241
|
(method as any).device = device;
|
|
247
242
|
|
|
248
243
|
method.init();
|
|
249
|
-
await
|
|
244
|
+
await runMethodWithUnlockRetry(method, device as any);
|
|
250
245
|
|
|
251
246
|
expect(unlockDevice).not.toHaveBeenCalled();
|
|
252
247
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
@@ -422,16 +417,14 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
422
417
|
}
|
|
423
418
|
return d.features;
|
|
424
419
|
};
|
|
425
|
-
d.ensureProtocolV2RuntimeContext ??= (timeoutMs?: number) =>
|
|
426
|
-
requestProtocolV2ProtocolInfo({
|
|
427
|
-
commands: d.getCommands?.() ?? d.commands,
|
|
428
|
-
timeoutMs,
|
|
429
|
-
});
|
|
430
420
|
d.probeProtocolV2RuntimeState ??= async (
|
|
431
421
|
deviceInfo: ProtocolV2DeviceInfo,
|
|
432
422
|
timeoutMs?: number
|
|
433
423
|
) => {
|
|
434
|
-
const protocolInfo = await
|
|
424
|
+
const protocolInfo = await requestProtocolV2ProtocolInfo({
|
|
425
|
+
commands: d.getCommands(),
|
|
426
|
+
timeoutMs,
|
|
427
|
+
});
|
|
435
428
|
const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
|
|
436
429
|
if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
|
|
437
430
|
return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
@@ -788,9 +781,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
788
781
|
if (request === 'DeviceStatusGet') {
|
|
789
782
|
return { type: 'DeviceStatus', message: { unlocked: true } };
|
|
790
783
|
}
|
|
791
|
-
if (request === 'DeviceSessionAskPassphrase') {
|
|
792
|
-
return { type: 'Success', message: {} };
|
|
793
|
-
}
|
|
794
784
|
if (request === 'DeviceSessionGet') {
|
|
795
785
|
return {
|
|
796
786
|
type: 'DeviceSession',
|
|
@@ -815,7 +805,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
815
805
|
).resolves.toEqual({
|
|
816
806
|
passphraseState: 'state-1',
|
|
817
807
|
newSession: 'session-1',
|
|
818
|
-
unlockedAttachPin:
|
|
808
|
+
unlockedAttachPin: undefined,
|
|
819
809
|
resumed: false,
|
|
820
810
|
});
|
|
821
811
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
@@ -824,13 +814,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
824
814
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
825
815
|
type: DeviceSessionPinType.Main,
|
|
826
816
|
});
|
|
827
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
828
|
-
passphrase: '',
|
|
829
|
-
on_device: false,
|
|
830
|
-
});
|
|
831
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
832
|
-
seed_domains: [],
|
|
833
|
-
});
|
|
817
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
834
818
|
device.passphraseState = 'state-1';
|
|
835
819
|
expect(device.getInternalState()).toBe('session-1');
|
|
836
820
|
});
|
|
@@ -863,7 +847,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
863
847
|
},
|
|
864
848
|
};
|
|
865
849
|
}
|
|
866
|
-
if (request === 'DeviceSessionAskPassphrase') return { message: {} };
|
|
867
850
|
if (request === 'DeviceSessionGet') {
|
|
868
851
|
return {
|
|
869
852
|
message: {
|
|
@@ -880,16 +863,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
880
863
|
await getProtocolV2WalletSession(device, { onlyMainPin: true });
|
|
881
864
|
|
|
882
865
|
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPin')).toHaveLength(1);
|
|
883
|
-
expect(
|
|
884
|
-
typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
|
|
885
|
-
).toHaveLength(1);
|
|
886
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
887
|
-
seed_domains: [],
|
|
888
|
-
});
|
|
866
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
889
867
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
890
868
|
session_id: 'standard-session',
|
|
891
|
-
btc_test_address: 'standard-state',
|
|
892
|
-
seed_domains: [],
|
|
893
869
|
});
|
|
894
870
|
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
895
871
|
passphraseState: 'standard-state',
|
|
@@ -931,7 +907,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
931
907
|
},
|
|
932
908
|
};
|
|
933
909
|
}
|
|
934
|
-
if (request === 'DeviceSessionAskPassphrase') return { message: {} };
|
|
935
910
|
if (request === 'DeviceSessionAskPin') {
|
|
936
911
|
return { type: 'Success', message: { message: 'PIN verified' } };
|
|
937
912
|
}
|
|
@@ -1026,7 +1001,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1026
1001
|
if (request === 'DeviceStatusGet') {
|
|
1027
1002
|
return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
|
|
1028
1003
|
}
|
|
1029
|
-
if (request === 'DeviceSessionAskPassphrase') return { message: {} };
|
|
1030
1004
|
if (request === 'DeviceSessionGet') {
|
|
1031
1005
|
sessionGetCalls += 1;
|
|
1032
1006
|
return {
|
|
@@ -1253,9 +1227,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1253
1227
|
passphrase: 'host hidden wallet',
|
|
1254
1228
|
on_device: false,
|
|
1255
1229
|
});
|
|
1256
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1257
|
-
seed_domains: [],
|
|
1258
|
-
});
|
|
1230
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1259
1231
|
});
|
|
1260
1232
|
|
|
1261
1233
|
test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
|
|
@@ -1363,8 +1335,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1363
1335
|
|
|
1364
1336
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1365
1337
|
session_id: 'session-a',
|
|
1366
|
-
btc_test_address: 'state-a',
|
|
1367
|
-
seed_domains: [],
|
|
1368
1338
|
});
|
|
1369
1339
|
expect(device.getInternalState()).toBe('session-b');
|
|
1370
1340
|
});
|
|
@@ -1432,15 +1402,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1432
1402
|
});
|
|
1433
1403
|
expect(typedCall.mock.calls).toEqual([
|
|
1434
1404
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1435
|
-
[
|
|
1436
|
-
'DeviceSessionGet',
|
|
1437
|
-
'DeviceSession',
|
|
1438
|
-
{
|
|
1439
|
-
session_id: 'session-a',
|
|
1440
|
-
btc_test_address: 'state-a',
|
|
1441
|
-
seed_domains: [],
|
|
1442
|
-
},
|
|
1443
|
-
],
|
|
1405
|
+
['DeviceSessionGet', 'DeviceSession', { session_id: 'session-a' }],
|
|
1444
1406
|
]);
|
|
1445
1407
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1446
1408
|
expect(device.getInternalState()).toBeUndefined();
|
|
@@ -1488,7 +1450,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1488
1450
|
{ passphrase: 'host hidden wallet', on_device: false },
|
|
1489
1451
|
],
|
|
1490
1452
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1491
|
-
['DeviceSessionGet', 'DeviceSession', {
|
|
1453
|
+
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1492
1454
|
]);
|
|
1493
1455
|
});
|
|
1494
1456
|
|
|
@@ -1591,73 +1553,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1591
1553
|
expect(device.getInternalState()).toBeUndefined();
|
|
1592
1554
|
});
|
|
1593
1555
|
|
|
1594
|
-
test.each([false, true])(
|
|
1595
|
-
'rejects a different Attach PIN wallet when Pro2 lock failure is %s',
|
|
1596
|
-
async lockFails => {
|
|
1597
|
-
const deviceId = 'attach-mismatch-device';
|
|
1598
|
-
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1599
|
-
(device as any).features = normalizeProtocolV2Features(
|
|
1600
|
-
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1601
|
-
{
|
|
1602
|
-
status: {
|
|
1603
|
-
device_id: deviceId,
|
|
1604
|
-
unlocked: true,
|
|
1605
|
-
passphrase_enabled: true,
|
|
1606
|
-
attach_to_pin_enabled: true,
|
|
1607
|
-
unlocked_by_attach_to_pin: false,
|
|
1608
|
-
},
|
|
1609
|
-
}
|
|
1610
|
-
);
|
|
1611
|
-
device.passphraseState = 'expected-state';
|
|
1612
|
-
|
|
1613
|
-
const typedCall = jest.fn((request: string) => {
|
|
1614
|
-
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
1615
|
-
if (request === 'DeviceSessionAskPin') return { message: { message: 'PIN verified' } };
|
|
1616
|
-
if (request === 'DeviceStatusGet') {
|
|
1617
|
-
return {
|
|
1618
|
-
message: {
|
|
1619
|
-
device_id: deviceId,
|
|
1620
|
-
unlocked: true,
|
|
1621
|
-
passphrase_enabled: true,
|
|
1622
|
-
attach_to_pin_enabled: true,
|
|
1623
|
-
unlocked_by_attach_to_pin: true,
|
|
1624
|
-
},
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
if (request === 'DeviceSessionGet') {
|
|
1628
|
-
return {
|
|
1629
|
-
message: {
|
|
1630
|
-
btc_test_address: 'wrong-attach-state',
|
|
1631
|
-
session_id: 'wrong-attach-session',
|
|
1632
|
-
},
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
if (request === 'LockDevice') {
|
|
1636
|
-
if (lockFails) throw new Error('Lock unsupported');
|
|
1637
|
-
return { message: { message: 'Device locked' } };
|
|
1638
|
-
}
|
|
1639
|
-
throw new Error(`Unexpected request: ${request}`);
|
|
1640
|
-
});
|
|
1641
|
-
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
1642
|
-
(device as any).commands = { typedCall, promptPassphrase };
|
|
1643
|
-
|
|
1644
|
-
await expect(
|
|
1645
|
-
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1646
|
-
).rejects.toMatchObject({
|
|
1647
|
-
errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
|
|
1648
|
-
});
|
|
1649
|
-
|
|
1650
|
-
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1651
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1652
|
-
btc_test_address: 'expected-state',
|
|
1653
|
-
seed_domains: [],
|
|
1654
|
-
});
|
|
1655
|
-
expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
|
|
1656
|
-
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
1657
|
-
expect(device.getInternalState()).toBeUndefined();
|
|
1658
|
-
}
|
|
1659
|
-
);
|
|
1660
|
-
|
|
1661
1556
|
test('recovers an expired Attach PIN wallet through the unified wallet selector', async () => {
|
|
1662
1557
|
const device = Device.fromDescriptor({
|
|
1663
1558
|
id: 'cache-device-attach-recovery',
|
|
@@ -1714,17 +1609,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1714
1609
|
}),
|
|
1715
1610
|
{ cancelDeviceOnReject: false }
|
|
1716
1611
|
);
|
|
1717
|
-
expect(unlockDevice).toHaveBeenCalledWith(
|
|
1718
|
-
DeviceSessionPinType.AttachToPin,
|
|
1719
|
-
expect.objectContaining({
|
|
1720
|
-
emitUiEvent: false,
|
|
1721
|
-
interaction: expect.objectContaining({
|
|
1722
|
-
phase: 'pin',
|
|
1723
|
-
transition: 'start',
|
|
1724
|
-
protocol: 'V2',
|
|
1725
|
-
}),
|
|
1726
|
-
})
|
|
1727
|
-
);
|
|
1612
|
+
expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
|
|
1728
1613
|
expect(device.getInternalState()).toBe('new-attach-session');
|
|
1729
1614
|
});
|
|
1730
1615
|
|
|
@@ -2008,10 +1893,12 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2008
1893
|
expect(getFeatures).not.toHaveBeenCalled();
|
|
2009
1894
|
});
|
|
2010
1895
|
|
|
2011
|
-
test('
|
|
1896
|
+
test('marks an interactive cache miss as a wallet-session recovery', async () => {
|
|
2012
1897
|
const typedCall = jest
|
|
2013
1898
|
.fn()
|
|
2014
1899
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1900
|
+
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1901
|
+
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
2015
1902
|
.mockResolvedValueOnce({
|
|
2016
1903
|
message: {
|
|
2017
1904
|
btc_test_address: 'expected-state',
|
|
@@ -2039,11 +1926,16 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2039
1926
|
await expect(
|
|
2040
1927
|
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
2041
1928
|
).resolves.toMatchObject({ passphraseState: 'expected-state' });
|
|
2042
|
-
expect(
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
1929
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
1930
|
+
{
|
|
1931
|
+
existsAttachPinUser: false,
|
|
1932
|
+
deviceOnly: false,
|
|
1933
|
+
source: 'wallet-session-coordinator',
|
|
1934
|
+
reason: 'session-recovery',
|
|
1935
|
+
expectedPassphraseState: 'expected-state',
|
|
1936
|
+
},
|
|
1937
|
+
{ cancelDeviceOnReject: false }
|
|
1938
|
+
);
|
|
2047
1939
|
});
|
|
2048
1940
|
|
|
2049
1941
|
test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
|
|
@@ -2109,15 +2001,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2109
2001
|
true
|
|
2110
2002
|
);
|
|
2111
2003
|
expect(typedCall.mock.calls).toEqual([
|
|
2112
|
-
[
|
|
2113
|
-
|
|
2114
|
-
'DeviceSession',
|
|
2115
|
-
{
|
|
2116
|
-
session_id: 'session-pro2-app',
|
|
2117
|
-
btc_test_address: 'state-pro2-app',
|
|
2118
|
-
seed_domains: [],
|
|
2119
|
-
},
|
|
2120
|
-
],
|
|
2004
|
+
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
2005
|
+
['DeviceSessionGet', 'DeviceSession', { session_id: 'session-pro2-app' }],
|
|
2121
2006
|
]);
|
|
2122
2007
|
});
|
|
2123
2008
|
|
|
@@ -2306,9 +2191,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2306
2191
|
passphrase: 'host hidden wallet',
|
|
2307
2192
|
on_device: false,
|
|
2308
2193
|
});
|
|
2309
|
-
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2310
|
-
seed_domains: [],
|
|
2311
|
-
});
|
|
2194
|
+
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2312
2195
|
});
|
|
2313
2196
|
|
|
2314
2197
|
test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
|
|
@@ -2386,18 +2269,12 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2386
2269
|
);
|
|
2387
2270
|
|
|
2388
2271
|
expect(device.getInternalState()).toBeUndefined();
|
|
2389
|
-
expect(typedCall).
|
|
2390
|
-
btc_test_address: 'expected-state',
|
|
2391
|
-
seed_domains: [],
|
|
2392
|
-
});
|
|
2393
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2272
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
2394
2273
|
passphrase: 'host hidden wallet',
|
|
2395
2274
|
on_device: false,
|
|
2396
2275
|
});
|
|
2397
|
-
expect(typedCall).
|
|
2398
|
-
expect(typedCall).
|
|
2399
|
-
seed_domains: [],
|
|
2400
|
-
});
|
|
2276
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2277
|
+
expect(typedCall).toHaveBeenNthCalledWith(4, 'DeviceSessionGet', 'DeviceSession', {});
|
|
2401
2278
|
});
|
|
2402
2279
|
|
|
2403
2280
|
test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
|
|
@@ -2434,15 +2311,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2434
2311
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2435
2312
|
eventless_wallet_session: true,
|
|
2436
2313
|
});
|
|
2437
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2438
|
-
seed_domains: [],
|
|
2439
|
-
});
|
|
2440
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2441
|
-
passphrase: '',
|
|
2442
|
-
on_device: false,
|
|
2443
|
-
});
|
|
2444
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2445
|
-
expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
|
|
2446
2314
|
});
|
|
2447
2315
|
|
|
2448
2316
|
test('marks fallback features as unavailable when DeviceInfo is missing', () => {
|
|
@@ -2813,9 +2681,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2813
2681
|
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
2814
2682
|
}
|
|
2815
2683
|
);
|
|
2816
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2817
|
-
eventless_wallet_session: true,
|
|
2818
|
-
});
|
|
2684
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
|
|
2819
2685
|
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2820
2686
|
});
|
|
2821
2687
|
|
|
@@ -2853,9 +2719,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2853
2719
|
}),
|
|
2854
2720
|
expect.anything()
|
|
2855
2721
|
);
|
|
2856
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2857
|
-
eventless_wallet_session: true,
|
|
2858
|
-
});
|
|
2722
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
|
|
2859
2723
|
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2860
2724
|
expect(device.features).toMatchObject({
|
|
2861
2725
|
deviceId: null,
|
|
@@ -3121,6 +2985,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3121
2985
|
fw: { application: { version: '1.2.4' } },
|
|
3122
2986
|
},
|
|
3123
2987
|
})
|
|
2988
|
+
.mockResolvedValueOnce({
|
|
2989
|
+
type: 'ProtocolInfo',
|
|
2990
|
+
message: protocolV2ApplicationInfo,
|
|
2991
|
+
})
|
|
3124
2992
|
.mockResolvedValueOnce({
|
|
3125
2993
|
type: 'DeviceStatus',
|
|
3126
2994
|
message: { init_states: true, unlocked: true },
|
|
@@ -3142,7 +3010,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3142
3010
|
firmwareVersion: '1.2.4',
|
|
3143
3011
|
passphraseProtection: null,
|
|
3144
3012
|
});
|
|
3145
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3013
|
+
expect(typedCall).toHaveBeenCalledTimes(6);
|
|
3146
3014
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3147
3015
|
4,
|
|
3148
3016
|
'DeviceInfoGet',
|
|
@@ -3256,23 +3124,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3256
3124
|
status: { init_states: true },
|
|
3257
3125
|
}
|
|
3258
3126
|
);
|
|
3259
|
-
const pinOnDevice = jest.fn();
|
|
3260
|
-
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
3261
3127
|
|
|
3262
3128
|
const features = await device.unlockDevice();
|
|
3263
3129
|
|
|
3264
|
-
expect(pinOnDevice).toHaveBeenCalledWith(
|
|
3265
|
-
device,
|
|
3266
|
-
DeviceSessionPinType.Main,
|
|
3267
|
-
expect.objectContaining({
|
|
3268
|
-
source: 'unlock-coordinator',
|
|
3269
|
-
reason: 'device-unlock',
|
|
3270
|
-
deviceOnly: true,
|
|
3271
|
-
})
|
|
3272
|
-
);
|
|
3273
|
-
expect(pinOnDevice.mock.invocationCallOrder[0]).toBeLessThan(
|
|
3274
|
-
typedCall.mock.invocationCallOrder[0]
|
|
3275
|
-
);
|
|
3276
3130
|
expect(typedCall.mock.calls).toEqual([
|
|
3277
3131
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
3278
3132
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -3318,12 +3172,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3318
3172
|
throw new Error(`Unexpected request: ${requestType}`);
|
|
3319
3173
|
});
|
|
3320
3174
|
(device as any).commands = { typedCall };
|
|
3321
|
-
const pinOnDevice = jest.fn();
|
|
3322
|
-
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
3323
3175
|
|
|
3324
|
-
await device.unlockDevice(
|
|
3176
|
+
await device.unlockDevice();
|
|
3325
3177
|
|
|
3326
|
-
expect(pinOnDevice).not.toHaveBeenCalled();
|
|
3327
3178
|
expect(typedCall.mock.calls).toEqual([
|
|
3328
3179
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
3329
3180
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -3726,15 +3577,6 @@ describe('API compatibility handling', () => {
|
|
|
3726
3577
|
|
|
3727
3578
|
describe('Protocol V2 firmware update targets', () => {
|
|
3728
3579
|
const OKPP_HEADER_SIZE = 0x52a0;
|
|
3729
|
-
let forceReloadDataSpy: jest.SpyInstance<Promise<void>, []>;
|
|
3730
|
-
|
|
3731
|
-
beforeEach(() => {
|
|
3732
|
-
forceReloadDataSpy = jest.spyOn(DataManager, 'forceReloadData').mockResolvedValue(undefined);
|
|
3733
|
-
});
|
|
3734
|
-
|
|
3735
|
-
afterEach(() => {
|
|
3736
|
-
jest.restoreAllMocks();
|
|
3737
|
-
});
|
|
3738
3580
|
|
|
3739
3581
|
const buildOkppHeader = ({
|
|
3740
3582
|
type = 'RESC',
|
|
@@ -3875,7 +3717,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3875
3717
|
});
|
|
3876
3718
|
});
|
|
3877
3719
|
|
|
3878
|
-
test('
|
|
3720
|
+
test('delegates Protocol V2 mode transition to the phase executor', async () => {
|
|
3879
3721
|
const method = new FirmwareUpdateV4({
|
|
3880
3722
|
id: 1,
|
|
3881
3723
|
payload: {
|
|
@@ -3910,7 +3752,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3910
3752
|
|
|
3911
3753
|
await method.run();
|
|
3912
3754
|
|
|
3913
|
-
expect((method as any).enterProtocolV2BootloaderMode).
|
|
3755
|
+
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
3914
3756
|
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
3915
3757
|
fwBinaryMap: [
|
|
3916
3758
|
{
|
|
@@ -3923,68 +3765,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3923
3765
|
});
|
|
3924
3766
|
});
|
|
3925
3767
|
|
|
3926
|
-
test('enters Protocol V2 bootloader before reading and downloading remote resources', async () => {
|
|
3927
|
-
const method = new FirmwareUpdateV4({
|
|
3928
|
-
id: 1,
|
|
3929
|
-
payload: {
|
|
3930
|
-
method: 'firmwareUpdateV4',
|
|
3931
|
-
platform: 'web',
|
|
3932
|
-
targetsToUpdate: ['resource'],
|
|
3933
|
-
},
|
|
3934
|
-
});
|
|
3935
|
-
method.init();
|
|
3936
|
-
|
|
3937
|
-
(method as any).device = stubDevice({
|
|
3938
|
-
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
3939
|
-
features: {
|
|
3940
|
-
deviceType: 'pro2',
|
|
3941
|
-
firmwareVersion: '1.0.0',
|
|
3942
|
-
mode: 'normal',
|
|
3943
|
-
bootloaderMode: false,
|
|
3944
|
-
capabilities: [],
|
|
3945
|
-
},
|
|
3946
|
-
isBootloader: () => false,
|
|
3947
|
-
isRomloader: () => false,
|
|
3948
|
-
});
|
|
3949
|
-
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
3950
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
3951
|
-
bootloaderBinary: null,
|
|
3952
|
-
fwBinaryMap: [],
|
|
3953
|
-
installItems: [],
|
|
3954
|
-
});
|
|
3955
|
-
|
|
3956
|
-
const order: string[] = [];
|
|
3957
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
|
|
3958
|
-
order.push('enter-bootloader');
|
|
3959
|
-
return Promise.resolve(true);
|
|
3960
|
-
});
|
|
3961
|
-
(method as any).prepareProtocolV2ResourceBundles = jest.fn().mockImplementation(() => {
|
|
3962
|
-
order.push('prepare-resources');
|
|
3963
|
-
return Promise.resolve([
|
|
3964
|
-
{
|
|
3965
|
-
name: 'images.okpkg',
|
|
3966
|
-
binary: new Uint8Array([1]).buffer,
|
|
3967
|
-
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
3968
|
-
},
|
|
3969
|
-
]);
|
|
3970
|
-
});
|
|
3971
|
-
(method as any).executeProtocolV2Update = jest.fn().mockImplementation(() => {
|
|
3972
|
-
order.push('execute-update');
|
|
3973
|
-
return Promise.resolve();
|
|
3974
|
-
});
|
|
3975
|
-
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
3976
|
-
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
3977
|
-
bootloaderVersion: '1.0.0',
|
|
3978
|
-
bleVersion: '1.0.0',
|
|
3979
|
-
firmwareVersion: '1.0.0',
|
|
3980
|
-
});
|
|
3981
|
-
method.postTipMessage = jest.fn();
|
|
3982
|
-
|
|
3983
|
-
await method.run();
|
|
3984
|
-
|
|
3985
|
-
expect(order).toEqual(['enter-bootloader', 'prepare-resources', 'execute-update']);
|
|
3986
|
-
});
|
|
3987
|
-
|
|
3988
3768
|
test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
|
|
3989
3769
|
const method = new FirmwareUpdateV4({
|
|
3990
3770
|
id: 1,
|
|
@@ -4034,33 +3814,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4034
3814
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4035
3815
|
});
|
|
4036
3816
|
|
|
4037
|
-
test('keeps Protocol V2 bootloader active before firmware transfer', async () => {
|
|
4038
|
-
const method = new FirmwareUpdateV4({
|
|
4039
|
-
id: 1,
|
|
4040
|
-
payload: {
|
|
4041
|
-
method: 'firmwareUpdateV4',
|
|
4042
|
-
},
|
|
4043
|
-
});
|
|
4044
|
-
(method as any).device = stubDevice({
|
|
4045
|
-
originalDescriptor: { id: 'usb-id', path: 'bootloader-path', protocolType: 'V2' },
|
|
4046
|
-
features: {
|
|
4047
|
-
deviceType: 'pro2',
|
|
4048
|
-
mode: 'bootloader',
|
|
4049
|
-
bootloaderMode: true,
|
|
4050
|
-
capabilities: [],
|
|
4051
|
-
},
|
|
4052
|
-
isBootloader: () => true,
|
|
4053
|
-
isRomloader: () => false,
|
|
4054
|
-
});
|
|
4055
|
-
(method as any).protocolV2Reboot = jest.fn();
|
|
4056
|
-
method.postTipMessage = jest.fn();
|
|
4057
|
-
|
|
4058
|
-
await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
|
|
4059
|
-
|
|
4060
|
-
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
4061
|
-
expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
|
|
4062
|
-
});
|
|
4063
|
-
|
|
4064
3817
|
test('keeps Protocol V2 romloader active before firmware transfer', async () => {
|
|
4065
3818
|
const method = new FirmwareUpdateV4({
|
|
4066
3819
|
id: 1,
|
|
@@ -4113,9 +3866,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4113
3866
|
},
|
|
4114
3867
|
});
|
|
4115
3868
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4116
|
-
const prepareResourceBundles = jest
|
|
4117
|
-
.spyOn(method as any, 'prepareProtocolV2ResourceBundles')
|
|
4118
|
-
.mockResolvedValue(undefined);
|
|
4119
3869
|
(method as any).protocolV2Reboot = jest.fn();
|
|
4120
3870
|
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
4121
3871
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
@@ -4128,7 +3878,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4128
3878
|
|
|
4129
3879
|
await method.run();
|
|
4130
3880
|
|
|
4131
|
-
expect(prepareResourceBundles).not.toHaveBeenCalled();
|
|
4132
3881
|
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
4133
3882
|
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
4134
3883
|
bootloaderBinary: null,
|
|
@@ -4211,10 +3960,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4211
3960
|
getCommands: () => commands,
|
|
4212
3961
|
mainId: 'usb-path',
|
|
4213
3962
|
});
|
|
4214
|
-
const cachedDevice = {
|
|
4215
|
-
getConnectId: () => 'usb-path',
|
|
4216
|
-
getCurrentSerialNo: () => '',
|
|
4217
|
-
};
|
|
3963
|
+
const cachedDevice = { getConnectId: () => 'usb-path' };
|
|
4218
3964
|
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4219
3965
|
devices: {},
|
|
4220
3966
|
deviceList: [cachedDevice],
|
|
@@ -4231,259 +3977,83 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4231
3977
|
expect(initialize).not.toHaveBeenCalled();
|
|
4232
3978
|
});
|
|
4233
3979
|
|
|
4234
|
-
test('
|
|
3980
|
+
test('reuses an acquired Protocol V2 USB session while polling reconnect readiness', async () => {
|
|
4235
3981
|
const method = new FirmwareUpdateV4({
|
|
4236
3982
|
id: 1,
|
|
4237
3983
|
payload: {
|
|
4238
3984
|
method: 'firmwareUpdateV4',
|
|
4239
3985
|
},
|
|
4240
3986
|
});
|
|
4241
|
-
const commands = { disposed:
|
|
4242
|
-
const
|
|
3987
|
+
const commands = { disposed: false, mainId: 'usb-session' };
|
|
3988
|
+
const acquire = jest.fn().mockResolvedValue(undefined);
|
|
4243
3989
|
const device = stubDevice({
|
|
4244
|
-
originalDescriptor: {
|
|
3990
|
+
originalDescriptor: {
|
|
3991
|
+
id: 'usb-id',
|
|
3992
|
+
path: 'usb-path',
|
|
3993
|
+
session: 'usb-session',
|
|
3994
|
+
protocolType: 'V2',
|
|
3995
|
+
},
|
|
4245
3996
|
deviceConnector: {
|
|
4246
3997
|
enumerate: jest.fn().mockResolvedValue({
|
|
4247
3998
|
descriptors: [
|
|
4248
|
-
{
|
|
4249
|
-
|
|
3999
|
+
{
|
|
4000
|
+
id: 'usb-id',
|
|
4001
|
+
path: 'usb-path',
|
|
4002
|
+
session: 'usb-session',
|
|
4003
|
+
protocolType: 'V2',
|
|
4004
|
+
},
|
|
4250
4005
|
],
|
|
4251
4006
|
}),
|
|
4252
4007
|
},
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4008
|
+
updateDescriptor: jest.fn(),
|
|
4009
|
+
updateFromCache: jest.fn(),
|
|
4010
|
+
hasDeviceAcquire: jest.fn(() => true),
|
|
4011
|
+
acquire,
|
|
4256
4012
|
commands,
|
|
4257
4013
|
getCommands: () => commands,
|
|
4258
|
-
mainId: '
|
|
4014
|
+
mainId: 'usb-session',
|
|
4259
4015
|
});
|
|
4260
|
-
const otherDevice = {
|
|
4261
|
-
getConnectId: () => 'usb-a',
|
|
4262
|
-
getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
|
|
4263
|
-
};
|
|
4264
|
-
const expectedDevice = {
|
|
4265
|
-
getConnectId: () => 'usb-b',
|
|
4266
|
-
getCurrentSerialNo: () => 'PRO2-PHYSICAL-B',
|
|
4267
|
-
};
|
|
4268
4016
|
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4269
4017
|
devices: {},
|
|
4270
|
-
deviceList: [
|
|
4018
|
+
deviceList: [{ getConnectId: () => 'usb-path' }],
|
|
4271
4019
|
} as any);
|
|
4272
4020
|
(method as any).device = device;
|
|
4273
|
-
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-B';
|
|
4274
4021
|
|
|
4275
4022
|
try {
|
|
4276
4023
|
await (method as any).reconnectProtocolV2Device();
|
|
4024
|
+
await (method as any).reconnectProtocolV2Device();
|
|
4277
4025
|
} finally {
|
|
4278
4026
|
getDevices.mockRestore();
|
|
4279
4027
|
}
|
|
4280
4028
|
|
|
4281
|
-
expect(
|
|
4029
|
+
expect(acquire).not.toHaveBeenCalled();
|
|
4030
|
+
expect(device.hasDeviceAcquire).toHaveBeenCalledTimes(2);
|
|
4282
4031
|
});
|
|
4283
4032
|
|
|
4284
|
-
test('
|
|
4033
|
+
test('stops final reconnect polling immediately when firmware changes device identity', async () => {
|
|
4285
4034
|
const method = new FirmwareUpdateV4({
|
|
4286
4035
|
id: 1,
|
|
4287
4036
|
payload: {
|
|
4288
4037
|
method: 'firmwareUpdateV4',
|
|
4289
4038
|
},
|
|
4290
4039
|
});
|
|
4291
|
-
const
|
|
4292
|
-
const
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' },
|
|
4298
|
-
{ id: 'usb-b', path: 'usb-b', protocolType: 'V2' },
|
|
4299
|
-
],
|
|
4300
|
-
}),
|
|
4040
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4041
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
4042
|
+
type: 'DeviceInfo',
|
|
4043
|
+
message: {
|
|
4044
|
+
hw: { serial_no: 'PR2SERIAL' },
|
|
4045
|
+
fw: { application: { version: '1.0.0' } },
|
|
4301
4046
|
},
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
getConnectId: () => 'usb-b',
|
|
4313
|
-
getCurrentSerialNo: () => 'PRO2-PHYSICAL-B',
|
|
4314
|
-
},
|
|
4315
|
-
],
|
|
4316
|
-
} as any);
|
|
4317
|
-
(method as any).device = device;
|
|
4318
|
-
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-C';
|
|
4319
|
-
|
|
4320
|
-
try {
|
|
4321
|
-
await expect((method as any).reconnectProtocolV2Device()).rejects.toBeDefined();
|
|
4322
|
-
} finally {
|
|
4323
|
-
getDevices.mockRestore();
|
|
4324
|
-
}
|
|
4325
|
-
|
|
4326
|
-
expect(updateFromCache).not.toHaveBeenCalled();
|
|
4327
|
-
});
|
|
4328
|
-
|
|
4329
|
-
test('allows a single Protocol V2 USB reconnect candidate when its physical serial is unavailable', async () => {
|
|
4330
|
-
const method = new FirmwareUpdateV4({
|
|
4331
|
-
id: 1,
|
|
4332
|
-
payload: {
|
|
4333
|
-
method: 'firmwareUpdateV4',
|
|
4334
|
-
},
|
|
4335
|
-
});
|
|
4336
|
-
const commands = { disposed: true, mainId: '' };
|
|
4337
|
-
const updateFromCache = jest.fn();
|
|
4338
|
-
const device = stubDevice({
|
|
4339
|
-
originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
|
|
4340
|
-
deviceConnector: {
|
|
4341
|
-
enumerate: jest.fn().mockResolvedValue({
|
|
4342
|
-
descriptors: [{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' }],
|
|
4343
|
-
}),
|
|
4344
|
-
},
|
|
4345
|
-
updateFromCache,
|
|
4346
|
-
hasDeviceAcquire: jest.fn(() => false),
|
|
4347
|
-
acquire: jest.fn().mockResolvedValue(undefined),
|
|
4348
|
-
commands,
|
|
4349
|
-
getCommands: () => commands,
|
|
4350
|
-
mainId: 'old-usb-path',
|
|
4351
|
-
});
|
|
4352
|
-
const candidate = {
|
|
4353
|
-
getConnectId: () => 'usb-a',
|
|
4354
|
-
getCurrentSerialNo: () => '',
|
|
4355
|
-
};
|
|
4356
|
-
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4357
|
-
devices: {},
|
|
4358
|
-
deviceList: [candidate],
|
|
4359
|
-
} as any);
|
|
4360
|
-
(method as any).device = device;
|
|
4361
|
-
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-A';
|
|
4362
|
-
|
|
4363
|
-
try {
|
|
4364
|
-
await (method as any).reconnectProtocolV2Device();
|
|
4365
|
-
} finally {
|
|
4366
|
-
getDevices.mockRestore();
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
expect(updateFromCache).toHaveBeenCalledWith(candidate);
|
|
4370
|
-
});
|
|
4371
|
-
|
|
4372
|
-
test('rejects a single Protocol V2 USB reconnect candidate with a different physical serial', async () => {
|
|
4373
|
-
const method = new FirmwareUpdateV4({
|
|
4374
|
-
id: 1,
|
|
4375
|
-
payload: {
|
|
4376
|
-
method: 'firmwareUpdateV4',
|
|
4377
|
-
},
|
|
4378
|
-
});
|
|
4379
|
-
const updateFromCache = jest.fn();
|
|
4380
|
-
const device = stubDevice({
|
|
4381
|
-
originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
|
|
4382
|
-
deviceConnector: {
|
|
4383
|
-
enumerate: jest.fn().mockResolvedValue({
|
|
4384
|
-
descriptors: [{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' }],
|
|
4385
|
-
}),
|
|
4386
|
-
},
|
|
4387
|
-
updateFromCache,
|
|
4388
|
-
});
|
|
4389
|
-
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4390
|
-
devices: {},
|
|
4391
|
-
deviceList: [
|
|
4392
|
-
{
|
|
4393
|
-
getConnectId: () => 'usb-a',
|
|
4394
|
-
getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
|
|
4395
|
-
},
|
|
4396
|
-
],
|
|
4397
|
-
} as any);
|
|
4398
|
-
(method as any).device = device;
|
|
4399
|
-
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-B';
|
|
4400
|
-
|
|
4401
|
-
try {
|
|
4402
|
-
await expect((method as any).reconnectProtocolV2Device()).rejects.toBeDefined();
|
|
4403
|
-
} finally {
|
|
4404
|
-
getDevices.mockRestore();
|
|
4405
|
-
}
|
|
4406
|
-
|
|
4407
|
-
expect(updateFromCache).not.toHaveBeenCalled();
|
|
4408
|
-
});
|
|
4409
|
-
|
|
4410
|
-
test('reuses an acquired Protocol V2 USB session while polling reconnect readiness', async () => {
|
|
4411
|
-
const method = new FirmwareUpdateV4({
|
|
4412
|
-
id: 1,
|
|
4413
|
-
payload: {
|
|
4414
|
-
method: 'firmwareUpdateV4',
|
|
4415
|
-
},
|
|
4416
|
-
});
|
|
4417
|
-
const commands = { disposed: false, mainId: 'usb-session' };
|
|
4418
|
-
const acquire = jest.fn().mockResolvedValue(undefined);
|
|
4419
|
-
const device = stubDevice({
|
|
4420
|
-
originalDescriptor: {
|
|
4421
|
-
id: 'usb-id',
|
|
4422
|
-
path: 'usb-path',
|
|
4423
|
-
session: 'usb-session',
|
|
4424
|
-
protocolType: 'V2',
|
|
4425
|
-
},
|
|
4426
|
-
deviceConnector: {
|
|
4427
|
-
enumerate: jest.fn().mockResolvedValue({
|
|
4428
|
-
descriptors: [
|
|
4429
|
-
{
|
|
4430
|
-
id: 'usb-id',
|
|
4431
|
-
path: 'usb-path',
|
|
4432
|
-
session: 'usb-session',
|
|
4433
|
-
protocolType: 'V2',
|
|
4434
|
-
},
|
|
4435
|
-
],
|
|
4436
|
-
}),
|
|
4437
|
-
},
|
|
4438
|
-
updateDescriptor: jest.fn(),
|
|
4439
|
-
updateFromCache: jest.fn(),
|
|
4440
|
-
hasDeviceAcquire: jest.fn(() => true),
|
|
4441
|
-
acquire,
|
|
4442
|
-
commands,
|
|
4443
|
-
getCommands: () => commands,
|
|
4444
|
-
mainId: 'usb-session',
|
|
4445
|
-
});
|
|
4446
|
-
const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
4447
|
-
devices: {},
|
|
4448
|
-
deviceList: [{ getConnectId: () => 'usb-path' }],
|
|
4449
|
-
} as any);
|
|
4450
|
-
(method as any).device = device;
|
|
4451
|
-
|
|
4452
|
-
try {
|
|
4453
|
-
await (method as any).reconnectProtocolV2Device();
|
|
4454
|
-
await (method as any).reconnectProtocolV2Device();
|
|
4455
|
-
} finally {
|
|
4456
|
-
getDevices.mockRestore();
|
|
4457
|
-
}
|
|
4458
|
-
|
|
4459
|
-
expect(acquire).not.toHaveBeenCalled();
|
|
4460
|
-
expect(device.hasDeviceAcquire).toHaveBeenCalledTimes(2);
|
|
4461
|
-
});
|
|
4462
|
-
|
|
4463
|
-
test('stops final reconnect polling immediately when firmware changes device identity', async () => {
|
|
4464
|
-
const method = new FirmwareUpdateV4({
|
|
4465
|
-
id: 1,
|
|
4466
|
-
payload: {
|
|
4467
|
-
method: 'firmwareUpdateV4',
|
|
4468
|
-
},
|
|
4469
|
-
});
|
|
4470
|
-
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4471
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
4472
|
-
type: 'DeviceInfo',
|
|
4473
|
-
message: {
|
|
4474
|
-
hw: { serial_no: 'PR2SERIAL' },
|
|
4475
|
-
fw: { application: { version: '1.0.0' } },
|
|
4476
|
-
},
|
|
4477
|
-
});
|
|
4478
|
-
(method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
|
|
4479
|
-
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4480
|
-
(method as any).device = stubDevice({
|
|
4481
|
-
getCommands: () => ({ typedCall }),
|
|
4482
|
-
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4483
|
-
deviceId: 'changed-device',
|
|
4484
|
-
mode: 'normal',
|
|
4485
|
-
bootloaderMode: false,
|
|
4486
|
-
}),
|
|
4047
|
+
});
|
|
4048
|
+
(method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
|
|
4049
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4050
|
+
(method as any).device = stubDevice({
|
|
4051
|
+
getCommands: () => ({ typedCall }),
|
|
4052
|
+
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4053
|
+
deviceId: 'changed-device',
|
|
4054
|
+
mode: 'normal',
|
|
4055
|
+
bootloaderMode: false,
|
|
4056
|
+
}),
|
|
4487
4057
|
});
|
|
4488
4058
|
|
|
4489
4059
|
await expect((method as any).waitForProtocolV2ReconnectAndFeatures(60_000)).rejects.toThrow(
|
|
@@ -4565,9 +4135,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4565
4135
|
const typedCall = jest
|
|
4566
4136
|
.fn()
|
|
4567
4137
|
.mockRejectedValue(
|
|
4568
|
-
|
|
4569
|
-
"Failed to execute 'open' on 'USBDevice': The device was disconnected",
|
|
4570
|
-
'NotFoundError'
|
|
4138
|
+
Object.assign(
|
|
4139
|
+
new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
|
|
4140
|
+
{ name: 'NotFoundError' }
|
|
4571
4141
|
)
|
|
4572
4142
|
);
|
|
4573
4143
|
|
|
@@ -4748,6 +4318,77 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4748
4318
|
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
4749
4319
|
});
|
|
4750
4320
|
|
|
4321
|
+
test('rejects a finished target whose payload version conflicts with the plan', () => {
|
|
4322
|
+
const method = new FirmwareUpdateV4({
|
|
4323
|
+
id: 1,
|
|
4324
|
+
payload: {
|
|
4325
|
+
method: 'firmwareUpdateV4',
|
|
4326
|
+
platform: 'desktop',
|
|
4327
|
+
expectedTargetVersions: {
|
|
4328
|
+
app_v1: '2.0.0',
|
|
4329
|
+
},
|
|
4330
|
+
},
|
|
4331
|
+
});
|
|
4332
|
+
method.init();
|
|
4333
|
+
method.postProgressMessage = jest.fn();
|
|
4334
|
+
|
|
4335
|
+
expect(() =>
|
|
4336
|
+
(method as any).assertProtocolV2TargetStatus(
|
|
4337
|
+
[{ target_id: 4, status: 2, payload_version: 0x010000 }],
|
|
4338
|
+
new Set([4])
|
|
4339
|
+
)
|
|
4340
|
+
).toThrow('expected 131072');
|
|
4341
|
+
});
|
|
4342
|
+
|
|
4343
|
+
test('does not fail a completed multi-app install when final app versions are unobservable', () => {
|
|
4344
|
+
const method = new FirmwareUpdateV4({
|
|
4345
|
+
id: 1,
|
|
4346
|
+
payload: {
|
|
4347
|
+
method: 'firmwareUpdateV4',
|
|
4348
|
+
platform: 'desktop',
|
|
4349
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
4350
|
+
expectedTargetVersions: {
|
|
4351
|
+
app_v1: '1.0.0',
|
|
4352
|
+
app_v2: '2.0.0',
|
|
4353
|
+
},
|
|
4354
|
+
},
|
|
4355
|
+
});
|
|
4356
|
+
method.init();
|
|
4357
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
4358
|
+
major_version: 3,
|
|
4359
|
+
minor_version: 0,
|
|
4360
|
+
patch_version: 0,
|
|
4361
|
+
};
|
|
4362
|
+
(method as any).protocolV2FinalStatusVerified = true;
|
|
4363
|
+
|
|
4364
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
4365
|
+
});
|
|
4366
|
+
|
|
4367
|
+
test('rejects unobservable final versions after the status endpoint fallback', () => {
|
|
4368
|
+
const method = new FirmwareUpdateV4({
|
|
4369
|
+
id: 1,
|
|
4370
|
+
payload: {
|
|
4371
|
+
method: 'firmwareUpdateV4',
|
|
4372
|
+
platform: 'desktop',
|
|
4373
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
4374
|
+
expectedTargetVersions: {
|
|
4375
|
+
app_v1: '1.0.0',
|
|
4376
|
+
app_v2: '2.0.0',
|
|
4377
|
+
},
|
|
4378
|
+
},
|
|
4379
|
+
});
|
|
4380
|
+
method.init();
|
|
4381
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
4382
|
+
major_version: 3,
|
|
4383
|
+
minor_version: 0,
|
|
4384
|
+
patch_version: 0,
|
|
4385
|
+
};
|
|
4386
|
+
|
|
4387
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
4388
|
+
'has no observable final version after status fallback'
|
|
4389
|
+
);
|
|
4390
|
+
});
|
|
4391
|
+
|
|
4751
4392
|
test('polls target status after update ACK and finishes when all targets complete', async () => {
|
|
4752
4393
|
const method = new FirmwareUpdateV4({
|
|
4753
4394
|
id: 1,
|
|
@@ -4824,7 +4465,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4824
4465
|
).toBe(true);
|
|
4825
4466
|
|
|
4826
4467
|
(method.postProgressMessage as jest.Mock).mockClear();
|
|
4827
|
-
expect(
|
|
4468
|
+
expect(() =>
|
|
4828
4469
|
(method as any).assertProtocolV2TargetStatus(
|
|
4829
4470
|
[
|
|
4830
4471
|
{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
|
|
@@ -4832,8 +4473,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4832
4473
|
],
|
|
4833
4474
|
new Set([4, 10])
|
|
4834
4475
|
)
|
|
4835
|
-
).
|
|
4836
|
-
expect(method.postProgressMessage).
|
|
4476
|
+
).toThrow('Protocol V2 install status conflicts with target 4');
|
|
4477
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
4837
4478
|
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4838
4479
|
|
|
4839
4480
|
expect(
|
|
@@ -4937,10 +4578,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4937
4578
|
const writtenPaths: string[] = [];
|
|
4938
4579
|
method.postTipMessage = jest.fn();
|
|
4939
4580
|
method.postProgressMessage = jest.fn();
|
|
4940
|
-
(method as any).
|
|
4581
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockImplementation(params => {
|
|
4941
4582
|
writtenPaths.push(params.filePath);
|
|
4942
|
-
return Number(params.processedSize ?? 0) + Number(params.
|
|
4583
|
+
return Number(params.processedSize ?? 0) + Number(params.source.size);
|
|
4943
4584
|
});
|
|
4585
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
4586
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
4587
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
|
|
4588
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
4944
4589
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
4945
4590
|
(method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
|
|
4946
4591
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest
|
|
@@ -4980,10 +4625,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4980
4625
|
'vol0:/coprocessor.bin',
|
|
4981
4626
|
1
|
|
4982
4627
|
);
|
|
4983
|
-
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(
|
|
4984
|
-
expect((method as any).protocolV2StartFirmwareUpdate).
|
|
4628
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
|
|
4629
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
|
|
4630
|
+
targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
|
|
4631
|
+
});
|
|
4632
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
|
|
4985
4633
|
targets: [
|
|
4986
|
-
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
4987
4634
|
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
4988
4635
|
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
4989
4636
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
@@ -4993,7 +4640,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4993
4640
|
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
|
|
4994
4641
|
});
|
|
4995
4642
|
|
|
4996
|
-
test('announces
|
|
4643
|
+
test('announces separate resource and component transfer phases', async () => {
|
|
4997
4644
|
const method = new FirmwareUpdateV4({
|
|
4998
4645
|
id: 1,
|
|
4999
4646
|
payload: {
|
|
@@ -5003,11 +4650,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5003
4650
|
|
|
5004
4651
|
method.postTipMessage = jest.fn();
|
|
5005
4652
|
method.postProgressMessage = jest.fn();
|
|
5006
|
-
(method as any).
|
|
4653
|
+
(method as any).protocolV2SourceUpdateProcess = jest
|
|
5007
4654
|
.fn()
|
|
5008
4655
|
.mockImplementation(params =>
|
|
5009
|
-
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.
|
|
4656
|
+
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
|
|
5010
4657
|
);
|
|
4658
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
4659
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
4660
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5011
4661
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
5012
4662
|
(method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
|
|
5013
4663
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest
|
|
@@ -5027,21 +4677,22 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5027
4677
|
{
|
|
5028
4678
|
fileName: 'application_p1.bin',
|
|
5029
4679
|
binary: new Uint8Array([3]).buffer,
|
|
5030
|
-
targetId:
|
|
4680
|
+
targetId: 4,
|
|
5031
4681
|
},
|
|
5032
4682
|
],
|
|
5033
4683
|
});
|
|
5034
4684
|
|
|
5035
|
-
expect(method.postTipMessage).toHaveBeenCalledTimes(
|
|
4685
|
+
expect(method.postTipMessage).toHaveBeenCalledTimes(3);
|
|
5036
4686
|
expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
|
|
5037
|
-
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, '
|
|
5038
|
-
expect(
|
|
4687
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
|
|
4688
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
|
|
4689
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5039
4690
|
1,
|
|
5040
|
-
expect.objectContaining({ processedSize: 0, totalSize:
|
|
4691
|
+
expect.objectContaining({ processedSize: 0, totalSize: 2 })
|
|
5041
4692
|
);
|
|
5042
|
-
expect((method as any).
|
|
4693
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5043
4694
|
2,
|
|
5044
|
-
expect.objectContaining({ processedSize:
|
|
4695
|
+
expect.objectContaining({ processedSize: 0, totalSize: 1 })
|
|
5045
4696
|
);
|
|
5046
4697
|
});
|
|
5047
4698
|
|
|
@@ -5055,7 +4706,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5055
4706
|
|
|
5056
4707
|
method.postTipMessage = jest.fn();
|
|
5057
4708
|
method.postProgressMessage = jest.fn();
|
|
5058
|
-
(method as any).
|
|
4709
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
4710
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5059
4711
|
(method as any).device = stubDevice({
|
|
5060
4712
|
getCommands: () => ({
|
|
5061
4713
|
typedCall: jest.fn().mockResolvedValue({
|
|
@@ -5101,11 +4753,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5101
4753
|
|
|
5102
4754
|
method.postTipMessage = jest.fn();
|
|
5103
4755
|
method.postProgressMessage = jest.fn();
|
|
5104
|
-
(method as any).
|
|
4756
|
+
(method as any).protocolV2SourceUpdateProcess = jest
|
|
5105
4757
|
.fn()
|
|
5106
4758
|
.mockImplementation(params =>
|
|
5107
|
-
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.
|
|
4759
|
+
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
|
|
5108
4760
|
);
|
|
4761
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
4762
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
4763
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5109
4764
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
5110
4765
|
(method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
|
|
5111
4766
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest
|
|
@@ -5320,42 +4975,33 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5320
4975
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5321
4976
|
});
|
|
5322
4977
|
|
|
5323
|
-
test('
|
|
4978
|
+
test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
|
|
5324
4979
|
const method = new FirmwareUpdateV4({
|
|
5325
4980
|
id: 1,
|
|
5326
4981
|
payload: {
|
|
5327
4982
|
method: 'firmwareUpdateV4',
|
|
5328
4983
|
platform: 'web',
|
|
4984
|
+
targetsToUpdate: ['resource'],
|
|
5329
4985
|
},
|
|
5330
4986
|
});
|
|
5331
4987
|
method.init();
|
|
5332
4988
|
|
|
5333
|
-
const getSysResourceBinarySpy = jest
|
|
5334
|
-
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5335
|
-
.mockResolvedValue({ binary: new Uint8Array([1]).buffer });
|
|
5336
4989
|
const getFirmwareLatestReleaseSpy = jest
|
|
5337
4990
|
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5338
4991
|
.mockReturnValue({
|
|
5339
4992
|
required: false,
|
|
5340
4993
|
version: [1, 0, 0],
|
|
5341
|
-
url: '
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
applicationP1: {
|
|
5351
|
-
target: 'APPLICATION_P1',
|
|
5352
|
-
url: 'https://example.com/applicationP1.pp.bin',
|
|
5353
|
-
},
|
|
5354
|
-
applicationP2: {
|
|
5355
|
-
target: 'APPLICATION_P2',
|
|
5356
|
-
url: 'https://example.com/applicationP2.pp.bin',
|
|
4994
|
+
url: '',
|
|
4995
|
+
resourceBundles: [
|
|
4996
|
+
{
|
|
4997
|
+
name: 'images',
|
|
4998
|
+
url: 'https://example.com/images.okpkg',
|
|
4999
|
+
devicePath: 'VOL0:/resource/images/images.okpkg',
|
|
5000
|
+
version: [2, 0, 0],
|
|
5001
|
+
payloadHash: 'ab'.repeat(64),
|
|
5002
|
+
headerHash: 'cd'.repeat(64),
|
|
5357
5003
|
},
|
|
5358
|
-
|
|
5004
|
+
],
|
|
5359
5005
|
fingerprint: '',
|
|
5360
5006
|
changelog: {
|
|
5361
5007
|
'zh-CN': '',
|
|
@@ -5363,53 +5009,187 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5363
5009
|
},
|
|
5364
5010
|
});
|
|
5365
5011
|
|
|
5366
|
-
const
|
|
5012
|
+
const bundles = (method as any).prepareProtocolV2ResourceBundles('universal', {
|
|
5367
5013
|
deviceType: 'pro2',
|
|
5368
5014
|
firmwareVersion: '1.0.0',
|
|
5369
|
-
bootloaderVersion: '1.0.0',
|
|
5370
5015
|
capabilities: [],
|
|
5371
5016
|
});
|
|
5372
5017
|
|
|
5373
|
-
expect(
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5018
|
+
expect(bundles).toEqual([
|
|
5019
|
+
{
|
|
5020
|
+
name: 'images',
|
|
5021
|
+
binary: expect.any(ArrayBuffer),
|
|
5022
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5023
|
+
url: 'https://example.com/images.okpkg',
|
|
5024
|
+
version: [2, 0, 0],
|
|
5025
|
+
payloadHash: 'ab'.repeat(64),
|
|
5026
|
+
headerHash: 'cd'.repeat(64),
|
|
5027
|
+
},
|
|
5028
|
+
]);
|
|
5029
|
+
expect(bundles[0].binary.byteLength).toBe(0);
|
|
5379
5030
|
|
|
5380
|
-
getSysResourceBinarySpy.mockRestore();
|
|
5381
5031
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5382
5032
|
});
|
|
5383
5033
|
|
|
5384
|
-
test('
|
|
5034
|
+
test('rejects remote RESC bundle paths before download or bootloader entry', async () => {
|
|
5385
5035
|
const method = new FirmwareUpdateV4({
|
|
5386
5036
|
id: 1,
|
|
5387
5037
|
payload: {
|
|
5388
5038
|
method: 'firmwareUpdateV4',
|
|
5389
5039
|
platform: 'web',
|
|
5040
|
+
targetsToUpdate: ['resource'],
|
|
5390
5041
|
},
|
|
5391
5042
|
});
|
|
5392
5043
|
method.init();
|
|
5393
5044
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5394
5045
|
|
|
5395
|
-
const
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5046
|
+
const getSysResourceBinarySpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
5047
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
5048
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5049
|
+
.mockReturnValue({
|
|
5050
|
+
required: false,
|
|
5051
|
+
version: [1, 0, 0],
|
|
5052
|
+
url: '',
|
|
5053
|
+
resourceBundles: [
|
|
5054
|
+
{
|
|
5055
|
+
name: 'images',
|
|
5056
|
+
url: 'https://example.com/images.okpkg',
|
|
5057
|
+
devicePath: 'vol0:/resource/../images.okpkg',
|
|
5058
|
+
},
|
|
5059
|
+
],
|
|
5060
|
+
fingerprint: '',
|
|
5061
|
+
changelog: {
|
|
5062
|
+
'zh-CN': '',
|
|
5063
|
+
'en-US': '',
|
|
5402
5064
|
},
|
|
5403
|
-
|
|
5404
|
-
};
|
|
5065
|
+
});
|
|
5405
5066
|
|
|
5406
5067
|
(method as any).device = stubDevice({
|
|
5407
5068
|
originalDescriptor: { protocolType: 'V2' },
|
|
5408
|
-
features: { deviceType: 'pro2', firmwareVersion: '
|
|
5069
|
+
features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
|
|
5409
5070
|
});
|
|
5410
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(
|
|
5411
|
-
|
|
5412
|
-
|
|
5071
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
5072
|
+
bootloaderBinary: null,
|
|
5073
|
+
fwBinaryMap: [],
|
|
5074
|
+
});
|
|
5075
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
5076
|
+
method.postTipMessage = jest.fn();
|
|
5077
|
+
|
|
5078
|
+
await expect(method.run()).rejects.toThrow('resourceBundles[0].devicePath');
|
|
5079
|
+
expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
|
|
5080
|
+
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
5081
|
+
|
|
5082
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5083
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5084
|
+
});
|
|
5085
|
+
|
|
5086
|
+
test('rejects prepared RESC bundle paths before opening an artifact source', async () => {
|
|
5087
|
+
const method = new FirmwareUpdateV4({
|
|
5088
|
+
id: 1,
|
|
5089
|
+
payload: {
|
|
5090
|
+
method: 'firmwareUpdateV4',
|
|
5091
|
+
},
|
|
5092
|
+
});
|
|
5093
|
+
const openPreparedSource = jest.fn();
|
|
5094
|
+
(method as any).params = {
|
|
5095
|
+
targetsToUpdate: ['resource'],
|
|
5096
|
+
resourceBundleArtifacts: [
|
|
5097
|
+
{
|
|
5098
|
+
name: 'images',
|
|
5099
|
+
artifact: {
|
|
5100
|
+
artifactRef: `fw:${'a'.repeat(64)}`,
|
|
5101
|
+
size: 1,
|
|
5102
|
+
sha256: 'a'.repeat(64),
|
|
5103
|
+
},
|
|
5104
|
+
},
|
|
5105
|
+
],
|
|
5106
|
+
};
|
|
5107
|
+
(method as any).openProtocolV2PreparedSource = openPreparedSource;
|
|
5108
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
5109
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5110
|
+
.mockReturnValue({
|
|
5111
|
+
required: false,
|
|
5112
|
+
version: [1, 0, 0],
|
|
5113
|
+
url: '',
|
|
5114
|
+
resourceBundles: [
|
|
5115
|
+
{
|
|
5116
|
+
name: 'images',
|
|
5117
|
+
url: 'https://example.com/images.okpkg',
|
|
5118
|
+
devicePath: 'vol0:/resource/../images.okpkg',
|
|
5119
|
+
},
|
|
5120
|
+
],
|
|
5121
|
+
fingerprint: '',
|
|
5122
|
+
changelog: {
|
|
5123
|
+
'zh-CN': '',
|
|
5124
|
+
'en-US': '',
|
|
5125
|
+
},
|
|
5126
|
+
});
|
|
5127
|
+
|
|
5128
|
+
await expect(
|
|
5129
|
+
(method as any).prepareProtocolV2ResourceSources('universal', {
|
|
5130
|
+
deviceType: 'pro2',
|
|
5131
|
+
})
|
|
5132
|
+
).rejects.toThrow('resourceBundles[].devicePath');
|
|
5133
|
+
expect(openPreparedSource).not.toHaveBeenCalled();
|
|
5134
|
+
|
|
5135
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5136
|
+
});
|
|
5137
|
+
|
|
5138
|
+
test('downloads and validates remote RESC bundles before entering bootloader', async () => {
|
|
5139
|
+
const method = new FirmwareUpdateV4({
|
|
5140
|
+
id: 1,
|
|
5141
|
+
payload: {
|
|
5142
|
+
method: 'firmwareUpdateV4',
|
|
5143
|
+
platform: 'web',
|
|
5144
|
+
targetsToUpdate: ['resource'],
|
|
5145
|
+
},
|
|
5146
|
+
});
|
|
5147
|
+
method.init();
|
|
5148
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5149
|
+
|
|
5150
|
+
const order: string[] = [];
|
|
5151
|
+
const resourceBinary = buildOkppHeader().buffer as ArrayBuffer;
|
|
5152
|
+
const getSysResourceBinarySpy = jest
|
|
5153
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5154
|
+
.mockImplementation(() => {
|
|
5155
|
+
order.push('download');
|
|
5156
|
+
return Promise.resolve({ binary: resourceBinary });
|
|
5157
|
+
});
|
|
5158
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
5159
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5160
|
+
.mockReturnValue({
|
|
5161
|
+
required: false,
|
|
5162
|
+
version: [1, 0, 0],
|
|
5163
|
+
url: '',
|
|
5164
|
+
resourceBundles: [
|
|
5165
|
+
{
|
|
5166
|
+
name: 'images',
|
|
5167
|
+
url: 'https://example.com/images.okpkg',
|
|
5168
|
+
devicePath: ' VOL0:/resource/images/images.okpkg ',
|
|
5169
|
+
},
|
|
5170
|
+
],
|
|
5171
|
+
fingerprint: '',
|
|
5172
|
+
changelog: {
|
|
5173
|
+
'zh-CN': '',
|
|
5174
|
+
'en-US': '',
|
|
5175
|
+
},
|
|
5176
|
+
});
|
|
5177
|
+
|
|
5178
|
+
(method as any).device = stubDevice({
|
|
5179
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
5180
|
+
features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
|
|
5181
|
+
});
|
|
5182
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
5183
|
+
bootloaderBinary: null,
|
|
5184
|
+
fwBinaryMap: [],
|
|
5185
|
+
});
|
|
5186
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
|
|
5187
|
+
order.push('bootloader');
|
|
5188
|
+
return Promise.resolve(true);
|
|
5189
|
+
});
|
|
5190
|
+
const executeProtocolV2UpdateSpy = jest.spyOn(method as any, 'executeProtocolV2Update');
|
|
5191
|
+
(method as any).executeProtocolV2TransferPhase = jest.fn().mockResolvedValue(undefined);
|
|
5192
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue(undefined);
|
|
5413
5193
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5414
5194
|
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
5415
5195
|
bootloaderVersion: '1.0.0',
|
|
@@ -5420,37 +5200,208 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5420
5200
|
|
|
5421
5201
|
await method.run();
|
|
5422
5202
|
|
|
5423
|
-
expect(
|
|
5424
|
-
expect(
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5203
|
+
expect(order).toEqual(['download', 'bootloader']);
|
|
5204
|
+
expect(executeProtocolV2UpdateSpy).toHaveBeenCalledWith({
|
|
5205
|
+
bootloaderBinary: null,
|
|
5206
|
+
fwBinaryMap: [],
|
|
5207
|
+
resourceBundles: [
|
|
5208
|
+
{
|
|
5209
|
+
name: 'images',
|
|
5210
|
+
binary: resourceBinary,
|
|
5211
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5212
|
+
url: 'https://example.com/images.okpkg',
|
|
5213
|
+
},
|
|
5214
|
+
],
|
|
5428
5215
|
});
|
|
5216
|
+
|
|
5217
|
+
executeProtocolV2UpdateSpy.mockRestore();
|
|
5218
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5219
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5429
5220
|
});
|
|
5430
5221
|
|
|
5431
|
-
test('
|
|
5222
|
+
test('rejects a remote RESC bundle without an OKPP header when metadata is absent', async () => {
|
|
5223
|
+
const method = new FirmwareUpdateV4({
|
|
5224
|
+
id: 1,
|
|
5225
|
+
payload: {
|
|
5226
|
+
method: 'firmwareUpdateV4',
|
|
5227
|
+
},
|
|
5228
|
+
});
|
|
5229
|
+
method.init();
|
|
5230
|
+
const getSysResourceBinarySpy = jest
|
|
5231
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5232
|
+
.mockResolvedValue({ binary: new Uint8Array([1, 2, 3]).buffer });
|
|
5233
|
+
|
|
5234
|
+
await expect(
|
|
5235
|
+
(method as any).materializeProtocolV2ResourceBundles([
|
|
5236
|
+
{
|
|
5237
|
+
name: 'images',
|
|
5238
|
+
binary: new ArrayBuffer(0),
|
|
5239
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5240
|
+
url: 'https://example.com/images.okpkg',
|
|
5241
|
+
},
|
|
5242
|
+
])
|
|
5243
|
+
).rejects.toThrow('Invalid Protocol V2 RESC bundle header: images');
|
|
5244
|
+
|
|
5245
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5246
|
+
});
|
|
5247
|
+
|
|
5248
|
+
test('rejects an empty remote RESC bundle before entering bootloader', async () => {
|
|
5432
5249
|
const method = new FirmwareUpdateV4({
|
|
5433
5250
|
id: 1,
|
|
5434
5251
|
payload: {
|
|
5435
5252
|
method: 'firmwareUpdateV4',
|
|
5436
5253
|
platform: 'web',
|
|
5254
|
+
targetsToUpdate: ['resource'],
|
|
5437
5255
|
},
|
|
5438
5256
|
});
|
|
5439
5257
|
method.init();
|
|
5440
5258
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5259
|
+
|
|
5260
|
+
const getSysResourceBinarySpy = jest
|
|
5261
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5262
|
+
.mockResolvedValue({ binary: new ArrayBuffer(0) });
|
|
5263
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
5264
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5265
|
+
.mockReturnValue({
|
|
5266
|
+
required: false,
|
|
5267
|
+
version: [1, 0, 0],
|
|
5268
|
+
url: '',
|
|
5269
|
+
resourceBundles: [
|
|
5270
|
+
{
|
|
5271
|
+
name: 'images',
|
|
5272
|
+
url: 'https://example.com/images.okpkg',
|
|
5273
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5274
|
+
},
|
|
5275
|
+
],
|
|
5276
|
+
fingerprint: '',
|
|
5277
|
+
changelog: {
|
|
5278
|
+
'zh-CN': '',
|
|
5279
|
+
'en-US': '',
|
|
5280
|
+
},
|
|
5281
|
+
});
|
|
5441
5282
|
(method as any).device = stubDevice({
|
|
5442
5283
|
originalDescriptor: { protocolType: 'V2' },
|
|
5443
|
-
features: { deviceType: 'pro2', firmwareVersion: '
|
|
5284
|
+
features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
|
|
5285
|
+
});
|
|
5286
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
5287
|
+
bootloaderBinary: null,
|
|
5288
|
+
fwBinaryMap: [],
|
|
5444
5289
|
});
|
|
5445
|
-
forceReloadDataSpy.mockRejectedValue(new Error('latest config unavailable'));
|
|
5446
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn();
|
|
5447
5290
|
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
5448
5291
|
method.postTipMessage = jest.fn();
|
|
5449
5292
|
|
|
5450
|
-
await expect(method.run()).rejects.toThrow('
|
|
5451
|
-
|
|
5452
|
-
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
5293
|
+
await expect(method.run()).rejects.toThrow('Protocol V2 RESC bundle is empty: images');
|
|
5453
5294
|
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
5295
|
+
|
|
5296
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5297
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5298
|
+
});
|
|
5299
|
+
|
|
5300
|
+
test('does not download Pro2 firmware components that App did not select', async () => {
|
|
5301
|
+
const method = new FirmwareUpdateV4({
|
|
5302
|
+
id: 1,
|
|
5303
|
+
payload: {
|
|
5304
|
+
method: 'firmwareUpdateV4',
|
|
5305
|
+
platform: 'web',
|
|
5306
|
+
},
|
|
5307
|
+
});
|
|
5308
|
+
method.init();
|
|
5309
|
+
|
|
5310
|
+
const getSysResourceBinarySpy = jest
|
|
5311
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5312
|
+
.mockResolvedValue({ binary: new Uint8Array([1]).buffer });
|
|
5313
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
5314
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5315
|
+
.mockReturnValue({
|
|
5316
|
+
required: false,
|
|
5317
|
+
version: [1, 0, 0],
|
|
5318
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
5319
|
+
bootloaderVersion: [1, 0, 0],
|
|
5320
|
+
upgradeType: 'payload-package-set',
|
|
5321
|
+
installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
|
|
5322
|
+
components: {
|
|
5323
|
+
bootloader: {
|
|
5324
|
+
target: 'BOOTLOADER',
|
|
5325
|
+
url: 'https://example.com/bootloader.pp.bin',
|
|
5326
|
+
},
|
|
5327
|
+
applicationP1: {
|
|
5328
|
+
target: 'APPLICATION_P1',
|
|
5329
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
5330
|
+
},
|
|
5331
|
+
applicationP2: {
|
|
5332
|
+
target: 'APPLICATION_P2',
|
|
5333
|
+
url: 'https://example.com/applicationP2.pp.bin',
|
|
5334
|
+
},
|
|
5335
|
+
},
|
|
5336
|
+
fingerprint: '',
|
|
5337
|
+
changelog: {
|
|
5338
|
+
'zh-CN': '',
|
|
5339
|
+
'en-US': '',
|
|
5340
|
+
},
|
|
5341
|
+
});
|
|
5342
|
+
|
|
5343
|
+
const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
|
|
5344
|
+
deviceType: 'pro2',
|
|
5345
|
+
firmwareVersion: '1.0.0',
|
|
5346
|
+
bootloaderVersion: '1.0.0',
|
|
5347
|
+
capabilities: [],
|
|
5348
|
+
});
|
|
5349
|
+
|
|
5350
|
+
expect(remoteBinaries).toEqual({
|
|
5351
|
+
bootloaderBinary: null,
|
|
5352
|
+
fwBinaryMap: [],
|
|
5353
|
+
installItems: [],
|
|
5354
|
+
});
|
|
5355
|
+
expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
|
|
5356
|
+
|
|
5357
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5358
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5359
|
+
});
|
|
5360
|
+
|
|
5361
|
+
test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => {
|
|
5362
|
+
const method = new FirmwareUpdateV4({
|
|
5363
|
+
id: 1,
|
|
5364
|
+
payload: {
|
|
5365
|
+
method: 'firmwareUpdateV4',
|
|
5366
|
+
platform: 'web',
|
|
5367
|
+
},
|
|
5368
|
+
});
|
|
5369
|
+
method.init();
|
|
5370
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5371
|
+
|
|
5372
|
+
const remoteBinaries = {
|
|
5373
|
+
bootloaderBinary: new Uint8Array([1]).buffer,
|
|
5374
|
+
fwBinaryMap: [
|
|
5375
|
+
{
|
|
5376
|
+
fileName: 'application_p1.bin',
|
|
5377
|
+
binary: new Uint8Array([2]).buffer,
|
|
5378
|
+
targetId: 3,
|
|
5379
|
+
},
|
|
5380
|
+
],
|
|
5381
|
+
};
|
|
5382
|
+
|
|
5383
|
+
(method as any).device = stubDevice({
|
|
5384
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
5385
|
+
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
5386
|
+
});
|
|
5387
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
|
|
5388
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
5389
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
5390
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5391
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
5392
|
+
bootloaderVersion: '1.0.0',
|
|
5393
|
+
bleVersion: '0.0.0',
|
|
5394
|
+
firmwareVersion: '1.0.0',
|
|
5395
|
+
});
|
|
5396
|
+
method.postTipMessage = jest.fn();
|
|
5397
|
+
|
|
5398
|
+
await method.run();
|
|
5399
|
+
|
|
5400
|
+
expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
|
|
5401
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
5402
|
+
bootloaderBinary: remoteBinaries.bootloaderBinary,
|
|
5403
|
+
fwBinaryMap: remoteBinaries.fwBinaryMap,
|
|
5404
|
+
});
|
|
5454
5405
|
});
|
|
5455
5406
|
|
|
5456
5407
|
test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
|
|
@@ -5578,7 +5529,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5578
5529
|
|
|
5579
5530
|
method.postTipMessage = jest.fn();
|
|
5580
5531
|
method.postProgressMessage = jest.fn();
|
|
5581
|
-
(method as any).
|
|
5532
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
5533
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5534
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5535
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
5582
5536
|
(method as any).protocolV2StartFirmwareUpdate = jest.fn();
|
|
5583
5537
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
|
|
5584
5538
|
|
|
@@ -5594,7 +5548,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5594
5548
|
fwBinaryMap: [],
|
|
5595
5549
|
});
|
|
5596
5550
|
|
|
5597
|
-
expect((method as any).
|
|
5551
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
|
|
5598
5552
|
expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
|
|
5599
5553
|
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
|
|
5600
5554
|
});
|
|
@@ -5625,12 +5579,19 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5625
5579
|
});
|
|
5626
5580
|
method.postProgressMessage = jest.fn();
|
|
5627
5581
|
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
filePath: 'vol1:firmware.bin',
|
|
5631
|
-
processedSize: 0,
|
|
5632
|
-
totalSize: 4097,
|
|
5582
|
+
const source = await openFirmwareByteSource({
|
|
5583
|
+
binary: new Uint8Array(4097).buffer,
|
|
5633
5584
|
});
|
|
5585
|
+
try {
|
|
5586
|
+
await (method as any).protocolV2SourceUpdateProcess({
|
|
5587
|
+
source,
|
|
5588
|
+
filePath: 'vol1:firmware.bin',
|
|
5589
|
+
processedSize: 0,
|
|
5590
|
+
totalSize: 4097,
|
|
5591
|
+
});
|
|
5592
|
+
} finally {
|
|
5593
|
+
await source?.close();
|
|
5594
|
+
}
|
|
5634
5595
|
|
|
5635
5596
|
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
5636
5597
|
expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
|
|
@@ -5687,12 +5648,16 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5687
5648
|
method.postProgressMessage = jest.fn();
|
|
5688
5649
|
|
|
5689
5650
|
try {
|
|
5690
|
-
|
|
5691
|
-
|
|
5651
|
+
const source = await openFirmwareByteSource({
|
|
5652
|
+
binary: new Uint8Array(8001).buffer,
|
|
5653
|
+
});
|
|
5654
|
+
await (method as any).protocolV2SourceUpdateProcess({
|
|
5655
|
+
source,
|
|
5692
5656
|
filePath: 'vol0:/firmware.bin',
|
|
5693
5657
|
processedSize: 0,
|
|
5694
5658
|
totalSize: 8001,
|
|
5695
5659
|
});
|
|
5660
|
+
await source?.close();
|
|
5696
5661
|
} finally {
|
|
5697
5662
|
setTimeoutSpy.mockRestore();
|
|
5698
5663
|
}
|
|
@@ -5727,16 +5692,21 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5727
5692
|
});
|
|
5728
5693
|
method.postProgressMessage = jest.fn();
|
|
5729
5694
|
method.postTipMessage = jest.fn();
|
|
5695
|
+
(method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
|
|
5730
5696
|
|
|
5697
|
+
const source = await openFirmwareByteSource({
|
|
5698
|
+
binary: new Uint8Array(4097).buffer,
|
|
5699
|
+
});
|
|
5731
5700
|
await expect(
|
|
5732
|
-
(method as any).
|
|
5733
|
-
|
|
5701
|
+
(method as any).protocolV2SourceUpdateProcess({
|
|
5702
|
+
source,
|
|
5734
5703
|
filePath: 'vol0:/firmware.bin',
|
|
5735
5704
|
processedSize: 0,
|
|
5736
5705
|
totalSize: 4097,
|
|
5737
5706
|
})
|
|
5738
5707
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
|
|
5739
|
-
|
|
5708
|
+
await source?.close();
|
|
5709
|
+
expect(typedCall).toHaveBeenCalledTimes(6);
|
|
5740
5710
|
});
|
|
5741
5711
|
|
|
5742
5712
|
test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
|
|
@@ -5769,12 +5739,19 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5769
5739
|
});
|
|
5770
5740
|
method.postProgressMessage = jest.fn();
|
|
5771
5741
|
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
filePath: 'vol1:ble-firmware.bin',
|
|
5775
|
-
processedSize: 0,
|
|
5776
|
-
totalSize: 1801,
|
|
5742
|
+
const source = await openFirmwareByteSource({
|
|
5743
|
+
binary: new Uint8Array(1801).buffer,
|
|
5777
5744
|
});
|
|
5745
|
+
try {
|
|
5746
|
+
await (method as any).protocolV2SourceUpdateProcess({
|
|
5747
|
+
source,
|
|
5748
|
+
filePath: 'vol1:ble-firmware.bin',
|
|
5749
|
+
processedSize: 0,
|
|
5750
|
+
totalSize: 1801,
|
|
5751
|
+
});
|
|
5752
|
+
} finally {
|
|
5753
|
+
await source?.close();
|
|
5754
|
+
}
|
|
5778
5755
|
|
|
5779
5756
|
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
5780
5757
|
expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
|
|
@@ -5891,9 +5868,9 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
5891
5868
|
const typedCall = jest
|
|
5892
5869
|
.fn()
|
|
5893
5870
|
.mockRejectedValue(
|
|
5894
|
-
|
|
5895
|
-
"Failed to execute 'open' on 'USBDevice': The device was disconnected",
|
|
5896
|
-
'NotFoundError'
|
|
5871
|
+
Object.assign(
|
|
5872
|
+
new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
|
|
5873
|
+
{ name: 'NotFoundError' }
|
|
5897
5874
|
)
|
|
5898
5875
|
);
|
|
5899
5876
|
|
|
@@ -6058,63 +6035,20 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
6058
6035
|
});
|
|
6059
6036
|
|
|
6060
6037
|
describe('Protocol V2 firmware reconnect identity', () => {
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
resource.type as keyof typeof PROTOCOL_V2_RESOURCE_DEVICE_PATHS
|
|
6070
|
-
],
|
|
6071
|
-
resource,
|
|
6072
|
-
])
|
|
6073
|
-
);
|
|
6074
|
-
return jest.fn((requestType: string, _responseType: string, payload: Record<string, any>) => {
|
|
6075
|
-
if (requestType === 'ResourceInventoryGet') {
|
|
6076
|
-
throw new Error('ResourceInventoryGet is unavailable on released firmware');
|
|
6077
|
-
}
|
|
6078
|
-
if (requestType === 'FilesystemPathInfoQuery') {
|
|
6079
|
-
const resource = resourceByPath.get(payload.path);
|
|
6080
|
-
const exists = Boolean(resource && !missing.has(resource.type));
|
|
6081
|
-
return {
|
|
6082
|
-
message: {
|
|
6083
|
-
exist: exists,
|
|
6084
|
-
directory: false,
|
|
6085
|
-
size: exists ? resource?.size : 0,
|
|
6086
|
-
},
|
|
6087
|
-
};
|
|
6088
|
-
}
|
|
6089
|
-
if (requestType === 'FilesystemFileRead') {
|
|
6090
|
-
const resource = resourceByPath.get(payload.file.path);
|
|
6091
|
-
if (!resource || missing.has(resource.type)) throw new Error('missing resource');
|
|
6092
|
-
const header = new Uint8Array(0x52a0);
|
|
6093
|
-
const view = new DataView(header.buffer);
|
|
6094
|
-
'OKPP'.split('').forEach((char, index) => {
|
|
6095
|
-
header[index] = char.charCodeAt(0);
|
|
6096
|
-
});
|
|
6097
|
-
'RESC'.split('').forEach((char, index) => {
|
|
6098
|
-
header[0x08 + index] = char.charCodeAt(0);
|
|
6099
|
-
});
|
|
6100
|
-
view.setUint32(0x0c, header.byteLength, true);
|
|
6101
|
-
for (let index = 0; index < resource.headerHash.length / 2; index++) {
|
|
6102
|
-
header[0x240 + index] = Number.parseInt(
|
|
6103
|
-
resource.headerHash.slice(index * 2, index * 2 + 2),
|
|
6104
|
-
16
|
|
6105
|
-
);
|
|
6106
|
-
}
|
|
6107
|
-
const offset = Number(payload.file.offset);
|
|
6108
|
-
const chunkLength = Number(payload.chunk_len);
|
|
6109
|
-
return {
|
|
6110
|
-
message: {
|
|
6111
|
-
data: header.slice(offset, offset + chunkLength),
|
|
6112
|
-
},
|
|
6113
|
-
};
|
|
6114
|
-
}
|
|
6115
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6038
|
+
test('keeps the host plan identity as the V4 reconnect anchor', () => {
|
|
6039
|
+
const method = new FirmwareUpdateV4({
|
|
6040
|
+
id: 1,
|
|
6041
|
+
payload: {
|
|
6042
|
+
method: 'firmwareUpdateV4',
|
|
6043
|
+
platform: 'native',
|
|
6044
|
+
expectedDeviceId: 'persisted-device-id',
|
|
6045
|
+
},
|
|
6116
6046
|
});
|
|
6117
|
-
|
|
6047
|
+
|
|
6048
|
+
method.init();
|
|
6049
|
+
|
|
6050
|
+
expect((method as any).params.expectedDeviceId).toBe('persisted-device-id');
|
|
6051
|
+
});
|
|
6118
6052
|
|
|
6119
6053
|
test('rejects a different physical serial before firmware transfer resumes', () => {
|
|
6120
6054
|
expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
|
|
@@ -6125,10 +6059,16 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
6125
6059
|
).not.toThrow();
|
|
6126
6060
|
});
|
|
6127
6061
|
|
|
6128
|
-
test('
|
|
6129
|
-
expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).
|
|
6130
|
-
|
|
6131
|
-
|
|
6062
|
+
test('fails closed when the physical serial is unavailable', () => {
|
|
6063
|
+
expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).toThrow(
|
|
6064
|
+
expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
|
|
6065
|
+
);
|
|
6066
|
+
expect(() => assertProtocolV2ReconnectIdentity(undefined, 'actual-serial')).toThrow(
|
|
6067
|
+
expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
|
|
6068
|
+
);
|
|
6069
|
+
expect(() => assertProtocolV2ReconnectIdentity(undefined, undefined)).toThrow(
|
|
6070
|
+
expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
|
|
6071
|
+
);
|
|
6132
6072
|
});
|
|
6133
6073
|
|
|
6134
6074
|
test('captures physical identity from active DeviceInfo instead of wallet deviceId', async () => {
|
|
@@ -6161,7 +6101,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
6161
6101
|
);
|
|
6162
6102
|
});
|
|
6163
6103
|
|
|
6164
|
-
test('
|
|
6104
|
+
test('rejects firmware update startup when DeviceInfo has no physical serial', async () => {
|
|
6165
6105
|
const method = new FirmwareUpdateV4({
|
|
6166
6106
|
id: 1,
|
|
6167
6107
|
payload: { method: 'firmwareUpdateV4' },
|
|
@@ -6174,209 +6114,39 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
6174
6114
|
getCommands: () => ({ typedCall }),
|
|
6175
6115
|
});
|
|
6176
6116
|
|
|
6177
|
-
await expect((method as any).captureProtocolV2PhysicalIdentity()).
|
|
6178
|
-
|
|
6179
|
-
});
|
|
6180
|
-
|
|
6181
|
-
test('uses filesystem reads instead of ResourceInventoryGet for resource comparison', async () => {
|
|
6182
|
-
const method = new FirmwareUpdateV4({
|
|
6183
|
-
id: 1,
|
|
6184
|
-
payload: {
|
|
6185
|
-
method: 'firmwareUpdateV4',
|
|
6186
|
-
platform: 'web',
|
|
6187
|
-
targetsToUpdate: ['resource'],
|
|
6188
|
-
},
|
|
6117
|
+
await expect((method as any).captureProtocolV2PhysicalIdentity()).rejects.toMatchObject({
|
|
6118
|
+
errorCode: HardwareErrorCode.DeviceNotFound,
|
|
6189
6119
|
});
|
|
6190
|
-
method.init();
|
|
6191
|
-
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
6192
|
-
(type, index) => ({
|
|
6193
|
-
type,
|
|
6194
|
-
url: `https://example.com/${type}.okpkg`,
|
|
6195
|
-
size: 0x52a0 + index + 1,
|
|
6196
|
-
fileHash: 'a'.repeat(64),
|
|
6197
|
-
headerHash: index.toString(16).padStart(128, '0'),
|
|
6198
|
-
})
|
|
6199
|
-
);
|
|
6200
|
-
const typedCall = createResourceFilesystemTypedCall(stable);
|
|
6201
|
-
(method as any).device = stubDevice({
|
|
6202
|
-
getCommands: () => ({ typedCall }),
|
|
6203
|
-
});
|
|
6204
|
-
const resourcesSpy = jest
|
|
6205
|
-
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
6206
|
-
.mockReturnValue(stable as any);
|
|
6207
|
-
(method as any).downloadProtocolV2Resource = jest.fn();
|
|
6208
|
-
|
|
6209
|
-
await expect((method as any).prepareProtocolV2ResourceBundles()).resolves.toEqual([]);
|
|
6210
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
6211
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
6212
|
-
expect((method as any).downloadProtocolV2Resource).not.toHaveBeenCalled();
|
|
6213
|
-
resourcesSpy.mockRestore();
|
|
6214
6120
|
});
|
|
6215
6121
|
|
|
6216
|
-
test('
|
|
6217
|
-
const method = new FirmwareUpdateV4({
|
|
6218
|
-
id: 1,
|
|
6219
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
6220
|
-
});
|
|
6221
|
-
method.init();
|
|
6222
|
-
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
6223
|
-
(type, index) => ({
|
|
6224
|
-
type,
|
|
6225
|
-
url: `https://example.com/${type}.okpkg`,
|
|
6226
|
-
size: 0x52a0 + index + 1,
|
|
6227
|
-
fileHash: 'a'.repeat(64),
|
|
6228
|
-
headerHash: index.toString(16).padStart(128, '0'),
|
|
6229
|
-
})
|
|
6230
|
-
);
|
|
6231
|
-
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
6232
|
-
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
6233
|
-
const resourcesSpy = jest
|
|
6234
|
-
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
6235
|
-
.mockReturnValue(stable as any);
|
|
6236
|
-
(method as any).downloadProtocolV2Resource = jest.fn(resource =>
|
|
6237
|
-
Promise.resolve({
|
|
6238
|
-
name: `${resource.type}.okpkg`,
|
|
6239
|
-
binary: new ArrayBuffer(resource.size),
|
|
6240
|
-
devicePath: `vol0:/${resource.type}.okpkg`,
|
|
6241
|
-
})
|
|
6242
|
-
);
|
|
6243
|
-
|
|
6244
|
-
const bundles = await (method as any).prepareProtocolV2ResourceBundles();
|
|
6245
|
-
|
|
6246
|
-
expect(bundles).toHaveLength(1);
|
|
6247
|
-
expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
|
|
6248
|
-
resourcesSpy.mockRestore();
|
|
6249
|
-
});
|
|
6250
|
-
|
|
6251
|
-
test('uses filesystem inventory for incremental Bootloader recovery', async () => {
|
|
6252
|
-
const method = new FirmwareUpdateV4({
|
|
6253
|
-
id: 1,
|
|
6254
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
6255
|
-
});
|
|
6256
|
-
method.init();
|
|
6257
|
-
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
6258
|
-
(type, index) => ({
|
|
6259
|
-
type,
|
|
6260
|
-
url: `https://example.com/${type}.okpkg`,
|
|
6261
|
-
size: 0x52a0 + index + 1,
|
|
6262
|
-
fileHash: 'a'.repeat(64),
|
|
6263
|
-
headerHash: index.toString(16).padStart(128, '0'),
|
|
6264
|
-
})
|
|
6265
|
-
);
|
|
6266
|
-
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
6267
|
-
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
6268
|
-
const resourcesSpy = jest
|
|
6269
|
-
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
6270
|
-
.mockReturnValue(stable as any);
|
|
6271
|
-
(method as any).downloadProtocolV2Resource = jest.fn(resource =>
|
|
6272
|
-
Promise.resolve({
|
|
6273
|
-
name: `${resource.type}.okpkg`,
|
|
6274
|
-
binary: new ArrayBuffer(resource.size),
|
|
6275
|
-
devicePath: `vol0:/${resource.type}.okpkg`,
|
|
6276
|
-
})
|
|
6277
|
-
);
|
|
6278
|
-
|
|
6279
|
-
const bundles = await (method as any).prepareProtocolV2ResourceBundles();
|
|
6280
|
-
|
|
6281
|
-
expect(bundles).toHaveLength(1);
|
|
6282
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
6283
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemPathInfoQuery')).toBe(true);
|
|
6284
|
-
expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
|
|
6285
|
-
resourcesSpy.mockRestore();
|
|
6286
|
-
});
|
|
6287
|
-
|
|
6288
|
-
const buildBootResourcesHeader = ({
|
|
6289
|
-
type = 'CRAT',
|
|
6290
|
-
payloadHash = 'ab'.repeat(64),
|
|
6291
|
-
headerHash = 'cd'.repeat(64),
|
|
6292
|
-
}: {
|
|
6293
|
-
type?: string;
|
|
6294
|
-
payloadHash?: string;
|
|
6295
|
-
headerHash?: string;
|
|
6296
|
-
} = {}) => {
|
|
6297
|
-
const header = new Uint8Array(0x52a0);
|
|
6298
|
-
const view = new DataView(header.buffer);
|
|
6299
|
-
'OKPP'.split('').forEach((char, index) => {
|
|
6300
|
-
header[index] = char.charCodeAt(0);
|
|
6301
|
-
});
|
|
6302
|
-
type.split('').forEach((char, index) => {
|
|
6303
|
-
header[0x08 + index] = char.charCodeAt(0);
|
|
6304
|
-
});
|
|
6305
|
-
view.setUint32(0x0c, header.byteLength, true);
|
|
6306
|
-
const writeHex = (offset: number, hex: string) => {
|
|
6307
|
-
for (let index = 0; index < hex.length / 2; index++) {
|
|
6308
|
-
header[offset + index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
6309
|
-
}
|
|
6310
|
-
};
|
|
6311
|
-
writeHex(0x200, payloadHash);
|
|
6312
|
-
writeHex(0x240, headerHash);
|
|
6313
|
-
return header;
|
|
6314
|
-
};
|
|
6315
|
-
|
|
6316
|
-
test('does not resolve boot resources unless the optional target is selected', async () => {
|
|
6317
|
-
const method = new FirmwareUpdateV4({
|
|
6318
|
-
id: 1,
|
|
6319
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web' },
|
|
6320
|
-
});
|
|
6321
|
-
method.init();
|
|
6322
|
-
const configSpy = jest.spyOn(DataManager, 'getProtocolV2BootResources');
|
|
6323
|
-
const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
6324
|
-
|
|
6325
|
-
await expect((method as any).prepareProtocolV2BootResources()).resolves.toBeUndefined();
|
|
6326
|
-
|
|
6327
|
-
expect(configSpy).not.toHaveBeenCalled();
|
|
6328
|
-
expect(downloadSpy).not.toHaveBeenCalled();
|
|
6329
|
-
});
|
|
6330
|
-
|
|
6331
|
-
test('downloads and maps the selected boot resources CRATE target', async () => {
|
|
6332
|
-
const payloadHash = 'ab'.repeat(64);
|
|
6333
|
-
const headerHash = 'cd'.repeat(64);
|
|
6334
|
-
const bytes = buildBootResourcesHeader({ payloadHash, headerHash });
|
|
6335
|
-
const binary = bytes.buffer as ArrayBuffer;
|
|
6336
|
-
const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
6337
|
-
const resource = {
|
|
6338
|
-
required: false as const,
|
|
6339
|
-
target: 'CRATE' as const,
|
|
6340
|
-
url: 'https://example.com/boot-resources.crate.okpkg',
|
|
6341
|
-
size: bytes.byteLength,
|
|
6342
|
-
fileHash,
|
|
6343
|
-
payloadHash,
|
|
6344
|
-
headerHash,
|
|
6345
|
-
};
|
|
6122
|
+
test('uses the BLE read limit when reading an existing firmware bundle header', async () => {
|
|
6346
6123
|
const method = new FirmwareUpdateV4({
|
|
6347
6124
|
id: 1,
|
|
6348
6125
|
payload: {
|
|
6349
6126
|
method: 'firmwareUpdateV4',
|
|
6350
|
-
platform: '
|
|
6351
|
-
|
|
6127
|
+
platform: 'native',
|
|
6128
|
+
chunkSize: 1800,
|
|
6352
6129
|
},
|
|
6353
6130
|
});
|
|
6354
6131
|
method.init();
|
|
6355
|
-
|
|
6356
|
-
jest.
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6132
|
+
const readChunkLengths: number[] = [];
|
|
6133
|
+
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
6134
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
6135
|
+
return Promise.resolve({ message: { exist: true, directory: false, size: 0x52a0 } });
|
|
6136
|
+
}
|
|
6137
|
+
if (requestType === 'FilesystemFileRead') {
|
|
6138
|
+
readChunkLengths.push(request.chunk_len);
|
|
6139
|
+
return Promise.resolve({ message: { data: new Uint8Array(request.chunk_len) } });
|
|
6140
|
+
}
|
|
6141
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
6363
6142
|
});
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
test('rejects a non-CRATE manual boot resources package', async () => {
|
|
6367
|
-
const method = new FirmwareUpdateV4({
|
|
6368
|
-
id: 1,
|
|
6369
|
-
payload: {
|
|
6370
|
-
method: 'firmwareUpdateV4',
|
|
6371
|
-
platform: 'web',
|
|
6372
|
-
bootResourcesBinary: buildBootResourcesHeader({ type: 'RESC' }).buffer,
|
|
6373
|
-
},
|
|
6143
|
+
(method as any).device = stubDevice({
|
|
6144
|
+
getCommands: () => ({ typedCall }),
|
|
6374
6145
|
});
|
|
6375
|
-
method.init();
|
|
6376
6146
|
|
|
6377
|
-
await expect((method as any).
|
|
6378
|
-
|
|
6379
|
-
);
|
|
6147
|
+
await expect((method as any).readProtocolV2DeviceFileHeader('bundle.okpp')).resolves.toBeNull();
|
|
6148
|
+
expect(readChunkLengths.length).toBeGreaterThan(1);
|
|
6149
|
+
expect(Math.max(...readChunkLengths)).toBe(900);
|
|
6380
6150
|
});
|
|
6381
6151
|
});
|
|
6382
6152
|
|
|
@@ -6491,257 +6261,338 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6491
6261
|
payload: { method: 'deviceSettings', label: 'My Pro 2' },
|
|
6492
6262
|
});
|
|
6493
6263
|
|
|
6494
|
-
method.init();
|
|
6495
|
-
|
|
6496
|
-
expect(method.protocolV2UiInteraction).toEqual({
|
|
6497
|
-
request: 'button',
|
|
6498
|
-
source: 'method-lifecycle',
|
|
6499
|
-
reason: 'device-management',
|
|
6500
|
-
completion: 'operation-completed',
|
|
6501
|
-
deviceOnly: true,
|
|
6502
|
-
operation: 'change-label',
|
|
6264
|
+
method.init();
|
|
6265
|
+
|
|
6266
|
+
expect(method.protocolV2UiInteraction).toEqual({
|
|
6267
|
+
request: 'button',
|
|
6268
|
+
source: 'method-lifecycle',
|
|
6269
|
+
reason: 'device-management',
|
|
6270
|
+
completion: 'operation-completed',
|
|
6271
|
+
deviceOnly: true,
|
|
6272
|
+
operation: 'change-label',
|
|
6273
|
+
});
|
|
6274
|
+
});
|
|
6275
|
+
|
|
6276
|
+
test.each([
|
|
6277
|
+
['deviceInfoGet', DeviceInfoGet],
|
|
6278
|
+
['deviceStatusGet', DeviceStatusGet],
|
|
6279
|
+
])('does not auto-unlock the read-only %s method', (_name, MethodClass) => {
|
|
6280
|
+
const method = new MethodClass({
|
|
6281
|
+
id: 1,
|
|
6282
|
+
payload: { method: _name },
|
|
6283
|
+
} as any);
|
|
6284
|
+
method.init();
|
|
6285
|
+
|
|
6286
|
+
expect(method.unlockPolicy).toBe('none');
|
|
6287
|
+
});
|
|
6288
|
+
|
|
6289
|
+
test('unlocks and retries an opted-in Protocol V2 method once', async () => {
|
|
6290
|
+
const calls: string[] = [];
|
|
6291
|
+
const method = {
|
|
6292
|
+
name: 'testBusinessMethod',
|
|
6293
|
+
unlockPolicy: 'retry-on-locked',
|
|
6294
|
+
protocolV2UiInteraction: { reason: 'settings-page' },
|
|
6295
|
+
run: jest
|
|
6296
|
+
.fn()
|
|
6297
|
+
.mockImplementationOnce(() => {
|
|
6298
|
+
calls.push('run-1');
|
|
6299
|
+
return Promise.reject(deviceLockedError());
|
|
6300
|
+
})
|
|
6301
|
+
.mockImplementationOnce(() => {
|
|
6302
|
+
calls.push('run-2');
|
|
6303
|
+
return Promise.resolve({ message: 'ok' });
|
|
6304
|
+
}),
|
|
6305
|
+
};
|
|
6306
|
+
const device = {
|
|
6307
|
+
isProtocolV2: () => true,
|
|
6308
|
+
unlockDevice: jest.fn(() => {
|
|
6309
|
+
calls.push('unlock');
|
|
6310
|
+
return Promise.resolve();
|
|
6311
|
+
}),
|
|
6312
|
+
};
|
|
6313
|
+
const uiCoordinator = {
|
|
6314
|
+
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6315
|
+
enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
|
|
6316
|
+
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6317
|
+
};
|
|
6318
|
+
|
|
6319
|
+
await expect(
|
|
6320
|
+
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
6321
|
+
).resolves.toEqual({ message: 'ok' });
|
|
6322
|
+
expect(calls).toEqual([
|
|
6323
|
+
'method-prompt',
|
|
6324
|
+
'run-1',
|
|
6325
|
+
'unlock-prompt',
|
|
6326
|
+
'unlock',
|
|
6327
|
+
'method-prompt',
|
|
6328
|
+
'run-2',
|
|
6329
|
+
]);
|
|
6330
|
+
});
|
|
6331
|
+
|
|
6332
|
+
test('restores the expected hidden-wallet session before retrying after unlock', async () => {
|
|
6333
|
+
const calls: string[] = [];
|
|
6334
|
+
const method = {
|
|
6335
|
+
name: 'evmSignMessage',
|
|
6336
|
+
payload: { passphraseState: 'hidden-state' },
|
|
6337
|
+
useDevicePassphraseState: true,
|
|
6338
|
+
unlockPolicy: 'retry-on-locked',
|
|
6339
|
+
run: jest
|
|
6340
|
+
.fn()
|
|
6341
|
+
.mockImplementationOnce(() => {
|
|
6342
|
+
calls.push('run-1');
|
|
6343
|
+
return Promise.reject(deviceLockedError());
|
|
6344
|
+
})
|
|
6345
|
+
.mockImplementationOnce(() => {
|
|
6346
|
+
calls.push('run-2');
|
|
6347
|
+
return Promise.resolve({ message: 'ok' });
|
|
6348
|
+
}),
|
|
6349
|
+
};
|
|
6350
|
+
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
6351
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
6352
|
+
calls.push('negotiate-session');
|
|
6353
|
+
return Promise.resolve({ message: { version: 2 } });
|
|
6354
|
+
}
|
|
6355
|
+
if (requestType === 'DeviceSessionGet') {
|
|
6356
|
+
calls.push('resume-hidden-session');
|
|
6357
|
+
expect(request).toEqual({ session_id: 'hidden-session' });
|
|
6358
|
+
return Promise.resolve({
|
|
6359
|
+
message: {
|
|
6360
|
+
session_id: 'hidden-session',
|
|
6361
|
+
btc_test_address: 'hidden-state',
|
|
6362
|
+
},
|
|
6363
|
+
});
|
|
6364
|
+
}
|
|
6365
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
6366
|
+
});
|
|
6367
|
+
const device = {
|
|
6368
|
+
features: { unlocked: true, passphraseProtection: true },
|
|
6369
|
+
passphraseState: 'hidden-state',
|
|
6370
|
+
commands: { typedCall },
|
|
6371
|
+
isProtocolV2: () => true,
|
|
6372
|
+
unlockDevice: jest.fn(() => {
|
|
6373
|
+
calls.push('unlock');
|
|
6374
|
+
return Promise.resolve();
|
|
6375
|
+
}),
|
|
6376
|
+
getCurrentPassphraseProtection: () => true,
|
|
6377
|
+
getInternalState: () => 'hidden-session',
|
|
6378
|
+
clearInternalState: jest.fn(),
|
|
6379
|
+
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6380
|
+
updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
|
|
6381
|
+
};
|
|
6382
|
+
|
|
6383
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6384
|
+
message: 'ok',
|
|
6503
6385
|
});
|
|
6386
|
+
expect(calls).toEqual([
|
|
6387
|
+
'run-1',
|
|
6388
|
+
'unlock',
|
|
6389
|
+
'negotiate-session',
|
|
6390
|
+
'resume-hidden-session',
|
|
6391
|
+
'validate-hidden-session',
|
|
6392
|
+
'run-2',
|
|
6393
|
+
]);
|
|
6504
6394
|
});
|
|
6505
6395
|
|
|
6506
|
-
test
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6396
|
+
test('does not replay a business method when the hidden-wallet session cannot be restored after unlock', async () => {
|
|
6397
|
+
const initialError = deviceLockedError();
|
|
6398
|
+
const restoreError = new Error('Invalid wallet session');
|
|
6399
|
+
const method = {
|
|
6400
|
+
name: 'evmSignMessage',
|
|
6401
|
+
payload: { passphraseState: 'hidden-state' },
|
|
6402
|
+
useDevicePassphraseState: true,
|
|
6403
|
+
unlockPolicy: 'retry-on-locked',
|
|
6404
|
+
run: jest.fn().mockRejectedValueOnce(initialError),
|
|
6405
|
+
};
|
|
6406
|
+
const typedCall = jest.fn((requestType: string) => {
|
|
6407
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
6408
|
+
return Promise.resolve({ message: { version: 2 } });
|
|
6409
|
+
}
|
|
6410
|
+
if (requestType === 'DeviceSessionGet') {
|
|
6411
|
+
return Promise.reject(restoreError);
|
|
6412
|
+
}
|
|
6413
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
6414
|
+
});
|
|
6415
|
+
const device = {
|
|
6416
|
+
features: { unlocked: true, passphraseProtection: true },
|
|
6417
|
+
passphraseState: 'hidden-state',
|
|
6418
|
+
commands: { typedCall },
|
|
6419
|
+
isProtocolV2: () => true,
|
|
6420
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6421
|
+
getCurrentPassphraseProtection: () => true,
|
|
6422
|
+
getInternalState: () => 'hidden-session',
|
|
6423
|
+
clearInternalState: jest.fn(),
|
|
6424
|
+
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6425
|
+
updateInternalState: jest.fn(),
|
|
6426
|
+
};
|
|
6515
6427
|
|
|
6516
|
-
expect(method
|
|
6428
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(restoreError);
|
|
6429
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6430
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6431
|
+
expect(device.clearInternalState).toHaveBeenCalledTimes(1);
|
|
6432
|
+
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
6517
6433
|
});
|
|
6518
6434
|
|
|
6519
|
-
test('
|
|
6435
|
+
test('restores the expected hidden-wallet session after pre-unlock without selecting Attach PIN', async () => {
|
|
6520
6436
|
const calls: string[] = [];
|
|
6521
|
-
const features = { unlocked: true };
|
|
6522
6437
|
const method = {
|
|
6523
|
-
name: '
|
|
6524
|
-
|
|
6438
|
+
name: 'evmSignMessage',
|
|
6439
|
+
payload: { passphraseState: 'hidden-state' },
|
|
6525
6440
|
useDevicePassphraseState: true,
|
|
6526
|
-
|
|
6441
|
+
unlockPolicy: 'retry-on-locked',
|
|
6527
6442
|
run: jest.fn(() => {
|
|
6528
6443
|
calls.push('run');
|
|
6529
|
-
return Promise.resolve({
|
|
6444
|
+
return Promise.resolve({ message: 'ok' });
|
|
6530
6445
|
}),
|
|
6531
6446
|
};
|
|
6447
|
+
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
6448
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
6449
|
+
calls.push('negotiate-session');
|
|
6450
|
+
return Promise.resolve({ message: { version: 2 } });
|
|
6451
|
+
}
|
|
6452
|
+
if (requestType === 'DeviceSessionGet') {
|
|
6453
|
+
calls.push('resume-hidden-session');
|
|
6454
|
+
expect(request).toEqual({ session_id: 'hidden-session' });
|
|
6455
|
+
return Promise.resolve({
|
|
6456
|
+
message: {
|
|
6457
|
+
session_id: 'hidden-session',
|
|
6458
|
+
btc_test_address: 'hidden-state',
|
|
6459
|
+
},
|
|
6460
|
+
});
|
|
6461
|
+
}
|
|
6462
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
6463
|
+
});
|
|
6464
|
+
const features = {
|
|
6465
|
+
unlocked: false,
|
|
6466
|
+
unlockedAttachPin: true,
|
|
6467
|
+
passphraseProtection: true,
|
|
6468
|
+
};
|
|
6532
6469
|
const device = {
|
|
6533
6470
|
features,
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
calls.push('status');
|
|
6537
|
-
return Promise.resolve({ message: { unlocked: true } });
|
|
6538
|
-
}),
|
|
6539
|
-
},
|
|
6471
|
+
passphraseState: 'hidden-state',
|
|
6472
|
+
commands: { typedCall },
|
|
6540
6473
|
isProtocolV2: () => true,
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
features.
|
|
6545
|
-
return
|
|
6474
|
+
unlockDevice: jest.fn(() => {
|
|
6475
|
+
calls.push('unlock-main');
|
|
6476
|
+
features.unlocked = true;
|
|
6477
|
+
features.unlockedAttachPin = false;
|
|
6478
|
+
return Promise.resolve();
|
|
6546
6479
|
}),
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6480
|
+
getCurrentPassphraseProtection: () => true,
|
|
6481
|
+
getInternalState: () => 'hidden-session',
|
|
6482
|
+
clearInternalState: jest.fn(),
|
|
6483
|
+
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6484
|
+
updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
|
|
6552
6485
|
};
|
|
6553
6486
|
|
|
6554
|
-
await expect(
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
).
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6487
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6488
|
+
message: 'ok',
|
|
6489
|
+
});
|
|
6490
|
+
expect(device.unlockDevice).toHaveBeenCalledWith();
|
|
6491
|
+
expect(calls).toEqual([
|
|
6492
|
+
'unlock-main',
|
|
6493
|
+
'negotiate-session',
|
|
6494
|
+
'resume-hidden-session',
|
|
6495
|
+
'validate-hidden-session',
|
|
6496
|
+
'run',
|
|
6497
|
+
]);
|
|
6564
6498
|
});
|
|
6565
6499
|
|
|
6566
|
-
test('
|
|
6567
|
-
const calls: string[] = [];
|
|
6568
|
-
const identityError = new Error('Unexpected device');
|
|
6500
|
+
test('does not restore a hidden-wallet session for a standard-wallet pre-unlock', async () => {
|
|
6569
6501
|
const method = {
|
|
6570
|
-
name: '
|
|
6571
|
-
|
|
6502
|
+
name: 'evmGetAddress',
|
|
6503
|
+
payload: { useEmptyPassphrase: true },
|
|
6572
6504
|
useDevicePassphraseState: true,
|
|
6573
|
-
|
|
6505
|
+
unlockPolicy: 'retry-on-locked',
|
|
6506
|
+
run: jest.fn().mockResolvedValue({ address: 'standard-wallet-address' }),
|
|
6574
6507
|
};
|
|
6575
|
-
const features = { unlocked: false };
|
|
6576
6508
|
const device = {
|
|
6577
|
-
features,
|
|
6578
|
-
|
|
6579
|
-
typedCall: jest.fn(() => {
|
|
6580
|
-
calls.push('status');
|
|
6581
|
-
return Promise.resolve({ message: { unlocked: false } });
|
|
6582
|
-
}),
|
|
6583
|
-
},
|
|
6509
|
+
features: { unlocked: false },
|
|
6510
|
+
passphraseState: 'stale-hidden-state',
|
|
6584
6511
|
isProtocolV2: () => true,
|
|
6585
|
-
|
|
6586
|
-
isRomloader: () => false,
|
|
6587
|
-
updateProtocolV2Status: jest.fn(() => features),
|
|
6588
|
-
unlockDevice: jest.fn(),
|
|
6512
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6589
6513
|
};
|
|
6590
6514
|
|
|
6591
|
-
await expect(
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
},
|
|
6597
|
-
})
|
|
6598
|
-
).rejects.toBe(identityError);
|
|
6599
|
-
|
|
6600
|
-
expect(calls).toEqual(['status', 'identity']);
|
|
6601
|
-
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6602
|
-
expect(method.run).not.toHaveBeenCalled();
|
|
6515
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6516
|
+
address: 'standard-wallet-address',
|
|
6517
|
+
});
|
|
6518
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6519
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6603
6520
|
});
|
|
6604
6521
|
|
|
6605
|
-
test('unlocks before
|
|
6522
|
+
test('refreshes status and unlocks before a protected Protocol V2 method', async () => {
|
|
6606
6523
|
const calls: string[] = [];
|
|
6607
|
-
const features = { unlocked: true };
|
|
6608
6524
|
const method = {
|
|
6609
|
-
name: '
|
|
6525
|
+
name: 'deviceSettingsPageShow',
|
|
6610
6526
|
unlockPolicy: 'unlock-before-run',
|
|
6611
|
-
protocolV2UiInteraction: { reason: '
|
|
6527
|
+
protocolV2UiInteraction: { reason: 'settings-page' },
|
|
6612
6528
|
run: jest.fn(() => {
|
|
6613
6529
|
calls.push('run');
|
|
6614
|
-
return Promise.resolve({
|
|
6530
|
+
return Promise.resolve({ message: 'ok' });
|
|
6615
6531
|
}),
|
|
6616
6532
|
};
|
|
6533
|
+
const features = { unlocked: true };
|
|
6617
6534
|
const device = {
|
|
6618
6535
|
features,
|
|
6619
6536
|
commands: {
|
|
6620
6537
|
typedCall: jest.fn(() => {
|
|
6621
|
-
calls.push('status');
|
|
6538
|
+
calls.push('refresh-status');
|
|
6622
6539
|
return Promise.resolve({ message: { unlocked: false } });
|
|
6623
6540
|
}),
|
|
6624
6541
|
},
|
|
6625
6542
|
isProtocolV2: () => true,
|
|
6626
|
-
isBootloader: () => false,
|
|
6627
|
-
isRomloader: () => false,
|
|
6628
6543
|
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6629
|
-
features.unlocked = status.unlocked
|
|
6544
|
+
features.unlocked = status.unlocked ?? features.unlocked;
|
|
6630
6545
|
return features;
|
|
6631
6546
|
}),
|
|
6632
6547
|
unlockDevice: jest.fn(() => {
|
|
6633
6548
|
calls.push('unlock');
|
|
6634
6549
|
features.unlocked = true;
|
|
6635
|
-
return Promise.resolve(
|
|
6550
|
+
return Promise.resolve();
|
|
6636
6551
|
}),
|
|
6637
6552
|
};
|
|
6638
6553
|
const uiCoordinator = {
|
|
6639
6554
|
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6640
|
-
enterUnlockInteraction: jest.fn(() =>
|
|
6641
|
-
|
|
6642
|
-
return undefined;
|
|
6643
|
-
}),
|
|
6555
|
+
enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
|
|
6556
|
+
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6644
6557
|
};
|
|
6645
6558
|
|
|
6646
6559
|
await expect(
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
return Promise.resolve();
|
|
6652
|
-
},
|
|
6653
|
-
})
|
|
6654
|
-
).resolves.toEqual({ signature: 'signed' });
|
|
6655
|
-
|
|
6656
|
-
expect(calls).toEqual([
|
|
6657
|
-
'status',
|
|
6658
|
-
'unlock-prompt',
|
|
6659
|
-
'unlock',
|
|
6660
|
-
'wallet-session',
|
|
6661
|
-
'method-prompt',
|
|
6662
|
-
'run',
|
|
6663
|
-
]);
|
|
6664
|
-
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6560
|
+
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
6561
|
+
).resolves.toEqual({ message: 'ok' });
|
|
6562
|
+
expect(calls).toEqual(['refresh-status', 'unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
6563
|
+
expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
6665
6564
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6565
|
+
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
|
|
6566
|
+
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6666
6567
|
});
|
|
6667
6568
|
|
|
6668
|
-
test('
|
|
6669
|
-
const
|
|
6670
|
-
name: 'evmGetAddress',
|
|
6671
|
-
unlockPolicy: 'unlock-before-run',
|
|
6672
|
-
run: jest.fn(),
|
|
6673
|
-
};
|
|
6674
|
-
const device = {
|
|
6675
|
-
features: { unlocked: undefined },
|
|
6676
|
-
commands: { typedCall: jest.fn().mockResolvedValue({ message: {} }) },
|
|
6677
|
-
isProtocolV2: () => true,
|
|
6678
|
-
isBootloader: () => false,
|
|
6679
|
-
isRomloader: () => false,
|
|
6680
|
-
updateProtocolV2Status: jest.fn(() => ({ unlocked: undefined })),
|
|
6681
|
-
unlockDevice: jest.fn(),
|
|
6682
|
-
};
|
|
6683
|
-
|
|
6684
|
-
await expect(runMethodWithUnlockPolicy(method as any, device as any)).rejects.toMatchObject({
|
|
6685
|
-
errorCode: HardwareErrorCode.RuntimeError,
|
|
6686
|
-
});
|
|
6687
|
-
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6688
|
-
expect(method.run).not.toHaveBeenCalled();
|
|
6689
|
-
});
|
|
6690
|
-
|
|
6691
|
-
test('returns a business DeviceLocked error without unlocking or replaying', async () => {
|
|
6692
|
-
const error = deviceLockedError();
|
|
6569
|
+
test('uses refreshed unlocked status instead of a stale locked cache', async () => {
|
|
6570
|
+
const features = { unlocked: false };
|
|
6693
6571
|
const method = {
|
|
6694
|
-
name: '
|
|
6572
|
+
name: 'deviceSettings',
|
|
6695
6573
|
unlockPolicy: 'unlock-before-run',
|
|
6696
|
-
run: jest.fn().
|
|
6574
|
+
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6697
6575
|
};
|
|
6698
|
-
const features = { unlocked: true };
|
|
6699
6576
|
const device = {
|
|
6700
6577
|
features,
|
|
6701
|
-
commands: {
|
|
6578
|
+
commands: {
|
|
6579
|
+
typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
|
|
6580
|
+
},
|
|
6702
6581
|
isProtocolV2: () => true,
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6582
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6583
|
+
features.unlocked = status.unlocked ?? features.unlocked;
|
|
6584
|
+
return features;
|
|
6585
|
+
}),
|
|
6706
6586
|
unlockDevice: jest.fn(),
|
|
6707
6587
|
};
|
|
6708
6588
|
|
|
6709
|
-
await expect(
|
|
6589
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6590
|
+
message: 'ok',
|
|
6591
|
+
});
|
|
6710
6592
|
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6711
6593
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6712
6594
|
});
|
|
6713
6595
|
|
|
6714
|
-
test('reuses one lightweight preflight context across nested protected methods', async () => {
|
|
6715
|
-
const features = { unlocked: true };
|
|
6716
|
-
const context = createProtocolV2UnlockContext();
|
|
6717
|
-
const firstMethod = {
|
|
6718
|
-
name: 'allNetworkGetAddress',
|
|
6719
|
-
unlockPolicy: 'unlock-before-run',
|
|
6720
|
-
run: jest.fn().mockResolvedValue({ root: true }),
|
|
6721
|
-
};
|
|
6722
|
-
const nestedMethod = {
|
|
6723
|
-
name: 'evmGetAddress',
|
|
6724
|
-
unlockPolicy: 'unlock-before-run',
|
|
6725
|
-
run: jest.fn().mockResolvedValue({ address: '0x1' }),
|
|
6726
|
-
};
|
|
6727
|
-
const device = {
|
|
6728
|
-
features,
|
|
6729
|
-
commands: { typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }) },
|
|
6730
|
-
isProtocolV2: () => true,
|
|
6731
|
-
isBootloader: () => false,
|
|
6732
|
-
isRomloader: () => false,
|
|
6733
|
-
updateProtocolV2Status: jest.fn(() => features),
|
|
6734
|
-
unlockDevice: jest.fn(),
|
|
6735
|
-
};
|
|
6736
|
-
|
|
6737
|
-
await runMethodWithUnlockPolicy(firstMethod as any, device as any, { context });
|
|
6738
|
-
await runMethodWithUnlockPolicy(nestedMethod as any, device as any, { context });
|
|
6739
|
-
|
|
6740
|
-
expect(device.commands.typedCall).toHaveBeenCalledTimes(1);
|
|
6741
|
-
expect(firstMethod.run).toHaveBeenCalledTimes(1);
|
|
6742
|
-
expect(nestedMethod.run).toHaveBeenCalledTimes(1);
|
|
6743
|
-
});
|
|
6744
|
-
|
|
6745
6596
|
test.each(['bootloader', 'romloader'] as const)(
|
|
6746
6597
|
'skips DeviceStatus and unlock in Protocol V2 %s mode',
|
|
6747
6598
|
async mode => {
|
|
@@ -6759,7 +6610,7 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6759
6610
|
unlockDevice: jest.fn(),
|
|
6760
6611
|
};
|
|
6761
6612
|
|
|
6762
|
-
await expect(
|
|
6613
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
6763
6614
|
message: 'updating',
|
|
6764
6615
|
});
|
|
6765
6616
|
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
@@ -6768,30 +6619,157 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6768
6619
|
}
|
|
6769
6620
|
);
|
|
6770
6621
|
|
|
6771
|
-
test('
|
|
6622
|
+
test('infers a signing interaction before running a Protocol V2 business method', async () => {
|
|
6623
|
+
const method = {
|
|
6624
|
+
name: 'evmSignMessage',
|
|
6625
|
+
params: { message: 'not-exposed-in-event' },
|
|
6626
|
+
payload: {},
|
|
6627
|
+
unlockPolicy: 'retry-on-locked',
|
|
6628
|
+
run: jest.fn().mockResolvedValue({ signature: 'test' }),
|
|
6629
|
+
};
|
|
6630
|
+
const device = {
|
|
6631
|
+
isProtocolV2: () => true,
|
|
6632
|
+
unlockDevice: jest.fn(),
|
|
6633
|
+
};
|
|
6634
|
+
const uiCoordinator = {
|
|
6635
|
+
enterMethodInteraction: jest.fn(),
|
|
6636
|
+
enterUnlockInteraction: jest.fn(),
|
|
6637
|
+
resumeMethodInteraction: jest.fn(),
|
|
6638
|
+
};
|
|
6639
|
+
|
|
6640
|
+
await runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any);
|
|
6641
|
+
|
|
6642
|
+
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledWith({
|
|
6643
|
+
request: 'button',
|
|
6644
|
+
source: 'method-lifecycle',
|
|
6645
|
+
reason: 'signing-confirmation',
|
|
6646
|
+
completion: 'operation-completed',
|
|
6647
|
+
deviceOnly: true,
|
|
6648
|
+
operation: 'evmSignMessage',
|
|
6649
|
+
});
|
|
6650
|
+
});
|
|
6651
|
+
|
|
6652
|
+
test('does not unlock a Protocol V1 device or a method without the policy', async () => {
|
|
6772
6653
|
for (const [isProtocolV2, unlockPolicy] of [
|
|
6773
|
-
[false, '
|
|
6654
|
+
[false, 'retry-on-locked'],
|
|
6774
6655
|
[true, 'none'],
|
|
6775
6656
|
] as const) {
|
|
6657
|
+
const error = deviceLockedError();
|
|
6776
6658
|
const method = {
|
|
6777
6659
|
unlockPolicy,
|
|
6778
|
-
|
|
6779
|
-
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6660
|
+
run: jest.fn().mockRejectedValue(error),
|
|
6780
6661
|
};
|
|
6781
6662
|
const device = {
|
|
6782
|
-
commands: { typedCall: jest.fn() },
|
|
6783
6663
|
isProtocolV2: () => isProtocolV2,
|
|
6784
6664
|
unlockDevice: jest.fn(),
|
|
6785
6665
|
};
|
|
6786
6666
|
|
|
6787
|
-
await expect(
|
|
6788
|
-
message: 'ok',
|
|
6789
|
-
});
|
|
6790
|
-
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
6667
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
|
|
6791
6668
|
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6792
6669
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6793
6670
|
}
|
|
6794
6671
|
});
|
|
6672
|
+
|
|
6673
|
+
test('runs lock-free eventless methods without unlocking or synthesized UI', async () => {
|
|
6674
|
+
const method = {
|
|
6675
|
+
name: 'uploadPortfolio',
|
|
6676
|
+
unlockPolicy: 'none',
|
|
6677
|
+
protocolV2UiMode: 'none',
|
|
6678
|
+
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6679
|
+
};
|
|
6680
|
+
const device = {
|
|
6681
|
+
features: { unlocked: false },
|
|
6682
|
+
isProtocolV2: () => true,
|
|
6683
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6684
|
+
};
|
|
6685
|
+
const uiCoordinator = {
|
|
6686
|
+
enterMethodInteraction: jest.fn(),
|
|
6687
|
+
enterUnlockInteraction: jest.fn(),
|
|
6688
|
+
resumeMethodInteraction: jest.fn(),
|
|
6689
|
+
};
|
|
6690
|
+
|
|
6691
|
+
await expect(
|
|
6692
|
+
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
6693
|
+
).resolves.toEqual({ message: 'ok' });
|
|
6694
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6695
|
+
expect(uiCoordinator.enterMethodInteraction).not.toHaveBeenCalled();
|
|
6696
|
+
expect(uiCoordinator.enterUnlockInteraction).not.toHaveBeenCalled();
|
|
6697
|
+
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6698
|
+
});
|
|
6699
|
+
|
|
6700
|
+
test('does not unlock or replay a lock-free state-changing method after a locked response', async () => {
|
|
6701
|
+
const error = deviceLockedError();
|
|
6702
|
+
const method = {
|
|
6703
|
+
name: 'uploadPortfolio',
|
|
6704
|
+
unlockPolicy: 'none',
|
|
6705
|
+
run: jest.fn().mockRejectedValue(error),
|
|
6706
|
+
};
|
|
6707
|
+
const device = {
|
|
6708
|
+
features: { unlocked: true },
|
|
6709
|
+
isProtocolV2: () => true,
|
|
6710
|
+
unlockDevice: jest.fn(),
|
|
6711
|
+
};
|
|
6712
|
+
|
|
6713
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
|
|
6714
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6715
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6716
|
+
});
|
|
6717
|
+
|
|
6718
|
+
test('does not retry when unlock fails or when the retry is still locked', async () => {
|
|
6719
|
+
const initialError = deviceLockedError();
|
|
6720
|
+
const unlockError = new Error('PIN cancelled');
|
|
6721
|
+
const unlockFailMethod = {
|
|
6722
|
+
unlockPolicy: 'retry-on-locked',
|
|
6723
|
+
run: jest.fn().mockRejectedValue(initialError),
|
|
6724
|
+
};
|
|
6725
|
+
const unlockFailDevice = {
|
|
6726
|
+
isProtocolV2: () => true,
|
|
6727
|
+
unlockDevice: jest.fn().mockRejectedValue(unlockError),
|
|
6728
|
+
};
|
|
6729
|
+
const unlockFailCoordinator = {
|
|
6730
|
+
enterMethodInteraction: jest.fn(),
|
|
6731
|
+
enterUnlockInteraction: jest.fn(),
|
|
6732
|
+
resumeMethodInteraction: jest.fn(),
|
|
6733
|
+
};
|
|
6734
|
+
|
|
6735
|
+
await expect(
|
|
6736
|
+
runMethodWithUnlockRetry(
|
|
6737
|
+
unlockFailMethod as any,
|
|
6738
|
+
unlockFailDevice as any,
|
|
6739
|
+
unlockFailCoordinator as any
|
|
6740
|
+
)
|
|
6741
|
+
).rejects.toBe(unlockError);
|
|
6742
|
+
expect(unlockFailMethod.run).toHaveBeenCalledTimes(1);
|
|
6743
|
+
expect(unlockFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
6744
|
+
expect(unlockFailCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6745
|
+
|
|
6746
|
+
const retryError = deviceLockedError();
|
|
6747
|
+
const retryFailMethod = {
|
|
6748
|
+
unlockPolicy: 'retry-on-locked',
|
|
6749
|
+
run: jest.fn().mockRejectedValueOnce(initialError).mockRejectedValueOnce(retryError),
|
|
6750
|
+
};
|
|
6751
|
+
const retryFailDevice = {
|
|
6752
|
+
isProtocolV2: () => true,
|
|
6753
|
+
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6754
|
+
};
|
|
6755
|
+
const retryFailCoordinator = {
|
|
6756
|
+
enterMethodInteraction: jest.fn(),
|
|
6757
|
+
enterUnlockInteraction: jest.fn(),
|
|
6758
|
+
resumeMethodInteraction: jest.fn(),
|
|
6759
|
+
};
|
|
6760
|
+
|
|
6761
|
+
await expect(
|
|
6762
|
+
runMethodWithUnlockRetry(
|
|
6763
|
+
retryFailMethod as any,
|
|
6764
|
+
retryFailDevice as any,
|
|
6765
|
+
retryFailCoordinator as any
|
|
6766
|
+
)
|
|
6767
|
+
).rejects.toBe(retryError);
|
|
6768
|
+
expect(retryFailMethod.run).toHaveBeenCalledTimes(2);
|
|
6769
|
+
expect(retryFailDevice.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6770
|
+
expect(retryFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
6771
|
+
expect(retryFailCoordinator.resumeMethodInteraction).toHaveBeenCalledTimes(1);
|
|
6772
|
+
});
|
|
6795
6773
|
});
|
|
6796
6774
|
|
|
6797
6775
|
describe('Protocol V2 current low-level methods', () => {
|
|
@@ -6923,9 +6901,7 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
6923
6901
|
|
|
6924
6902
|
await method.run();
|
|
6925
6903
|
|
|
6926
|
-
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6927
|
-
eventless_wallet_session: true,
|
|
6928
|
-
});
|
|
6904
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {});
|
|
6929
6905
|
});
|
|
6930
6906
|
|
|
6931
6907
|
test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
|