From 8f6907a9fd99546d88520f2d167485ef59f8ca2e Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Tue, 10 Dec 2024 16:24:03 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(dmk):=20Add=20new=20ToggleDeviceSe?= =?UTF-8?q?ssionRefresher=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/metal-tables-move.md | 5 ++ .../src/api/DeviceManagementKit.test.ts | 4 + .../src/api/DeviceManagementKit.ts | 17 ++++ .../ToggleDeviceSessionRefresher.test.ts | 78 +++++++++++++++++++ .../use-case/ToggleDeviceSessionRefresher.ts | 41 ++++++++++ .../device-session/di/deviceSessionModule.ts | 7 ++ .../device-session/di/deviceSessionTypes.ts | 3 + .../device-session/model/DeviceSession.ts | 18 ++++- .../model/DeviceSessionRefresher.test.ts | 5 ++ .../model/DeviceSessionRefresher.ts | 26 ++++++- .../internal/device-session/model/Errors.ts | 10 +++ 11 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 .changeset/metal-tables-move.md create mode 100644 packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.test.ts create mode 100644 packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.ts diff --git a/.changeset/metal-tables-move.md b/.changeset/metal-tables-move.md new file mode 100644 index 000000000..dbae5d19c --- /dev/null +++ b/.changeset/metal-tables-move.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-management-kit": minor +--- + +Add new toggle for the device session refresher diff --git a/packages/device-management-kit/src/api/DeviceManagementKit.test.ts b/packages/device-management-kit/src/api/DeviceManagementKit.test.ts index 33c70a240..485e6b914 100644 --- a/packages/device-management-kit/src/api/DeviceManagementKit.test.ts +++ b/packages/device-management-kit/src/api/DeviceManagementKit.test.ts @@ -71,6 +71,10 @@ describe("DeviceManagementKit", () => { it("should have listenToConnectedDevice method", () => { expect(dmk.listenToConnectedDevice).toBeDefined(); }); + + it("should have toggleDeviceSessionRefresher method", () => { + expect(dmk.toggleDeviceSessionRefresher).toBeDefined(); + }); }); describe("stubbed", () => { diff --git a/packages/device-management-kit/src/api/DeviceManagementKit.ts b/packages/device-management-kit/src/api/DeviceManagementKit.ts index 3d74a37b7..fdab4ec3b 100644 --- a/packages/device-management-kit/src/api/DeviceManagementKit.ts +++ b/packages/device-management-kit/src/api/DeviceManagementKit.ts @@ -46,6 +46,7 @@ import { type ExecuteDeviceActionReturnType, } from "./device-action/DeviceAction"; import { deviceActionTypes } from "./device-action/di/deviceActionTypes"; +import { type ToggleDeviceSessionRefresherUseCase } from "./device-session/use-case/ToggleDeviceSessionRefresher"; import { type DmkError } from "./Error"; /** @@ -257,4 +258,20 @@ export class DeviceManagementKit { ) .execute(); } + + /** + * Toggle the device session refresher. + * + * @param {DeviceSessionId} args - The device session ID. + */ + toggleDeviceSessionRefresher(args: { + sessionId: DeviceSessionId; + enabled: boolean; + }) { + return this.container + .get( + deviceSessionTypes.ToggleDeviceSessionRefresherUseCase, + ) + .execute(args); + } } diff --git a/packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.test.ts b/packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.test.ts new file mode 100644 index 000000000..3a431d1d1 --- /dev/null +++ b/packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.test.ts @@ -0,0 +1,78 @@ +import { type LoggerPublisherService } from "@api/logger-publisher/service/LoggerPublisherService"; +import { type DeviceSession } from "@internal/device-session/model/DeviceSession"; +import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub"; +import { DeviceSessionNotFound } from "@internal/device-session/model/Errors"; +import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService"; +import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; +import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; +import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource"; +import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService"; +import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; + +import { ToggleDeviceSessionRefresherUseCase } from "./ToggleDeviceSessionRefresher"; + +let logger: LoggerPublisherService; +let sessionService: DeviceSessionService; +let useCase: ToggleDeviceSessionRefresherUseCase; +let deviceSession: DeviceSession; +let managerApi: ManagerApiService; +describe("ToggleDeviceSessionRefresherUseCase", () => { + beforeEach(() => { + logger = new DefaultLoggerPublisherService( + [], + "get-connected-device-use-case-test", + ); + sessionService = new DefaultDeviceSessionService(() => logger); + managerApi = new DefaultManagerApiService( + new AxiosManagerApiDataSource({ + managerApiUrl: "http://fake.url", + mockUrl: "http://fake-mock.url", + }), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("execute", () => { + it("should toggle the device session refresher", () => { + // given + deviceSession = deviceSessionStubBuilder( + { id: "fakeSessionId" }, + () => logger, + managerApi, + ); + sessionService.addDeviceSession(deviceSession); + useCase = new ToggleDeviceSessionRefresherUseCase( + sessionService, + () => logger, + ); + + const spy = jest.spyOn(deviceSession, "toggleRefresher"); + + // when + useCase.execute({ sessionId: "fakeSessionId", enabled: false }); + + // then + expect(spy).toHaveBeenCalledWith(false); + deviceSession.close(); + }); + + it("should throw error when deviceSession is not found", () => { + // given + useCase = new ToggleDeviceSessionRefresherUseCase( + sessionService, + () => logger, + ); + + // when + try { + useCase.execute({ sessionId: "fakeSessionId", enabled: false }); + } catch (error) { + // then + expect(error).toBeInstanceOf(DeviceSessionNotFound); + } + }); + }); +}); diff --git a/packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.ts b/packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.ts new file mode 100644 index 000000000..dbcad28d2 --- /dev/null +++ b/packages/device-management-kit/src/api/device-session/use-case/ToggleDeviceSessionRefresher.ts @@ -0,0 +1,41 @@ +import { inject, injectable } from "inversify"; + +import { type DeviceSessionId } from "@api/device-session/types"; +import { LoggerPublisherService } from "@api/logger-publisher/service/LoggerPublisherService"; +import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; +import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService"; +import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; + +export type ToggleDeviceSessionRefresherUseCaseArgs = { + sessionId: DeviceSessionId; + enabled: boolean; +}; + +/** + * Toggle the device session refresher. + */ +@injectable() +export class ToggleDeviceSessionRefresherUseCase { + private readonly _logger: LoggerPublisherService; + constructor( + @inject(deviceSessionTypes.DeviceSessionService) + private readonly _sessionService: DeviceSessionService, + @inject(loggerTypes.LoggerPublisherServiceFactory) + loggerFactory: (tag: string) => LoggerPublisherService, + ) { + this._logger = loggerFactory("ToggleDeviceSessionRefresherUseCase"); + } + + execute({ sessionId, enabled }: ToggleDeviceSessionRefresherUseCaseArgs) { + const errorOrDeviceSession = + this._sessionService.getDeviceSessionById(sessionId); + + return errorOrDeviceSession.caseOf({ + Left: (error) => { + this._logger.error("Error getting device session", { data: { error } }); + throw error; + }, + Right: (deviceSession) => deviceSession.toggleRefresher(enabled), + }); + } +} diff --git a/packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.ts b/packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.ts index ddf8e56c0..d8d3e4108 100644 --- a/packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.ts +++ b/packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.ts @@ -4,6 +4,7 @@ import { type ApduReceiverService } from "@api/device-session/service/ApduReceiv import { type ApduReceiverConstructorArgs } from "@api/device-session/service/ApduReceiverService"; import { type ApduSenderService } from "@api/device-session/service/ApduSenderService"; import { type ApduSenderServiceConstructorArgs } from "@api/device-session/service/ApduSenderService"; +import { ToggleDeviceSessionRefresherUseCase } from "@api/device-session/use-case/ToggleDeviceSessionRefresher"; import { type LoggerPublisherService } from "@api/logger-publisher/service/LoggerPublisherService"; import { DefaultApduReceiverService } from "@internal/device-session/service/DefaultApduReceiverService"; import { DefaultApduSenderService } from "@internal/device-session/service/DefaultApduSenderService"; @@ -64,9 +65,15 @@ export const deviceSessionModuleFactory = ( GetDeviceSessionStateUseCase, ); bind(deviceSessionTypes.CloseSessionsUseCase).to(CloseSessionsUseCase); + bind(deviceSessionTypes.ToggleDeviceSessionRefresherUseCase).to( + ToggleDeviceSessionRefresherUseCase, + ); if (stub) { rebind(deviceSessionTypes.GetDeviceSessionStateUseCase).to(StubUseCase); + rebind(deviceSessionTypes.ToggleDeviceSessionRefresherUseCase).to( + StubUseCase, + ); } }, ); diff --git a/packages/device-management-kit/src/internal/device-session/di/deviceSessionTypes.ts b/packages/device-management-kit/src/internal/device-session/di/deviceSessionTypes.ts index 16934e974..07481c55e 100644 --- a/packages/device-management-kit/src/internal/device-session/di/deviceSessionTypes.ts +++ b/packages/device-management-kit/src/internal/device-session/di/deviceSessionTypes.ts @@ -3,5 +3,8 @@ export const deviceSessionTypes = { ApduReceiverServiceFactory: Symbol.for("ApduReceiverServiceFactory"), DeviceSessionService: Symbol.for("DeviceSessionService"), GetDeviceSessionStateUseCase: Symbol.for("GetDeviceSessionStateUseCase"), + ToggleDeviceSessionRefresherUseCase: Symbol.for( + "ToggleDeviceSessionRefresherUseCase", + ), CloseSessionsUseCase: Symbol.for("CloseSessionsUseCase"), }; diff --git a/packages/device-management-kit/src/internal/device-session/model/DeviceSession.ts b/packages/device-management-kit/src/internal/device-session/model/DeviceSession.ts index e0a5959bc..0b9d22996 100644 --- a/packages/device-management-kit/src/internal/device-session/model/DeviceSession.ts +++ b/packages/device-management-kit/src/internal/device-session/model/DeviceSession.ts @@ -1,3 +1,4 @@ +import { type Either, Left } from "purify-ts"; import { BehaviorSubject } from "rxjs"; import { v4 as uuidv4 } from "uuid"; @@ -24,6 +25,7 @@ import { DEVICE_SESSION_REFRESH_INTERVAL } from "@internal/device-session/data/D import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService"; import { DeviceSessionRefresher } from "./DeviceSessionRefresher"; +import { DeviceBusyError } from "./Errors"; export type SessionConstructorArgs = { connectedDevice: TransportConnectedDevice; @@ -105,7 +107,12 @@ export class DeviceSession { isPolling: false, triggersDisconnection: false, }, - ) { + ): Promise> { + const sessionState = this._deviceState.getValue(); + if (sessionState.deviceStatus === DeviceStatus.BUSY) { + return Left(new DeviceBusyError()); + } + if (!options.isPolling) this.updateDeviceStatus(DeviceStatus.BUSY); const errorOrResponse = await this._connectedDevice.sendApdu( @@ -171,5 +178,14 @@ export class DeviceSession { close() { this.updateDeviceStatus(DeviceStatus.NOT_CONNECTED); this._deviceState.complete(); + this._refresher.stop(); + } + + toggleRefresher(enabled: boolean) { + if (enabled) { + this._refresher.start(); + } else { + this._refresher.stop(); + } } } diff --git a/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.test.ts b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.test.ts index e495a831d..7d417441f 100644 --- a/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.test.ts +++ b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.test.ts @@ -126,6 +126,11 @@ describe("DeviceSessionRefresher", () => { deviceSessionRefresher.stop(); expect(() => deviceSessionRefresher.stop()).not.toThrow(); }); + + it("should not throw error if start is called on a started refresher", () => { + deviceSessionRefresher.start(); + expect(() => deviceSessionRefresher.start()).not.toThrow(); + }); }); describe("With a NanoS device", () => { diff --git a/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.ts b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.ts index e53efdaf2..c3cf6a440 100644 --- a/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.ts +++ b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.ts @@ -76,7 +76,9 @@ export class DeviceSessionRefresher { private readonly _getAppAndVersionCommand = new GetAppAndVersionCommand(); private readonly _getOsVersionCommand = new GetOsVersionCommand(); private _deviceStatus: DeviceStatus; - private _subscription: Subscription; + private _subscription?: Subscription; + private readonly _refreshInterval: number; + private readonly _deviceModelId: DeviceModelId; private readonly _sendApduFn: SendApduFnType; private readonly _updateStateFn: UpdateStateFnType; @@ -94,12 +96,27 @@ export class DeviceSessionRefresher { this._logger = logger; this._sendApduFn = sendApduFn; this._updateStateFn = updateStateFn; + this._refreshInterval = refreshInterval; + this._deviceModelId = deviceModelId; + + this.start(); + } + + /** + * Start the session refresher. + * The refresher will send commands to refresh the session. + */ + start() { + if (this._subscription && !this._subscription.closed) { + this._logger.warn("Refresher already started"); + return; + } // NanoS has a specific refresher that sends GetAppAndVersion and GetOsVersion commands const refreshObservable = - deviceModelId === DeviceModelId.NANO_S - ? this._getNanoSRefreshObservable(refreshInterval * 2) - : this._getDefaultRefreshObservable(interval(refreshInterval)); + this._deviceModelId === DeviceModelId.NANO_S + ? this._getNanoSRefreshObservable(this._refreshInterval * 2) + : this._getDefaultRefreshObservable(interval(this._refreshInterval)); this._subscription = refreshObservable.subscribe((parsedResponse) => { if (!parsedResponse || !isSuccessCommandResult(parsedResponse)) { @@ -214,5 +231,6 @@ export class DeviceSessionRefresher { return; } this._subscription.unsubscribe(); + this._subscription = undefined; } } diff --git a/packages/device-management-kit/src/internal/device-session/model/Errors.ts b/packages/device-management-kit/src/internal/device-session/model/Errors.ts index d036ef84f..9a292fdc0 100644 --- a/packages/device-management-kit/src/internal/device-session/model/Errors.ts +++ b/packages/device-management-kit/src/internal/device-session/model/Errors.ts @@ -46,3 +46,13 @@ export class DeviceSessionRefresherError implements DmkError { originalError ?? new Error("Device session refresher error"); } } + +export class DeviceBusyError implements DmkError { + readonly _tag = "DeviceBusyError"; + originalError?: Error; + + constructor(originalError?: Error) { + this.originalError = + originalError ?? new Error("Device is busy, please try again later"); + } +}