Skip to content

Commit

Permalink
Merge pull request #6856 from LedgerHQ/bugfix/LIVE-12411-europa-cls-s…
Browse files Browse the repository at this point in the history
…torage

fix(common/listApps): custom lock screen size for all compatible models
  • Loading branch information
ofreyssinet-ledger authored May 20, 2024
2 parents 6db18ce + be28e8d commit db77ae6
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-houses-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-common": patch
---

Fix device available storage computation for all devices supporting custom lock screens.
3 changes: 2 additions & 1 deletion libs/ledger-live-common/src/apps/listApps/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { calculateDependencies, polyfillApp, polyfillApplication } from "../poly
import { getDeviceName } from "../../device/use-cases/getDeviceNameUseCase";
import { getLatestFirmwareForDeviceUseCase } from "../../device/use-cases/getLatestFirmwareForDeviceUseCase";
import { ManagerApiRepository } from "../../device/factories/HttpManagerApiRepositoryFactory";
import { isCustomLockScreenSupported } from "../../device/use-cases/isCustomLockScreenSupported";

const appsThatKeepChangingHashes = ["Fido U2F", "Security Key"];

Expand Down Expand Up @@ -258,7 +259,7 @@ export const listApps = (
.filter(Boolean);

let customImageBlocks = 0;
if (deviceModelId === DeviceModelId.stax && !deviceInfo.isRecoveryMode) {
if (isCustomLockScreenSupported(deviceModelId) && !deviceInfo.isRecoveryMode) {
const customImageSize = await customLockScreenFetchSize(transport);
if (customImageSize) {
customImageBlocks = Math.ceil(customImageSize / bytesPerBlock);
Expand Down
97 changes: 76 additions & 21 deletions libs/ledger-live-common/src/apps/listApps/v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,26 @@ import {
ManagerApiRepository,
StubManagerApiRepository,
} from "../../device/factories/HttpManagerApiRepositoryFactory";
import { supportedDeviceModelIds as clsSupportedDeviceModelIds } from "../../device/use-cases/isCustomLockScreenSupported";
import { DeviceModel } from "@ledgerhq/devices";
import customLockScreenFetchSize from "../../hw/customLockScreenFetchSize";
import { getDeviceName } from "../../device/use-cases/getDeviceNameUseCase";
import { currenciesByMarketcap, listCryptoCurrencies } from "../../currencies";

jest.useFakeTimers();
jest.mock("../../hw/customLockScreenFetchSize");
jest.mock("../../device/use-cases/getDeviceNameUseCase");
jest.mock("../../currencies");

const mockedCustomLockScreenFetchSize = jest.mocked(customLockScreenFetchSize);
const mockedGetDeviceName = jest.mocked(getDeviceName);
const mockedListCryptoCurrencies = jest.mocked(listCryptoCurrencies);
const mockedCurrenciesByMarketCap = jest.mocked(currenciesByMarketcap);

describe("listApps v2", () => {
let mockedManagerApiRepository: ManagerApiRepository;
let listAppsCommandSpy: jest.SpyInstance;
let listInstalledAppsSpy: jest.SpyInstance;

beforeEach(() => {
jest
Expand All @@ -21,13 +36,21 @@ describe("listApps v2", () => {
"getLatestFirmwareForDeviceUseCase",
)
.mockReturnValue(Promise.resolve(null));

mockedManagerApiRepository = new StubManagerApiRepository();
mockedGetDeviceName.mockReturnValue(Promise.resolve("Mocked device name"));
mockedCurrenciesByMarketCap.mockReturnValue(Promise.resolve([]));
mockedListCryptoCurrencies.mockReturnValue([]);

listAppsCommandSpy = jest
.spyOn(jest.requireActual("../../hw/listApps"), "default")
.mockReturnValue(Promise.resolve([]));

listInstalledAppsSpy = jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
});

afterEach(() => {
jest.clearAllTimers();
jest.clearAllMocks();
jest.restoreAllMocks();
});

it("should return an observable that errors if deviceInfo.isOSU is true", done => {
Expand All @@ -45,7 +68,7 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

Expand All @@ -67,7 +90,7 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

Expand All @@ -89,19 +112,14 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

it("should call hwListApps() if deviceInfo.managerAllowed is true", done => {
const listAppsCommandSpy = jest
.spyOn(jest.requireActual("../../hw/listApps"), "default")
.mockReturnValue(Promise.resolve([]));
const listInstalledAppsSpy = jest.spyOn(ManagerAPI, "listInstalledApps");

const transport = aTransportBuilder();
const deviceInfo = aDeviceInfoBuilder({
isOSU: false,
Expand Down Expand Up @@ -131,11 +149,6 @@ describe("listApps v2", () => {
});

it("should call ManagerAPI.listInstalledApps() if deviceInfo.managerAllowed is false", () => {
const listAppsCommandSpy = jest.spyOn(jest.requireActual("../../hw/listApps"), "default");
const listInstalledAppsSpy = jest
.spyOn(ManagerAPI, "listInstalledApps")
.mockReturnValue(from([]));

const transport = aTransportBuilder();
const deviceInfo = aDeviceInfoBuilder({
isOSU: false,
Expand All @@ -157,7 +170,6 @@ describe("listApps v2", () => {
});

it("should return an observable that errors if getDeviceVersion() throws", done => {
jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
jest.spyOn(mockedManagerApiRepository, "getDeviceVersion").mockImplementation(() => {
throw new Error("getDeviceVersion failed");
});
Expand All @@ -181,15 +193,14 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

it("should return an observable that errors if catalogForDevice() throws", done => {
jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
jest.spyOn(mockedManagerApiRepository, "catalogForDevice").mockImplementation(() => {
throw new Error("catalogForDevice failed");
});
Expand All @@ -213,15 +224,14 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

it("should return an observable that errors if getLanguagePackagesForDevice() throws", done => {
jest.spyOn(ManagerAPI, "listInstalledApps").mockReturnValue(from([]));
jest
.spyOn(mockedManagerApiRepository, "getLanguagePackagesForDevice")
.mockImplementation(() => {
Expand All @@ -247,10 +257,55 @@ describe("listApps v2", () => {
done();
},
complete: () => {
fail("this observable should not complete");
done("this observable should not complete");
},
});

jest.advanceTimersByTime(1);
});

clsSupportedDeviceModelIds.forEach(deviceModelId => {
it(`should return customImageBlocks different than 0 for a ${deviceModelId} device with a custom lock screen`, done => {
const transport = aTransportBuilder({ deviceModel: { id: deviceModelId } as DeviceModel });
const deviceInfo = aDeviceInfoBuilder({
isOSU: false,
isBootloader: false,
managerAllowed: true,
targetId: 0x33200000,
});

mockedCustomLockScreenFetchSize.mockReturnValue(Promise.resolve(10));

let gotResult = false;

listApps({
transport,
deviceInfo,
managerApiRepository: mockedManagerApiRepository,
forceProvider: 1,
}).subscribe({
next: listAppsEvent => {
if (listAppsEvent.type === "result") {
gotResult = true;
const {
result: { customImageBlocks },
} = listAppsEvent;
try {
expect(customImageBlocks).not.toBe(0);
} catch (e) {
done(e);
}
}
},
complete: () => {
gotResult ? done() : done("this observable should not complete without a result");
},
error: error => {
done(error);
},
});

jest.advanceTimersByTime(1);
});
});
});
7 changes: 4 additions & 3 deletions libs/ledger-live-common/src/apps/listApps/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getLatestFirmwareForDeviceUseCase } from "../../device/use-cases/getLat
import { getProviderIdUseCase } from "../../device/use-cases/getProviderIdUseCase";
import { mapApplicationV2ToApp } from "../polyfill";
import { ManagerApiRepository } from "../../device/factories/HttpManagerApiRepositoryFactory";
import { isCustomLockScreenSupported } from "../../device/use-cases/isCustomLockScreenSupported";

// Hash discrepancies for these apps do NOT indicate a potential update,
// these apps have a mechanism that makes their hash change every time.
Expand Down Expand Up @@ -271,14 +272,14 @@ export const listApps = ({

/**
* Obtain remaining metadata:
* - Ledger Stax custom picture: number of blocks taken in storage
* - Ledger Stax/Europa custom picture: number of blocks taken in storage
* - Installed language pack
* - Device name
* */

// Stax specific, account for the size of the CLS for the storage bar.
// Stax/Europa specific, account for the size of the CLS for the storage bar.
let customImageBlocks = 0;
if (deviceModelId === DeviceModelId.stax && !deviceInfo.isRecoveryMode) {
if (isCustomLockScreenSupported(deviceModelId) && !deviceInfo.isRecoveryMode) {
const customImageSize = await customLockScreenFetchSize(transport);
if (customImageSize) {
customImageBlocks = Math.ceil(customImageSize / bytesPerBlock);
Expand Down

0 comments on commit db77ae6

Please sign in to comment.