From 1def5d1354a3f78cf670f1b0f2c2f1834a8310c7 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Thu, 19 May 2022 15:16:39 +0300 Subject: [PATCH] Allow downgrading app versions Signed-off-by: Janne Savolainen --- .../downgrading-version-update.test.ts | 88 +++++++++++++++++++ .../update-app/installing-update.test.ts | 5 ++ .../app-version/app-version.injectable.ts | 5 +- .../setup-update-checking.injectable.ts | 25 ------ src/main/getDiForUnitTesting.ts | 5 +- .../check-for-platform-updates.injectable.ts | 6 +- .../check-for-platform-updates.test.ts | 4 +- ...pdates-starting-from-channel.injectable.ts | 7 +- .../update-can-be-downgraded.injectable.ts | 29 ++++++ .../selected-update-channel.injectable.ts | 10 ++- 10 files changed, 146 insertions(+), 38 deletions(-) create mode 100644 src/behaviours/update-app/downgrading-version-update.test.ts delete mode 100644 src/main/electron-app/runnables/setup-update-checking.injectable.ts create mode 100644 src/main/update-app/check-for-updates/update-can-be-downgraded.injectable.ts diff --git a/src/behaviours/update-app/downgrading-version-update.test.ts b/src/behaviours/update-app/downgrading-version-update.test.ts new file mode 100644 index 0000000000..0b448b5c64 --- /dev/null +++ b/src/behaviours/update-app/downgrading-version-update.test.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "../../main/update-app/publish-is-configured.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { CheckForPlatformUpdates } from "../../main/update-app/check-for-platform-updates/check-for-platform-updates.injectable"; +import checkForPlatformUpdatesInjectable from "../../main/update-app/check-for-platform-updates/check-for-platform-updates.injectable"; +import checkForUpdatesInjectable from "../../main/update-app/check-for-updates/check-for-updates.injectable"; +import selectedUpdateChannelInjectable from "../../main/update-app/selected-update-channel.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable"; +import { updateChannels } from "../../main/update-app/update-channels"; + +describe("downgrading version update", () => { + let applicationBuilder: ApplicationBuilder; + let checkForPlatformUpdatesMock: AsyncFnMock; + let mainDi: DiContainer; + + beforeEach(() => { + jest.useFakeTimers(); + + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + checkForPlatformUpdatesMock = asyncFn(); + + mainDi.override( + checkForPlatformUpdatesInjectable, + () => checkForPlatformUpdatesMock, + ); + + mainDi.override(electronUpdaterIsActiveInjectable, () => true); + mainDi.override(publishIsConfiguredInjectable, () => true); + }); + + mainDi = applicationBuilder.dis.mainDi; + }); + + [ + { + updateChannel: updateChannels.latest, + appVersion: "4.0.0-beta", + downgradeIsAllowed: true, + }, + { + updateChannel: updateChannels.beta, + appVersion: "4.0.0-beta", + downgradeIsAllowed: false, + }, + { + updateChannel: updateChannels.beta, + appVersion: "4.0.0-beta.1", + downgradeIsAllowed: false, + }, + { + updateChannel: updateChannels.alpha, + appVersion: "4.0.0-beta", + downgradeIsAllowed: true, + }, + { + updateChannel: updateChannels.alpha, + appVersion: "4.0.0-alpha", + downgradeIsAllowed: false, + }, + ].forEach(({ appVersion, updateChannel, downgradeIsAllowed }) => { + it(`given application version "${appVersion}" and update channel "${updateChannel.id}", when checking for updates, can${downgradeIsAllowed ? "": "not"} downgrade`, async () => { + mainDi.override(appVersionInjectable, () => appVersion); + + await applicationBuilder.render(); + + const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable); + + selectedUpdateChannel.setValue(updateChannel.id); + + const checkForUpdates = mainDi.inject(checkForUpdatesInjectable); + + checkForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(expect.any(Object), { allowDowngrade: downgradeIsAllowed }); + }); + }); + +}); diff --git a/src/behaviours/update-app/installing-update.test.ts b/src/behaviours/update-app/installing-update.test.ts index 8250cbb03e..deba5ea3a2 100644 --- a/src/behaviours/update-app/installing-update.test.ts +++ b/src/behaviours/update-app/installing-update.test.ts @@ -93,6 +93,7 @@ describe("installing update", () => { it('checks for updates from "latest" update channel', () => { expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( updateChannels.latest, + { allowDowngrade: true }, ); }); @@ -247,6 +248,7 @@ describe("installing update", () => { expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( updateChannels.beta, + { allowDowngrade: true }, ); }); }); @@ -272,6 +274,7 @@ describe("installing update", () => { it('checks updates from update channel "alpha"', () => { expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( updateChannels.alpha, + { allowDowngrade: true }, ); }); @@ -298,6 +301,7 @@ describe("installing update", () => { it('checks updates from update channel "beta"', () => { expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( updateChannels.beta, + { allowDowngrade: true }, ); }); @@ -324,6 +328,7 @@ describe("installing update", () => { it('finally checks updates from update channel "latest"', () => { expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( updateChannels.latest, + { allowDowngrade: true }, ); }); diff --git a/src/common/get-configuration-file-model/app-version/app-version.injectable.ts b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts index 0fe3142332..5fdfd30eba 100644 --- a/src/common/get-configuration-file-model/app-version/app-version.injectable.ts +++ b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts @@ -3,12 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import packageInfo from "../../../../package.json"; +import packageJsonInjectable from "../../vars/package-json.injectable"; const appVersionInjectable = getInjectable({ id: "app-version", - instantiate: () => packageInfo.version, - causesSideEffects: true, + instantiate: (di) => di.inject(packageJsonInjectable).version, }); export default appVersionInjectable; diff --git a/src/main/electron-app/runnables/setup-update-checking.injectable.ts b/src/main/electron-app/runnables/setup-update-checking.injectable.ts deleted file mode 100644 index 918e985265..0000000000 --- a/src/main/electron-app/runnables/setup-update-checking.injectable.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token"; -import startUpdateCheckingInjectable from "../../start-update-checking.injectable"; - -const setupUpdateCheckingInjectable = getInjectable({ - id: "setup-update-checking", - - instantiate: (di) => { - const startUpdateChecking = di.inject(startUpdateCheckingInjectable); - - return { - run: () => { - startUpdateChecking(); - }, - }; - }, - - injectionToken: afterRootFrameIsReadyInjectionToken, -}); - -export default setupUpdateCheckingInjectable; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 605b733ac2..a030473679 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -84,8 +84,8 @@ import setUpdateOnQuitInjectable from "./electron-app/features/set-update-on-qui import downloadPlatformUpdateInjectable from "./update-app/download-platform-update/download-platform-update.injectable"; import startCatalogSyncInjectable from "./catalog-sync-to-renderer/start-catalog-sync.injectable"; import startKubeConfigSyncInjectable from "./start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable"; -import startCheckingForUpdatesInjectable - from "./update-app/periodical-check-for-updates/start-checking-for-updates.injectable"; +import startCheckingForUpdatesInjectable from "./update-app/periodical-check-for-updates/start-checking-for-updates.injectable"; +import appVersionInjectable from "../common/get-configuration-file-model/app-version/app-version.injectable"; export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { const { @@ -124,6 +124,7 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { di.override(commandLineArgumentsInjectable, () => []); di.override(productNameInjectable, () => "some-product-name"); + di.override(appVersionInjectable, () => "1.0.0"); di.override(clusterFramesInjectable, () => observable.map()); diff --git a/src/main/update-app/check-for-platform-updates/check-for-platform-updates.injectable.ts b/src/main/update-app/check-for-platform-updates/check-for-platform-updates.injectable.ts index 06ed1048fd..6dffb1e670 100644 --- a/src/main/update-app/check-for-platform-updates/check-for-platform-updates.injectable.ts +++ b/src/main/update-app/check-for-platform-updates/check-for-platform-updates.injectable.ts @@ -8,7 +8,7 @@ import type { UpdateChannel } from "../update-channels"; import loggerInjectable from "../../../common/logger.injectable"; import type { UpdateCheckResult } from "electron-updater"; -export type CheckForPlatformUpdates = (updateChannel: UpdateChannel) => Promise<{ updateWasDiscovered: boolean; version?: string }>; +export type CheckForPlatformUpdates = (updateChannel: UpdateChannel, opts: { allowDowngrade: boolean }) => Promise<{ updateWasDiscovered: boolean; version?: string }>; const checkForPlatformUpdatesInjectable = getInjectable({ id: "check-for-platform-updates", @@ -17,10 +17,10 @@ const checkForPlatformUpdatesInjectable = getInjectable({ const electronUpdater = di.inject(electronUpdaterInjectable); const logger = di.inject(loggerInjectable); - return async (updateChannel) => { + return async (updateChannel, { allowDowngrade }) => { electronUpdater.channel = updateChannel.id; electronUpdater.autoDownload = false; - electronUpdater.allowDowngrade = false; + electronUpdater.allowDowngrade = allowDowngrade; let result: UpdateCheckResult; diff --git a/src/main/update-app/check-for-platform-updates/check-for-platform-updates.test.ts b/src/main/update-app/check-for-platform-updates/check-for-platform-updates.test.ts index 77aadfd07a..987361f915 100644 --- a/src/main/update-app/check-for-platform-updates/check-for-platform-updates.test.ts +++ b/src/main/update-app/check-for-platform-updates/check-for-platform-updates.test.ts @@ -52,7 +52,7 @@ describe("check-for-platform-updates", () => { moreStableUpdateChannel: null, }; - actualPromise = checkForPlatformUpdates(testUpdateChannel); + actualPromise = checkForPlatformUpdates(testUpdateChannel, { allowDowngrade: true }); }); it("sets update channel", () => { @@ -60,7 +60,7 @@ describe("check-for-platform-updates", () => { }); it("sets flag for allowing downgrade", () => { - expect(electronUpdaterFake.allowDowngrade).toBe(false); + expect(electronUpdaterFake.allowDowngrade).toBe(true); }); it("disables auto downloading for being controlled", () => { diff --git a/src/main/update-app/check-for-updates/check-for-updates-starting-from-channel.injectable.ts b/src/main/update-app/check-for-updates/check-for-updates-starting-from-channel.injectable.ts index a81f7c4947..da27d8bcf2 100644 --- a/src/main/update-app/check-for-updates/check-for-updates-starting-from-channel.injectable.ts +++ b/src/main/update-app/check-for-updates/check-for-updates-starting-from-channel.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { UpdateChannel } from "../update-channels"; import checkForPlatformUpdatesInjectable from "../check-for-platform-updates/check-for-platform-updates.injectable"; +import updateCanBeDowngradedInjectable from "./update-can-be-downgraded.injectable"; interface CheckForUpdatesFromChannelResult { updateWasDiscovered: boolean; @@ -20,10 +21,14 @@ const checkForUpdatesStartingFromChannelInjectable = getInjectable({ checkForPlatformUpdatesInjectable, ); + const updateCanBeDowngraded = di.inject(updateCanBeDowngradedInjectable); + const _recursiveCheck = async ( updateChannel: UpdateChannel, ): Promise => { - const result = await checkForPlatformUpdates(updateChannel); + const result = await checkForPlatformUpdates(updateChannel, { + allowDowngrade: updateCanBeDowngraded.get(), + }); if (result.updateWasDiscovered) { return { diff --git a/src/main/update-app/check-for-updates/update-can-be-downgraded.injectable.ts b/src/main/update-app/check-for-updates/update-can-be-downgraded.injectable.ts new file mode 100644 index 0000000000..e1102e1026 --- /dev/null +++ b/src/main/update-app/check-for-updates/update-can-be-downgraded.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import selectedUpdateChannelInjectable from "../selected-update-channel.injectable"; +import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable"; +import { SemVer } from "semver"; + +const updateCanBeDowngradedInjectable = getInjectable({ + id: "update-can-be-downgraded", + + instantiate: (di) => { + const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable); + const appVersion = di.inject(appVersionInjectable); + + return computed(() => { + const semVer = new SemVer(appVersion); + + return ( + semVer.prerelease[0] !== + selectedUpdateChannel.value.get().id + ); + }); + }, +}); + +export default updateCanBeDowngradedInjectable; diff --git a/src/main/update-app/selected-update-channel.injectable.ts b/src/main/update-app/selected-update-channel.injectable.ts index 5291baa240..a7b6a531c3 100644 --- a/src/main/update-app/selected-update-channel.injectable.ts +++ b/src/main/update-app/selected-update-channel.injectable.ts @@ -3,14 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; import { action, computed, observable } from "mobx"; -import type { UpdateChannelId } from "./update-channels"; +import type { UpdateChannel, UpdateChannelId } from "./update-channels"; import { updateChannels } from "./update-channels"; +export interface SelectedUpdateChannel { + value: IComputedValue; + setValue: (channelId: UpdateChannelId) => void; +} + const selectedUpdateChannelInjectable = getInjectable({ id: "selected-update-channel", - instantiate: () => { + instantiate: (): SelectedUpdateChannel => { const state = observable.box(updateChannels.latest); return {