import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex } from '@noble/hashes/utils'; import DeviceUpdateBootloader from '../../src/api/device/DeviceUpdateBootloader'; import { registerFirmwareUpdateHostBinding, unregisterFirmwareUpdateHostBinding, } from '../../src/api/firmware/FirmwareHostBinding'; jest.mock('../../src/api/firmware/FirmwareUpdatePreparedPlan', () => ({ assertFirmwareUpdatePreparedPlanBinding: jest.fn(), assertFirmwareUpdatePreparedPlanDeviceIdentity: jest.fn(), clearFirmwareUpdatePreparedPlanDeviceIdentityPin: jest.fn(), getFirmwareUpdatePreparedRawArtifact: jest.fn( ({ preparedPlan }: { preparedPlan: { artifacts: unknown[] } }) => preparedPlan.artifacts[0] ), validateFirmwareUpdatePreparedPlan: jest.fn((preparedPlan: unknown) => preparedPlan), })); jest.mock('../../src/data-manager', () => ({ DataManager: { getSettings: jest.fn(), isBleConnect: jest.fn(), }, })); jest.mock('../../src/device/DevicePool', () => ({ DevicePool: {}, })); const artifactBytes = Uint8Array.from([1, 2, 3, 4]); const artifact = { artifactRef: `fw:${'a'.repeat(64)}`, size: artifactBytes.byteLength, sha256: bytesToHex(sha256(artifactBytes)), }; const createMethod = () => { const closeError = new Error('artifact reader close failed'); const close = jest.fn().mockRejectedValue(closeError); const artifactReader = { open: jest.fn().mockResolvedValue({ readerId: 'reader-1', size: artifact.size }), read: jest.fn(({ offset, length }: { offset: number; length: number }) => { const data = artifactBytes.slice(offset, offset + length).buffer; return Promise.resolve({ data, bytesRead: data.byteLength, eof: offset + length === artifactBytes.byteLength, }); }), close, }; const hostBindingGeneration = registerFirmwareUpdateHostBinding({ artifactReader, preparedPlanDigest: 'a'.repeat(64), }); const method = new DeviceUpdateBootloader({ id: 1, payload: { method: 'deviceUpdateBootloader', artifact, hostBindingGeneration, preparedPlan: { preparedPlanDigest: 'a'.repeat(64), firmwareType: EFirmwareType.Universal, artifacts: [{ artifact }], }, }, }); (method as any).device = { features: { deviceType: EDeviceType.Touch, serialNo: 'touch-device-id', }, getCurrentDeviceType: () => EDeviceType.Touch, getCurrentFirmwareType: () => EFirmwareType.Universal, }; return { close, closeError, method }; }; describe('DeviceUpdateBootloader artifact source cleanup', () => { afterEach(() => { unregisterFirmwareUpdateHostBinding(); jest.restoreAllMocks(); }); test('does not replace a successful update result when source close fails', async () => { const { close, method } = createMethod(); jest.spyOn(method, 'updateTouchBootloader').mockResolvedValue(true); await expect(method.run()).resolves.toBe(true); expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' }); }); test('does not replace the original hardware error when source close fails', async () => { const { close, method } = createMethod(); const hardwareError = ERRORS.TypedError( HardwareErrorCode.FirmwareError, 'original bootloader hardware error' ); jest.spyOn(method, 'updateTouchBootloader').mockRejectedValue(hardwareError); await expect(method.run()).rejects.toBe(hardwareError); expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' }); }); });