Skip to content

Commit

Permalink
✨ (dmk) [NO-ISSUE]: Add new ToggleDeviceSessionRefresher method (#548)
Browse files Browse the repository at this point in the history
  • Loading branch information
valpinkman authored Dec 10, 2024
2 parents ad4c3f5 + 8f6907a commit c11b93e
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-tables-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-management-kit": minor
---

Add new toggle for the device session refresher
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/device-management-kit/src/api/DeviceManagementKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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<ToggleDeviceSessionRefresherUseCase>(
deviceSessionTypes.ToggleDeviceSessionRefresherUseCase,
)
.execute(args);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});
Original file line number Diff line number Diff line change
@@ -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),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
);
}
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type Either, Left } from "purify-ts";
import { BehaviorSubject } from "rxjs";
import { v4 as uuidv4 } from "uuid";

Expand All @@ -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;
Expand Down Expand Up @@ -105,7 +107,12 @@ export class DeviceSession {
isPolling: false,
triggersDisconnection: false,
},
) {
): Promise<Either<DmkError, ApduResponse>> {
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(
Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)) {
Expand Down Expand Up @@ -214,5 +231,6 @@ export class DeviceSessionRefresher {
return;
}
this._subscription.unsubscribe();
this._subscription = undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

0 comments on commit c11b93e

Please sign in to comment.