Skip to content

Commit

Permalink
✨ (dmk) [DSDK-355]: Support nanoS (#487)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger authored Nov 18, 2024
2 parents 71d4cba + afaeb64 commit 8e1b58f
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .changeset/purple-buses-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-management-kit": patch
---

Add support for nanoS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEVICE_SESSION_REFRESH_INTERVAL = 1000;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "@api/device-session/DeviceSessionState";
import { type DeviceSessionId } from "@api/device-session/types";
import { type DmkError } from "@api/Error";
import { DEVICE_SESSION_REFRESH_INTERVAL } from "@internal/device-session/data/DeviceSessionRefresherConst";
import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
import { type InternalConnectedDevice } from "@internal/transport/model/InternalConnectedDevice";
Expand Down Expand Up @@ -52,8 +53,9 @@ export class DeviceSession {
});
this._refresher = new DeviceSessionRefresher(
{
refreshInterval: 1000,
refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
deviceStatus: DeviceStatus.CONNECTED,
deviceModelId: this._connectedDevice.deviceModel.id,
sendApduFn: (rawApdu: Uint8Array) =>
this.sendApdu(rawApdu, {
isPolling: true,
Expand Down Expand Up @@ -96,7 +98,10 @@ export class DeviceSession {

async sendApdu(
rawApdu: Uint8Array,
options: { isPolling: boolean; triggersDisconnection: boolean } = {
options: {
isPolling: boolean;
triggersDisconnection: boolean;
} = {
isPolling: false,
triggersDisconnection: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@ import {
GetAppAndVersionCommand,
type GetAppAndVersionResponse,
} from "@api/command/os/GetAppAndVersionCommand";
import {
GetOsVersionCommand,
type GetOsVersionResponse,
} from "@api/command/os/GetOsVersionCommand";
import { DeviceModelId } from "@api/device/DeviceModel";
import { DeviceStatus } from "@api/device/DeviceStatus";
import { type ApduResponse } from "@api/device-session/ApduResponse";
import { type DeviceSessionState } from "@api/device-session/DeviceSessionState";
import { DEVICE_SESSION_REFRESH_INTERVAL } from "@internal/device-session/data/DeviceSessionRefresherConst";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";

import { DeviceSessionRefresher } from "./DeviceSessionRefresher";

const mockSendApduFn = jest.fn().mockResolvedValue(Right({} as ApduResponse));
const mockSendApduFn = jest
.fn()
.mockResolvedValue(Promise.resolve(Right({} as ApduResponse)));
const mockUpdateStateFn = jest.fn().mockImplementation(() => undefined);

jest.useFakeTimers();
Expand All @@ -31,76 +40,169 @@ describe("DeviceSessionRefresher", () => {
} as GetAppAndVersionResponse,
}),
);
jest
.spyOn(GetOsVersionCommand.prototype, "parseResponse")
.mockReturnValueOnce(
CommandResultFactory({
data: {} as GetOsVersionResponse,
}),
);
logger = new DefaultLoggerPublisherService(
[],
"DeviceSessionRefresherTest",
);
deviceSessionRefresher = new DeviceSessionRefresher(
{
refreshInterval: 1000,
deviceStatus: DeviceStatus.CONNECTED,
sendApduFn: mockSendApduFn,
updateStateFn: mockUpdateStateFn,
},
logger,
);
});

afterEach(() => {
deviceSessionRefresher.stop();
jest.clearAllMocks();
});
describe("With a modern device", () => {
beforeEach(() => {
const deviceIds = Object.values(DeviceModelId).filter(
(id) => id !== DeviceModelId.NANO_S,
);
deviceSessionRefresher = new DeviceSessionRefresher(
{
refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
deviceStatus: DeviceStatus.CONNECTED,
sendApduFn: mockSendApduFn,
updateStateFn: mockUpdateStateFn,
deviceModelId:
deviceIds[Math.floor(Math.random() * deviceIds.length)]!,
},
logger,
);
});

it("should poll by calling sendApduFn", () => {
jest.advanceTimersByTime(1000);
expect(mockSendApduFn).toHaveBeenCalledTimes(1);
afterEach(() => {
deviceSessionRefresher.stop();
jest.clearAllMocks();
});

jest.advanceTimersByTime(1000);
expect(mockSendApduFn).toHaveBeenCalledTimes(2);
});
it("should poll by calling sendApduFn 2 times", () => {
jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL * 2);
expect(mockSendApduFn).toHaveBeenCalledTimes(2);
});

it("should not poll when device is busy", () => {
deviceSessionRefresher.setDeviceStatus(DeviceStatus.BUSY);
it("should not poll when device is busy", () => {
deviceSessionRefresher.setDeviceStatus(DeviceStatus.BUSY);

jest.advanceTimersByTime(1000);
jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);

expect(mockSendApduFn).not.toHaveBeenCalled();
});
expect(mockSendApduFn).not.toHaveBeenCalled();
});

it("should not poll when device is disconnected", () => {
deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);
it("should not poll when device is disconnected", () => {
deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);

jest.advanceTimersByTime(1000);
jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);

expect(mockSendApduFn).not.toHaveBeenCalled();
});
expect(mockSendApduFn).not.toHaveBeenCalled();
});

it("should update device session state by calling updateStateFn", async () => {
jest.advanceTimersByTime(1000);
it("should update device session state by calling updateStateFn", async () => {
jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);

expect(await mockSendApduFn()).toEqual(Right({}));
expect(mockUpdateStateFn).toHaveBeenCalled();
});
await expect(mockSendApduFn()).resolves.toEqual(Right({}));
expect(mockSendApduFn).toHaveBeenCalled();
expect(mockUpdateStateFn).toHaveBeenCalled();
});

it("should not update device session state with failed polling response", async () => {
mockSendApduFn.mockResolvedValueOnce(Left("error"));
const spy = jest.spyOn(logger, "error");
it("should not update device session state with failed polling response", async () => {
mockSendApduFn.mockResolvedValueOnce(Promise.resolve(Left("error")));
const spy = jest.spyOn(logger, "error");

jest.advanceTimersByTime(1000);
await mockSendApduFn();
jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);
await mockSendApduFn();

expect(mockUpdateStateFn).not.toHaveBeenCalled();
expect(spy).toHaveBeenCalled();
});
await expect(mockUpdateStateFn).not.toHaveBeenCalled();
expect(spy).toHaveBeenCalled();
});

it("should stop the refresher when device is disconnected", () => {
const spy = jest.spyOn(deviceSessionRefresher, "stop");
deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);
expect(spy).toHaveBeenCalledTimes(1);
it("should stop the refresher when device is disconnected", () => {
const spy = jest.spyOn(deviceSessionRefresher, "stop");
deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);
expect(spy).toHaveBeenCalledTimes(1);
});

it("should not throw error if stop is called on a stopped refresher", () => {
deviceSessionRefresher.stop();
expect(() => deviceSessionRefresher.stop()).not.toThrow();
});
});

it("should not throw error if stop is called on a stopped refresher", () => {
deviceSessionRefresher.stop();
expect(() => deviceSessionRefresher.stop()).not.toThrow();
describe("With a NanoS device", () => {
afterEach(() => {
deviceSessionRefresher.stop();
jest.clearAllMocks();
});

it("should call sendApduFn 2 times and update state 1 time for a single interval", async () => {
deviceSessionRefresher = new DeviceSessionRefresher(
{
refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
deviceStatus: DeviceStatus.CONNECTED,
sendApduFn: mockSendApduFn,
updateStateFn: mockUpdateStateFn,
deviceModelId: DeviceModelId.NANO_S,
},
logger,
);
jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL * 2 + 100);

await Promise.resolve();
expect(mockSendApduFn).toHaveBeenNthCalledWith(
1,
new GetAppAndVersionCommand().getApdu().getRawApdu(),
);
await Promise.resolve();
expect(mockSendApduFn).toHaveBeenLastCalledWith(
new GetOsVersionCommand().getApdu().getRawApdu(),
);
await Promise.resolve();
expect(mockSendApduFn).toHaveBeenCalledTimes(2);
await Promise.resolve();
expect(mockUpdateStateFn).toHaveBeenCalledTimes(1);
});

it("should set device locked when get os version times out", async () => {
mockSendApduFn.mockImplementation((apdu) => {
if (
apdu.toString() ===
new GetOsVersionCommand().getApdu().getRawApdu().toString()
) {
return new Promise((resolve) =>
setTimeout(
() => resolve(Left("timeout")),
DEVICE_SESSION_REFRESH_INTERVAL * 10,
),
);
}
return Promise.resolve(Right({}));
});
mockUpdateStateFn.mockImplementation(
(getState: () => DeviceSessionState) => {
deviceSessionRefresher.setDeviceStatus(getState().deviceStatus);
},
);
deviceSessionRefresher = new DeviceSessionRefresher(
{
refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
deviceStatus: DeviceStatus.CONNECTED,
sendApduFn: mockSendApduFn,
updateStateFn: mockUpdateStateFn,
deviceModelId: DeviceModelId.NANO_S,
},
logger,
);
jest.spyOn(deviceSessionRefresher, "setDeviceStatus");
jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL * 5 + 100);
await Promise.resolve();
expect(mockSendApduFn).toHaveBeenNthCalledWith(
1,
new GetAppAndVersionCommand().getApdu().getRawApdu(),
);
await Promise.resolve();
expect(deviceSessionRefresher.setDeviceStatus).toHaveBeenCalledWith(
DeviceStatus.LOCKED,
);
});
});
});
Loading

0 comments on commit 8e1b58f

Please sign in to comment.