fix(update): verify and apply the actual latest version

This commit is contained in:
Sucukdeluxe
2026-08-21 07:39:37 +02:00
parent 4cebc6fefd
commit 6efdca6885
10 changed files with 154 additions and 41 deletions
+31
View File
@@ -0,0 +1,31 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { resolveRuntimeAppVersion } from "../src/main/app-version";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("runtime app version", () => {
it("uses the version from the installed app package instead of a stale bundled fallback", () => {
const resourcesPath = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-version-"));
tempDirs.push(resourcesPath);
fs.mkdirSync(path.join(resourcesPath, "app.asar"));
fs.writeFileSync(path.join(resourcesPath, "app.asar", "package.json"), JSON.stringify({ version: "2.0.51" }), "utf8");
expect(resolveRuntimeAppVersion("2.0.50", resourcesPath)).toBe("2.0.51");
});
it("uses the bundled fallback when no installed package metadata is available", () => {
const resourcesPath = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-version-"));
tempDirs.push(resourcesPath);
expect(resolveRuntimeAppVersion("2.0.51", resourcesPath)).toBe("2.0.51");
});
});
+25 -2
View File
@@ -3,7 +3,8 @@ import os from "node:os";
import path from "node:path";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { createPackage } from "@electron/asar";
type ReleaseVerification = {
publish: {
@@ -38,7 +39,21 @@ const { verifyPublicRelease, verifyReleaseArchives } = await import(verifierUrl)
}
) => ArchiveVerification;
};
const fixtureRoots: string[] = [];
const fixtureRoots: string[] = [];
async function createFixtureAsar(version: string): Promise<Buffer> {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "public-release-asar-"));
const sourceDir = path.join(rootDir, "source");
const outputPath = path.join(rootDir, "app.asar");
fs.mkdirSync(path.join(sourceDir, "build", "main", "main"), { recursive: true });
fs.writeFileSync(path.join(sourceDir, "package.json"), JSON.stringify({ name: "multi-debrid-downloader", version }), "utf8");
fs.writeFileSync(path.join(sourceDir, "build", "main", "main", "main.js"), `var package_default = { name: "multi-debrid-downloader", version: "${version}" };`, "utf8");
await createPackage(sourceDir, outputPath);
const payload = fs.readFileSync(outputPath);
fs.rmSync(rootDir, { recursive: true, force: true });
return payload;
}
const validAppAsar = await createFixtureAsar("1.7.233");
const staleAppAsar = await createFixtureAsar("1.7.232");
const redistributionFiles = [
"LICENSE",
"THIRD_PARTY_NOTICES.md",
@@ -172,6 +187,7 @@ function createReleaseFixture(): string {
writeRedistributionFiles(rootDir, true);
writeFile(rootDir, "assets/app_icon.ico", "application-icon");
writeFile(rootDir, "win-unpacked/resources/assets/app_icon.ico", "application-icon");
writeFile(rootDir, "win-unpacked/resources/app.asar", validAppAsar);
writeFile(rootDir, "resources/installer.nsh", "!macro customCheckAppRunning\n${isUpdated}\nFIND_PROCESS\ntaskkill /f /im\n!macroend\n");
return rootDir;
@@ -357,6 +373,13 @@ describe("public release metadata", () => {
expect(() => verifyPublicRelease(rootDir)).toThrow(/NSIS|update|installer/i);
});
it("rejects a packaged main bundle built before the release version changed", () => {
const rootDir = createReleaseFixture();
fs.writeFileSync(path.join(rootDir, "win-unpacked", "resources", "app.asar"), staleAppAsar);
expect(() => verifyPublicRelease(rootDir)).toThrow(/main bundle|version/i);
});
it("rejects a packaged application without its window and tray icon", () => {
const rootDir = createReleaseFixture();
fs.rmSync(path.join(rootDir, "win-unpacked", "resources", "assets", "app_icon.ico"));
+11 -5
View File
@@ -1,7 +1,7 @@
import { readFileSync } from "node:fs";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult } from "../src/renderer/App";
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult, shouldOpenUpdatePrompt } from "../src/renderer/App";
import type { UpdateCheckResult } from "../src/shared/types";
import { AppHeader } from "../src/renderer/shell/AppHeader";
import { getUpdateDialogFocusTarget, UpdateExperience } from "../src/renderer/shell/UpdateExperience";
@@ -13,7 +13,13 @@ const callbacks = {
onLater: () => {}
};
describe("update experience", () => {
describe("update experience", () => {
it("keeps a dismissed update version closed until the app restarts or a newer version appears", () => {
expect(shouldOpenUpdatePrompt("startup", "v2.0.51", "v2.0.51")).toBe(false);
expect(shouldOpenUpdatePrompt("startup", "v2.0.52", "v2.0.51")).toBe(true);
expect(shouldOpenUpdatePrompt("manual", "v2.0.51", "v2.0.51")).toBe(true);
});
it("renders the available update and prompt as one accessible experience", () => {
const html = renderToStaticMarkup(
<UpdateExperience
@@ -146,15 +152,15 @@ describe("update experience", () => {
expect(css).toMatch(/\.md-update-dialog\s*\{[^}]*box-shadow:\s*0 12px 40px rgb\(0 0 0 \/ 45%\)/s);
});
it("uses a light-blue update affordance and a bounded scrollable changelog", () => {
it("uses a green update affordance and a bounded scrollable changelog", () => {
const css = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
const theme = readFileSync(new URL("../src/renderer/theme.css", import.meta.url), "utf8");
expect(theme).toMatch(/--ui-update:\s*#BAD0FC;/);
expect(theme).toMatch(/--ui-update-hover:\s*#8AA5DC;/);
expect(theme).toMatch(/--ui-update-text:\s*#181A1F;/);
expect(css).toMatch(/\.md-update-trigger\s*\{[^}]*background:\s*var\(--ui-update\);[^}]*color:\s*var\(--ui-update-text\);/s);
expect(css).toMatch(/\.md-update-trigger:hover\s*\{[^}]*background:\s*var\(--ui-update-hover\);/s);
expect(css).toMatch(/\.md-update-trigger\s*\{[^}]*background:\s*var\(--ui-success\);[^}]*color:\s*var\(--ui-update-text\);/s);
expect(css).toMatch(/\.md-update-trigger:hover\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--ui-success\) 82%, #000\);/s);
expect(css).toMatch(/\.md-update-release-notes pre\s*\{[^}]*max-height:\s*min\(360px, 45vh\);[^}]*overflow-y:\s*auto;/s);
});
+11 -2
View File
@@ -44,8 +44,17 @@ afterEach(() => {
vi.restoreAllMocks();
});
describe("update", () => {
it("normalizes update repo input", () => {
describe("update", () => {
it("always refreshes release metadata before installing instead of using the previous check result", () => {
const controller = fs.readFileSync(new URL("../src/main/app-controller.ts", import.meta.url), "utf8");
const install = controller.slice(controller.indexOf("public async installUpdate"), controller.indexOf("public addLinks"));
expect(install).toContain("installLatestUpdate(this.settings.updateRepo, undefined, onProgress)");
expect(install).not.toContain("cacheAgeMs");
expect(install).not.toContain("this.lastUpdateCheck &&");
});
it("normalizes update repo input", () => {
expect(normalizeUpdateRepo("")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
expect(normalizeUpdateRepo("owner/repo")).toBe("owner/repo");
expect(normalizeUpdateRepo("https://github.com/owner/repo")).toBe("owner/repo");