Skip to content

Commit

Permalink
feat(llc): getOnboardingStatePolling logic
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandremgo committed Jun 9, 2022
1 parent 8d40a79 commit d2a429d
Show file tree
Hide file tree
Showing 3 changed files with 414 additions and 3 deletions.
6 changes: 3 additions & 3 deletions apps/cli/src/commands/synchronousOnboarding.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
onboardingStatePolling,
getOnboardingStatePolling,
OnboardingStatePollingResult,
} from "@ledgerhq/live-common/lib/onboarding/hooks/useOnboardingStatePolling";
} from "@ledgerhq/live-common/lib/hw/getOnboardingStatePolling";
import { Observable } from "rxjs";
import { deviceOpt } from "../scan";

Expand All @@ -23,7 +23,7 @@ export default {
device: string;
pollingPeriodMs: number;
}>): Observable<OnboardingStatePollingResult | null> =>
onboardingStatePolling({
getOnboardingStatePolling({
deviceId: device ?? "",
pollingPeriodMs: pollingPeriodMs ?? 1000,
}),
Expand Down
225 changes: 225 additions & 0 deletions libs/ledger-live-common/src/hw/getOnboardingStatePolling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { getOnboardingStatePolling } from "./getOnboardingStatePolling";
import { from, Subscription, TimeoutError } from "rxjs";
import * as rxjsOperators from "rxjs/operators";
import { DeviceModelId } from "@ledgerhq/devices";
import Transport from "@ledgerhq/hw-transport";
import {
DeviceOnboardingStatePollingError,
DeviceExtractOnboardingStateError,
DisconnectedDevice,
} from "@ledgerhq/errors";
import { withDevice } from "./deviceAccess";
import getVersion from "./getVersion";
import {
extractOnboardingState,
OnboardingState,
SeedPhraseType,
OnboardingStep,
} from "./extractOnboardingState";

jest.mock("./deviceAccess");
jest.mock("./getVersion");
jest.mock("./extractOnboardingState");
jest.mock("@ledgerhq/hw-transport");
jest.useFakeTimers();

const aDevice = {
deviceId: "DEVICE_ID_A",
deviceName: "DEVICE_NAME_A",
modelId: DeviceModelId.nanoFTS,
wired: false,
};

// As extractOnboardingState is mocked, the firmwareInfo
// returned by getVersion does not matter
const aFirmwareInfo = {
isBootloader: false,
rawVersion: "",
targetId: 0,
mcuVersion: "",
flags: Buffer.from([]),
};

const pollingPeriodMs = 1000;

const mockedGetVersion = jest.mocked(getVersion);
// const mockedWithDevice = withDevice as jest.Mock;
const mockedWithDevice = jest.mocked(withDevice);
mockedWithDevice.mockReturnValue((job) => from(job(new Transport())));

const mockedExtractOnboardingState = jest.mocked(extractOnboardingState);

describe("getOnboardingStatePolling", () => {
let anOnboardingState: OnboardingState;
let onboardingStatePollingSubscription: Subscription | null;

beforeEach(() => {
anOnboardingState = {
isOnboarded: false,
isInRecoveryMode: false,
seedPhraseType: SeedPhraseType.TwentyFour,
currentSeedWordIndex: 0,
currentOnboardingStep: OnboardingStep.NewDevice,
};
});

afterEach(() => {
mockedGetVersion.mockClear();
mockedExtractOnboardingState.mockClear();
jest.clearAllTimers();
onboardingStatePollingSubscription?.unsubscribe();
});

describe("When a communication error occurs while fetching the device state", () => {
describe("and when the error is allowed and thrown before the defined timeout", () => {
it("should update the onboarding state to null and keep track of the allowed error", (done) => {
mockedGetVersion.mockRejectedValue(
new DisconnectedDevice("An allowed error")
);
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);

const device = aDevice;

getOnboardingStatePolling({
deviceId: device.deviceId,
pollingPeriodMs,
}).subscribe({
next: (value) => {
expect(value.onboardingState).toBeNull();
expect(value.allowedError).toBeInstanceOf(DisconnectedDevice);
done();
},
});

jest.runOnlyPendingTimers();
});
});

describe("and when a timeout occurred before the error (or the fetch took too long)", () => {
it("should update the allowed error value to notify the consumer", (done) => {
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);

const device = aDevice;

getOnboardingStatePolling({
deviceId: device.deviceId,
pollingPeriodMs,
}).subscribe({
next: (value) => {
expect(value.onboardingState).toBeNull();
expect(value.allowedError).toBeInstanceOf(TimeoutError);
done();
},
});

// Waits more than the timeout
jest.advanceTimersByTime(pollingPeriodMs + 1);
});
});

describe("and when the error is fatal and thrown before the defined timeout", () => {
it("should notify the consumer that a unallowed error occurred", (done) => {
mockedGetVersion.mockRejectedValue(new Error("Unknown error"));

const device = aDevice;

getOnboardingStatePolling({
deviceId: device.deviceId,
pollingPeriodMs,
}).subscribe({
error: (error) => {
expect(error).toBeInstanceOf(Error);
expect(error?.message).toBe("Unknown error");
done();
},
});

jest.runOnlyPendingTimers();
});
});
});

describe("When the fetched device state is incorrect", () => {
it("should return a null onboarding state, and keep track of the extract error", (done) => {
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
mockedExtractOnboardingState.mockImplementation(() => {
throw new DeviceExtractOnboardingStateError(
"Some incorrect device info"
);
});

const device = aDevice;

onboardingStatePollingSubscription = getOnboardingStatePolling({
deviceId: device.deviceId,
pollingPeriodMs,
}).subscribe({
next: (value) => {
expect(value.onboardingState).toBeNull();
expect(value.allowedError).toBeInstanceOf(
DeviceExtractOnboardingStateError
);
done();
},
});

jest.runOnlyPendingTimers();
});
});

describe("When polling returns a correct device state", () => {
it("should return a correct onboarding state", (done) => {
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);

const device = aDevice;

onboardingStatePollingSubscription = getOnboardingStatePolling({
deviceId: device.deviceId,
pollingPeriodMs,
}).subscribe({
next: (value) => {
expect(value.onboardingState).toEqual(anOnboardingState);
expect(value.allowedError).toBeNull();
done();
},
error: (error) => {
done(error);
},
});

jest.runOnlyPendingTimers();
});

it("should poll a new onboarding state after the defined period of time", (done) => {
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);

const device = aDevice;

// Did not manage to test that the polling is repeated by using jest's fake timer
// and advanceTimersByTime method or equivalent.
// Hacky test: spy on the repeat operator to see if it has been called.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const spiedRepeat = jest.spyOn(rxjsOperators, "repeat");

onboardingStatePollingSubscription = getOnboardingStatePolling({
deviceId: device.deviceId,
pollingPeriodMs,
}).subscribe({
next: (value) => {
expect(value.onboardingState).toEqual(anOnboardingState);
expect(value.allowedError).toBeNull();
expect(spiedRepeat).toHaveBeenCalledTimes(1);
done();
},
error: (error) => {
done(error);
},
});

jest.runOnlyPendingTimers();
});
});
});
Loading

0 comments on commit d2a429d

Please sign in to comment.