release: prepare v2.0.43

Add sanitized provider and account runtime diagnostics, complete the Multi-Debrid-Downloader product rename, preserve existing application data during migration, update public release verification, and publish the accompanying English documentation and regression coverage.
This commit is contained in:
Sucukdeluxe
2026-08-16 21:50:01 +02:00
parent 7107a92ae2
commit a8a5ca4b40
36 changed files with 1117 additions and 146 deletions
+3 -2
View File
@@ -11,11 +11,12 @@ const styles = readFileSync(
);
describe("account management layout", () => {
it("keeps both account tabs inside the same fixed content row", () => {
it("keeps all account tabs inside the same fixed content row", () => {
expect(workspaceSource).toContain('<div className="settings-account-workspace">');
expect(workspaceSource.match(/className="settings-account-panel"/g)).toHaveLength(2);
expect(workspaceSource.match(/className="settings-account-panel"/g)).toHaveLength(3);
expect(workspaceSource).toContain('hidden={model.activePanel !== "overview"}');
expect(workspaceSource).toContain('hidden={model.activePanel !== "rules"}');
expect(workspaceSource).toContain('hidden={model.activePanel !== "runtime"}');
expect(styles).toMatch(
/\.settings-account-workspace\s*{[^}]*grid-template-rows:\s*auto auto minmax\(0, 1fr\);[^}]*min-width:\s*0;[^}]*min-height:\s*0;/s
);
+86
View File
@@ -0,0 +1,86 @@
import { afterEach, describe, expect, it } from "vitest";
import { createAccountRuntimeEntries } from "../src/main/account-runtime-snapshot";
import {
recordAccountRuntimeAttempt,
recordAccountRuntimeFailure,
recordAccountRuntimeSuccess,
resetAccountRuntimeSessionForProvider
} from "../src/main/account-runtime";
import { resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
import type { DownloadItem, RendererAccount } from "../src/shared/types";
afterEach(() => {
resetMegaDebridRuntimeStateForTests();
resetAccountRuntimeSessionForProvider("megadebrid-api");
resetAccountRuntimeSessionForProvider("megadebrid-web");
});
function account(provider: "megadebrid-api" | "megadebrid-web", enabled = true): RendererAccount {
return {
accountId: "shared-account",
kind: provider,
provider,
identity: "runtime-user",
maskedIdentity: "run***ser",
hasSecret: true,
enabled,
dailyLimitBytes: 0,
dailyUsageBytes: provider === "megadebrid-api" ? 1000 : 2000,
totalUsageBytes: 0,
status: null
};
}
function item(provider: "megadebrid-api" | "megadebrid-web"): DownloadItem {
return {
id: `item-${provider}`,
packageId: "package-runtime",
url: "https://hoster.example/runtime.bin",
provider,
providerAccountId: "shared-account",
status: "downloading",
retries: 0,
speedBps: 100,
downloadedBytes: 10,
totalBytes: 100,
progressPercent: 10,
fileName: "runtime.bin",
targetPath: "runtime.bin",
resumable: true,
attempts: 1,
lastError: "",
fullStatus: "Download läuft",
createdAt: 1000,
updatedAt: 2000
};
}
describe("account runtime snapshot", () => {
it("keeps API and web telemetry separate when both modes share an account id", () => {
recordAccountRuntimeAttempt("megadebrid-api", "shared-account", 1500);
recordAccountRuntimeSuccess("megadebrid-api", "shared-account", 1600);
recordAccountRuntimeAttempt("megadebrid-web", "shared-account", 1700);
recordAccountRuntimeFailure("megadebrid-web", "shared-account", 1800);
const entries = createAccountRuntimeEntries(
[account("megadebrid-api"), account("megadebrid-web")],
[item("megadebrid-api")],
3000
);
expect(entries).toEqual([
expect.objectContaining({ provider: "megadebrid-api", activeDownloads: 1, attempts: 1, successes: 1, failures: 0 }),
expect.objectContaining({ provider: "megadebrid-web", activeDownloads: 0, attempts: 1, successes: 0, failures: 1 })
]);
});
it("keeps a disabled account disabled even when an old item still references it", () => {
const [entry] = createAccountRuntimeEntries([account("megadebrid-api", false)], [item("megadebrid-api")], 3000);
expect(entry).toEqual(expect.objectContaining({
state: "disabled",
reason: "Account deaktiviert",
activeDownloads: 1
}));
});
});
+4 -4
View File
@@ -147,13 +147,13 @@ describe("browser-security", () => {
externalHosts: githubOnly
});
const allowed = harness.navigate("https://github.com/Sucukdeluxe/multi-debrid-downloader");
const allowed = harness.navigate("https://github.com/Sucukdeluxe/Multi-Debrid-Downloader");
const lookalike = harness.navigate("https://github.com.evil.example/Sucukdeluxe");
expect(allowed.preventDefault).toHaveBeenCalledTimes(1);
expect(lookalike.preventDefault).toHaveBeenCalledTimes(1);
expect(electron.openExternal).toHaveBeenCalledTimes(1);
expect(electron.openExternal).toHaveBeenCalledWith("https://github.com/Sucukdeluxe/multi-debrid-downloader");
expect(electron.openExternal).toHaveBeenCalledWith("https://github.com/Sucukdeluxe/Multi-Debrid-Downloader");
});
it("applies the main-window navigation policy to redirects", () => {
@@ -163,13 +163,13 @@ describe("browser-security", () => {
externalHosts: githubOnly
});
const allowed = harness.redirect("https://github.com/Sucukdeluxe/multi-debrid-downloader");
const allowed = harness.redirect("https://github.com/Sucukdeluxe/Multi-Debrid-Downloader");
const lookalike = harness.redirect("https://github.com.evil.example/Sucukdeluxe");
expect(allowed.preventDefault).toHaveBeenCalledTimes(1);
expect(lookalike.preventDefault).toHaveBeenCalledTimes(1);
expect(electron.openExternal).toHaveBeenCalledTimes(1);
expect(electron.openExternal).toHaveBeenCalledWith("https://github.com/Sucukdeluxe/multi-debrid-downloader");
expect(electron.openExternal).toHaveBeenCalledWith("https://github.com/Sucukdeluxe/Multi-Debrid-Downloader");
});
it("denies local-file navigation outside the packaged main renderer file", () => {
+9
View File
@@ -6,6 +6,7 @@ import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accoun
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
import { isMegaDebridTransientResolveFailure } from "../src/shared/mega-debrid-errors";
import { checkRapidgatorOnline, classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getAvailableRealDebridAccounts, getDebridLinkKeyCooldownStateForTests, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, getProviderRuntimeSnapshot, leadProviderChainWith, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, parseRapidgatorFileSize, primeMegaDebridRuntimeCooldownForTests, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests, resetRealDebridRuntimeStateForTests } from "../src/main/debrid";
import { getAccountRuntimeSessionStats } from "../src/main/account-runtime";
const originalFetch = globalThis.fetch;
@@ -2857,6 +2858,12 @@ describe("Real-Debrid account rotation", () => {
expect(result.sourceAccountId).toBe("rda_one");
expect(result.sourceAccountLabel).toBe("API-Token 1");
expect(getAccountRuntimeSessionStats("realdebrid", "rda_one")).toEqual(expect.objectContaining({
attempts: 1,
successes: 1,
failures: 0,
lastUsedAt: expect.any(Number)
}));
});
it("fails over from a rejected API account to the next account in the same call", async () => {
@@ -2878,6 +2885,8 @@ describe("Real-Debrid account rotation", () => {
expect(result.sourceAccountId).toBe("rda_two");
expect(usedTokens).toEqual(["token-one", "token-two"]);
expect(getAccountRuntimeSessionStats("realdebrid", "rda_one")).toEqual(expect.objectContaining({ attempts: 1, successes: 0, failures: 1 }));
expect(getAccountRuntimeSessionStats("realdebrid", "rda_two")).toEqual(expect.objectContaining({ attempts: 1, successes: 1, failures: 0 }));
expect(getAvailableRealDebridAccounts(settings, Date.now() + 3 * 60 * 1000).map((account) => account.id)).toEqual(["rda_two"]);
});
+75
View File
@@ -13304,6 +13304,81 @@ describe("download manager", () => {
expect(getProviderRuntimeSnapshot().realDebrid.cooldownCount).toBe(0);
});
it("exposes sanitized per-account runtime state in the renderer snapshot", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
const settings = {
...defaultSettings(),
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_one", token: "token-one" }]),
realDebridAccountDailyUsageBytes: { rda_one: 4_200_000_000 }
};
const session = emptySession();
session.items.runtime_item = {
id: "runtime_item",
packageId: "runtime_package",
url: "https://hoster.example/runtime.bin",
provider: "realdebrid",
providerAccountId: "rda_one",
status: "downloading",
retries: 0,
speedBps: 1024,
downloadedBytes: 100,
totalBytes: 1000,
progressPercent: 10,
fileName: "runtime.bin",
targetPath: path.join(root, "runtime.bin"),
resumable: true,
attempts: 1,
lastError: "",
fullStatus: "Download läuft",
createdAt: Date.now() - 1000,
updatedAt: Date.now()
};
session.packages.runtime_package = {
id: "runtime_package",
name: "runtime",
outputDir: root,
extractDir: root,
status: "downloading",
itemIds: ["runtime_item"],
cancelled: false,
enabled: true,
createdAt: Date.now() - 1000,
updatedAt: Date.now()
};
session.packageOrder = ["runtime_package"];
const manager = new DownloadManager(settings, session, createStoragePaths(path.join(root, "state")));
const activeItem = (manager as unknown as { session: typeof session }).session.items.runtime_item;
activeItem.status = "downloading";
activeItem.provider = "realdebrid";
activeItem.providerAccountId = "rda_one";
primeRealDebridRuntimeCooldownForTests("rda_one", 60_000, "HTTP 429 token-one", "rate_limit");
const runtime = (manager.getSnapshot() as unknown as {
accountRuntime: Array<{
accountId: string;
state: string;
reason: string;
cooldownUntil: number | null;
dailyUsageBytes: number;
}>;
}).accountRuntime;
expect(runtime).toEqual([
expect.objectContaining({
accountId: "rda_one",
state: "cooldown",
reason: "Rate-Limit aktiv",
cooldownUntil: expect.any(Number),
dailyUsageBytes: 4_200_000_000,
activeDownloads: 1,
lastUsedAt: expect.any(Number)
})
]);
expect(JSON.stringify(runtime)).not.toContain("token-one");
expect(JSON.stringify(runtime)).not.toContain("HTTP 429");
});
it("does not hang when rapid stop is followed by disabling the last provider", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
+4
View File
@@ -84,6 +84,10 @@ describe("renderer localization", () => {
expect(translateUiText("Seite 2 von 7", "en")).toBe("Page 2 of 7");
expect(translateUiText("Page 2 of 7", "de")).toBe("Seite 2 von 7");
expect(translateUiText("Seite 1.000 von 2.500", "en")).toBe("Page 1.000 of 2.500");
expect(translateUiText("1 von 2 verfügbar", "en")).toBe("1 of 2 available");
expect(translateUiText("vor 2 Min.", "en")).toBe("2 min ago");
expect(translateUiText("Rate-Limit aktiv · 48 Sek.", "en")).toBe("Rate limit active · 48 sec");
expect(translateUiText("Rate limit active · 48 sec", "de")).toBe("Rate-Limit aktiv · 48 Sek.");
expect(translateUiText("Accountdaten werden aktualisiert.", "en")).toBe("Account data is being updated.");
expect(translateUiText("Keine passenden Dienste oder Zugangstypen gefunden.", "en"))
.toBe("No matching services or access types found.");
+68
View File
@@ -0,0 +1,68 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import * as storage from "../src/main/storage";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("Multi-Debrid-Downloader product metadata", () => {
it("uses the canonical public product and repository names while preserving the installer identity", () => {
const packageJson = JSON.parse(fs.readFileSync(path.resolve("package.json"), "utf8"));
const verifier = fs.readFileSync(path.resolve("scripts", "verify_public_release.mjs"), "utf8");
const readme = fs.readFileSync(path.resolve("README.md"), "utf8");
const supportBundle = fs.readFileSync(path.resolve("src", "main", "support-bundle.ts"), "utf8");
expect(packageJson.name).toBe("multi-debrid-downloader");
expect(packageJson.build.productName).toBe("Multi-Debrid-Downloader");
expect(packageJson.build.appId).toBe("com.sucukdeluxe.realdebrid");
expect(packageJson.build.publish.repo).toBe("Multi-Debrid-Downloader");
expect(verifier).toContain('repo: "Multi-Debrid-Downloader"');
expect(verifier).toContain('EXPECTED_PRODUCT_NAME = "Multi-Debrid-Downloader"');
expect(readme).toContain("Sucukdeluxe/Multi-Debrid-Downloader/releases/latest");
expect(readme).toContain("Multi-Debrid-Downloader-Setup-<version>.exe");
expect(readme).not.toContain("Real-Debrid-Downloader-Setup-");
expect(supportBundle).toContain("mdd-support-bundle-");
});
it("moves the legacy user data directory without losing runtime state", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-user-data-"));
tempDirs.push(root);
const legacy = path.join(root, "Real-Debrid-Downloader");
const target = path.join(root, "Multi-Debrid-Downloader");
fs.mkdirSync(path.join(legacy, "runtime"), { recursive: true });
fs.writeFileSync(path.join(legacy, "runtime", "settings.json"), "saved-state", "utf8");
const migrate = (storage as unknown as Record<string, (rootPath: string) => string>).migrateProductUserDataDirectory;
expect(typeof migrate).toBe("function");
expect(migrate(root)).toBe(target);
expect(fs.existsSync(legacy)).toBe(false);
expect(fs.readFileSync(path.join(target, "runtime", "settings.json"), "utf8")).toBe("saved-state");
});
it("keeps using legacy user data when the directory cannot be moved", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-user-data-fallback-"));
tempDirs.push(root);
const legacy = path.join(root, "Real-Debrid-Downloader");
fs.mkdirSync(legacy, { recursive: true });
const rename = fs.renameSync;
fs.renameSync = (() => {
throw Object.assign(new Error("locked"), { code: "EPERM" });
}) as typeof fs.renameSync;
try {
const migrate = (storage as unknown as Record<string, (rootPath: string) => string>).migrateProductUserDataDirectory;
expect(typeof migrate).toBe("function");
expect(migrate(root)).toBe(legacy);
} finally {
fs.renameSync = rename;
}
});
});
+19 -19
View File
@@ -111,14 +111,14 @@ function createReleaseFixture(): string {
const setupSha512 = crypto.createHash("sha512").update(setupPayload).digest("base64");
writeFile(rootDir, "package.json", `${JSON.stringify({
name: "real-debrid-downloader",
name: "multi-debrid-downloader",
version: "1.7.233",
build: {
productName: "Real-Debrid-Downloader",
productName: "Multi-Debrid-Downloader",
publish: {
provider: "github",
owner: "Sucukdeluxe",
repo: "multi-debrid-downloader"
repo: "Multi-Debrid-Downloader"
},
files: [
"build/main/**/*",
@@ -157,16 +157,16 @@ function createReleaseFixture(): string {
writeFile(
rootDir,
"latest.yml",
`version: 1.7.233\nfiles:\n - url: Real-Debrid-Downloader-Setup-1.7.233.exe\n sha512: ${setupSha512}\n size: ${setupPayload.length}\npath: Real-Debrid-Downloader-Setup-1.7.233.exe\nsha512: ${setupSha512}\n`
`version: 1.7.233\nfiles:\n - url: Multi-Debrid-Downloader-Setup-1.7.233.exe\n sha512: ${setupSha512}\n size: ${setupPayload.length}\npath: Multi-Debrid-Downloader-Setup-1.7.233.exe\nsha512: ${setupSha512}\n`
);
writeFile(
rootDir,
"win-unpacked/resources/app-update.yml",
"provider: github\nowner: Sucukdeluxe\nrepo: multi-debrid-downloader\n"
"provider: github\nowner: Sucukdeluxe\nrepo: Multi-Debrid-Downloader\n"
);
writeFile(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe", setupPayload);
writeFile(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe.blockmap", "blockmap");
writeFile(rootDir, "Real-Debrid-Downloader-1.7.233-portable.exe", "portable");
writeFile(rootDir, "Multi-Debrid-Downloader-Setup-1.7.233.exe", setupPayload);
writeFile(rootDir, "Multi-Debrid-Downloader-Setup-1.7.233.exe.blockmap", "blockmap");
writeFile(rootDir, "Multi-Debrid-Downloader-1.7.233-portable.exe", "portable");
writeRedistributionFiles(rootDir);
writeRedistributionFiles(rootDir, true);
writeFile(rootDir, "assets/app_icon.ico", "application-icon");
@@ -190,9 +190,9 @@ describe("public release metadata", () => {
expect(result.publish).toEqual({
provider: "github",
owner: "Sucukdeluxe",
repo: "multi-debrid-downloader"
repo: "Multi-Debrid-Downloader"
});
expect(result.latestArtifact).toBe("Real-Debrid-Downloader-Setup-1.7.233.exe");
expect(result.latestArtifact).toBe("Multi-Debrid-Downloader-Setup-1.7.233.exe");
expect(result.missingArtifacts).toEqual([]);
});
@@ -208,16 +208,16 @@ describe("public release metadata", () => {
it("rejects a latest.yml path whose artifact does not exist", () => {
const rootDir = createReleaseFixture();
fs.rmSync(path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe"));
fs.rmSync(path.join(rootDir, "Multi-Debrid-Downloader-Setup-1.7.233.exe"));
expect(() => verifyPublicRelease(rootDir)).toThrow(/Real-Debrid-Downloader-Setup-1\.7\.233\.exe/);
expect(() => verifyPublicRelease(rootDir)).toThrow(/Multi-Debrid-Downloader-Setup-1\.7\.233\.exe/);
});
it("rejects syntactically invalid latest.yml", () => {
const rootDir = createReleaseFixture();
fs.writeFileSync(
path.join(rootDir, "latest.yml"),
"version: 1.7.233\nfiles: [\npath: Real-Debrid-Downloader-Setup-1.7.233.exe\n"
"version: 1.7.233\nfiles: [\npath: Multi-Debrid-Downloader-Setup-1.7.233.exe\n"
);
expect(() => verifyPublicRelease(rootDir)).toThrow(/latest\.yml|yaml/i);
@@ -227,7 +227,7 @@ describe("public release metadata", () => {
const rootDir = createReleaseFixture();
const latestPath = path.join(rootDir, "latest.yml");
const latest = fs.readFileSync(latestPath, "utf8").replace(
"url: Real-Debrid-Downloader-Setup-1.7.233.exe",
"url: Multi-Debrid-Downloader-Setup-1.7.233.exe",
"url: Different-Setup-1.7.233.exe"
);
fs.writeFileSync(latestPath, latest);
@@ -247,7 +247,7 @@ describe("public release metadata", () => {
it("rejects a directory in place of an artifact file", () => {
const rootDir = createReleaseFixture();
const setupPath = path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe");
const setupPath = path.join(rootDir, "Multi-Debrid-Downloader-Setup-1.7.233.exe");
fs.rmSync(setupPath);
fs.mkdirSync(setupPath);
@@ -256,7 +256,7 @@ describe("public release metadata", () => {
it("rejects an empty artifact file", () => {
const rootDir = createReleaseFixture();
fs.writeFileSync(path.join(rootDir, "Real-Debrid-Downloader-1.7.233-portable.exe"), "");
fs.writeFileSync(path.join(rootDir, "Multi-Debrid-Downloader-1.7.233-portable.exe"), "");
expect(() => verifyPublicRelease(rootDir)).toThrow(/artifact|empty|file/i);
});
@@ -307,7 +307,7 @@ describe("public release metadata", () => {
it("rejects a symlink in place of a release artifact", () => {
const rootDir = createReleaseFixture();
const setupPath = path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe");
const setupPath = path.join(rootDir, "Multi-Debrid-Downloader-Setup-1.7.233.exe");
const targetPath = path.join(rootDir, "setup-target.exe");
fs.renameSync(setupPath, targetPath);
fs.symlinkSync(targetPath, setupPath, "file");
@@ -385,8 +385,8 @@ describe("public release metadata", () => {
});
expect(result.verifiedArchives).toEqual([
"Real-Debrid-Downloader-Setup-1.7.233.exe",
"Real-Debrid-Downloader-1.7.233-portable.exe"
"Multi-Debrid-Downloader-Setup-1.7.233.exe",
"Multi-Debrid-Downloader-1.7.233-portable.exe"
]);
});
+54 -8
View File
@@ -293,6 +293,33 @@ function workspaceModel(): AccountWorkspaceViewModel {
providerOrder: ["Debrid-Link", "Real-Debrid"],
routing: ["rapidgator.net → Debrid-Link"],
autoFallback: true
},
runtime: {
providers: [
{
id: "realdebrid",
label: "Real-Debrid",
accountCount: 2,
availableAccountCount: 1,
activeDownloads: 3,
dailyUsageText: "12,5 GB"
}
],
accounts: [
{
id: "rd-runtime",
providerLabel: "Real-Debrid",
modeLabel: "API",
identity: "stored-user",
stateLabel: "Cooldown",
stateTone: "warning",
activeDownloads: 0,
dailyUsageText: "4,2 GB",
successRateText: "75 % (3/4)",
lastUsedText: "vor 2 Min.",
cooldownText: "Rate-Limit aktiv · 48 Sek."
}
]
}
};
}
@@ -644,10 +671,28 @@ describe("account workspace", () => {
const html = renderToStaticMarkup(<AccountWorkspace actions={workspaceActions()} model={workspaceModel()} />);
expect(html).toContain("ui-sliding-selection ui-sliding-selection-horizontal");
expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(2);
expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(3);
expect(html.match(/data-sliding-selection-active="true"/g)).toHaveLength(1);
});
it("shows a live provider and account overview without exposing raw failure details", () => {
const html = renderToStaticMarkup(
<AccountWorkspace
actions={workspaceActions()}
model={{ ...workspaceModel(), activePanel: "runtime" as never }}
/>
);
expect(html).toContain("Laufzeit");
expect(html).toContain("Real-Debrid");
expect(html).toContain("1 von 2 verfügbar");
expect(html).toContain("<strong>3</strong> aktive Downloads");
expect(html).toContain("75 % (3/4)");
expect(html).toContain("Rate-Limit aktiv · 48 Sek.");
expect(html).not.toContain("HTTP 429");
expect(html).not.toContain("token-");
});
it("uses roving focus and horizontal keyboard navigation for account tabs", () => {
const panels: string[] = [];
const tree = AccountWorkspace({
@@ -656,13 +701,13 @@ describe("account workspace", () => {
});
const tabs = findElements(tree, (element) => element.props.role === "tab");
expect(tabs.map((tab) => tab.props.tabIndex)).toEqual([0, -1]);
expect(tabs.map((tab) => tab.props.tabIndex)).toEqual([0, -1, -1]);
for (const [key, sourceIndex, targetIndex, panel] of [
["ArrowRight", 0, 1, "rules"],
["ArrowLeft", 0, 1, "rules"],
["ArrowLeft", 0, 2, "runtime"],
["Home", 1, 0, "overview"],
["End", 0, 1, "rules"]
["End", 0, 2, "runtime"]
] as const) {
const target = compositeKeyboardTarget(tabs.length);
let prevented = false;
@@ -681,7 +726,7 @@ describe("account workspace", () => {
actions: workspaceActions()
});
const rulesTabs = findElements(rulesTree, (element) => element.props.role === "tab");
expect(rulesTabs.map((tab) => tab.props.tabIndex)).toEqual([-1, 0]);
expect(rulesTabs.map((tab) => tab.props.tabIndex)).toEqual([-1, 0, -1]);
});
it("announces account loading and errors without hiding the existing table state", () => {
@@ -870,13 +915,14 @@ describe("account workspace", () => {
expect(stopped).toBe(3);
});
it("keeps overview and rules in the same workspace while only one panel is active", () => {
it("keeps all account panels in the same workspace while only one panel is active", () => {
const html = renderToStaticMarkup(<AccountWorkspace actions={workspaceActions()} model={workspaceModel()} />);
expect(count(html, "class=\"settings-account-panel\"")).toBe(2);
expect(count(html, "hidden=\"\"")).toBe(1);
expect(count(html, "class=\"settings-account-panel\"")).toBe(3);
expect(count(html, "hidden=\"\"")).toBe(2);
expect(html).toContain("Provider-Reihenfolge");
expect(html).toContain("Hoster-Routing");
expect(html).toContain("Automatischer Fallback");
expect(html).toContain("Provider-Laufzeit");
});
it("keeps add and edit dialogs separate and every secret field protected", () => {
+4 -3
View File
@@ -379,9 +379,10 @@ describe("settings storage", () => {
it("migrates the previous private update repository to the public release repository", () => {
for (const updateRepo of [
"legacy-owner/real-debrid-downloader",
"legacy-owner/multi-debrid-downloader",
"legacy-owner/real-debrid-downloader.git"
"legacy-owner/real-debrid-downloader",
"legacy-owner/multi-debrid-downloader",
"legacy-owner/real-debrid-downloader.git",
"Sucukdeluxe/multi-debrid-downloader"
]) {
const normalized = normalizeSettings({ ...defaultSettings(), updateRepo });
expect(normalized.updateRepo).toBe(defaultSettings().updateRepo);
+16 -16
View File
@@ -46,7 +46,7 @@ afterEach(() => {
describe("update", () => {
it("normalizes update repo input", () => {
expect(normalizeUpdateRepo("")).toBe("Sucukdeluxe/multi-debrid-downloader");
expect(normalizeUpdateRepo("")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
expect(normalizeUpdateRepo("owner/repo")).toBe("owner/repo");
expect(normalizeUpdateRepo("https://github.com/owner/repo")).toBe("owner/repo");
expect(normalizeUpdateRepo("https://www.github.com/owner/repo")).toBe("owner/repo");
@@ -86,11 +86,11 @@ describe("update", () => {
html_url: "https://github.com/owner/repo/releases/tag/v9.9.9",
assets: [
{
name: "Real-Debrid-Downloader-9.9.9-portable.exe",
name: "Multi-Debrid-Downloader-9.9.9-portable.exe",
browser_download_url: "https://example.invalid/portable.exe"
},
{
name: "Real-Debrid-Downloader-Setup-9.9.9.exe",
name: "Multi-Debrid-Downloader-Setup-9.9.9.exe",
browser_download_url: "https://example.invalid/setup.exe",
digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
@@ -105,7 +105,7 @@ describe("update", () => {
const result = await checkGitHubUpdate("owner/repo");
expect(result.updateAvailable).toBe(true);
expect(result.setupAssetUrl).toBe("https://example.invalid/setup.exe");
expect(result.setupAssetName).toBe("Real-Debrid-Downloader-Setup-9.9.9.exe");
expect(result.setupAssetName).toBe("Multi-Debrid-Downloader-Setup-9.9.9.exe");
});
it("combines every stable release note newer than the installed version", async () => {
@@ -185,7 +185,7 @@ describe("update", () => {
latestTag: "v9.9.9",
releaseUrl: "https://github.com/owner/repo/releases/tag/v9.9.9",
setupAssetUrl: "https://example.invalid/stale-setup.exe",
setupAssetName: "Real-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetName: "Multi-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetDigest: `sha256:${executableDigest}`
};
@@ -411,7 +411,7 @@ describe("update", () => {
prerelease: false,
assets: [
{
name: "Real-Debrid-Downloader-Setup-9.9.9.exe",
name: "Multi-Debrid-Downloader-Setup-9.9.9.exe",
browser_download_url: "https://example.invalid/setup-no-digest.exe"
},
{
@@ -427,7 +427,7 @@ describe("update", () => {
if (url.includes("latest.yml")) {
return new Response(
`version: 9.9.9\npath: Real-Debrid-Downloader-Setup-9.9.9.exe\nsha512: ${digestSha512Base64}\n`,
`version: 9.9.9\npath: Multi-Debrid-Downloader-Setup-9.9.9.exe\nsha512: ${digestSha512Base64}\n`,
{
status: 200,
headers: { "Content-Type": "text/yaml" }
@@ -455,7 +455,7 @@ describe("update", () => {
latestTag: "v9.9.9",
releaseUrl: "https://github.com/owner/repo/releases/tag/v9.9.9",
setupAssetUrl: "https://example.invalid/setup-no-digest.exe",
setupAssetName: "Real-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetName: "Multi-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetDigest: ""
};
@@ -479,7 +479,7 @@ describe("update", () => {
prerelease: false,
assets: [
{
name: "Real-Debrid-Downloader-Setup-9.9.9.exe",
name: "Multi-Debrid-Downloader-Setup-9.9.9.exe",
browser_download_url: "https://example.invalid/setup-no-digest.exe"
},
{
@@ -495,7 +495,7 @@ describe("update", () => {
if (url.includes("latest.yml")) {
return new Response(
`version: 9.9.9\npath: Real-Debrid-Downloader-Setup-9.9.9.exe\nsha512: ${wrongDigestBase64}\n`,
`version: 9.9.9\npath: Multi-Debrid-Downloader-Setup-9.9.9.exe\nsha512: ${wrongDigestBase64}\n`,
{
status: 200,
headers: { "Content-Type": "text/yaml" }
@@ -523,7 +523,7 @@ describe("update", () => {
latestTag: "v9.9.9",
releaseUrl: "https://github.com/owner/repo/releases/tag/v9.9.9",
setupAssetUrl: "https://example.invalid/setup-no-digest.exe",
setupAssetName: "Real-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetName: "Multi-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetDigest: ""
};
@@ -593,14 +593,14 @@ describe("normalizeUpdateRepo extended", () => {
});
it("returns default for malformed inputs", () => {
expect(normalizeUpdateRepo("just-one-part")).toBe("Sucukdeluxe/multi-debrid-downloader");
expect(normalizeUpdateRepo(" ")).toBe("Sucukdeluxe/multi-debrid-downloader");
expect(normalizeUpdateRepo("just-one-part")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
expect(normalizeUpdateRepo(" ")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
});
it("rejects traversal-like owner or repo segments", () => {
expect(normalizeUpdateRepo("../owner/repo")).toBe("Sucukdeluxe/multi-debrid-downloader");
expect(normalizeUpdateRepo("owner/../repo")).toBe("Sucukdeluxe/multi-debrid-downloader");
expect(normalizeUpdateRepo("https://github.com/owner/../../repo")).toBe("Sucukdeluxe/multi-debrid-downloader");
expect(normalizeUpdateRepo("../owner/repo")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
expect(normalizeUpdateRepo("owner/../repo")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
expect(normalizeUpdateRepo("https://github.com/owner/../../repo")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
});
it("handles www prefix", () => {
+3 -3
View File
@@ -116,7 +116,7 @@ function createSettings(): AppSettings {
speedLimitEnabled: false,
speedLimitKbps: 0,
speedLimitMode: "global",
updateRepo: "Sucukdeluxe/multi-debrid-downloader",
updateRepo: "Sucukdeluxe/Multi-Debrid-Downloader",
autoUpdateCheck: true,
clipboardWatch: true,
minimizeToTray: false,
@@ -565,7 +565,7 @@ function createUpdate(updateAvailable: boolean): UpdateCheckResult {
currentVersion: "2.0.12",
latestVersion: "9.9.9",
latestTag: "v9.9.9",
releaseUrl: "https://github.com/Sucukdeluxe/multi-debrid-downloader/releases/tag/v9.9.9",
releaseUrl: "https://github.com/Sucukdeluxe/Multi-Debrid-Downloader/releases/tag/v9.9.9",
setupAssetUrl: "https://example.test/Multi-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetName: "Multi-Debrid-Downloader-Setup-9.9.9.exe",
setupAssetDigest: "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@@ -576,7 +576,7 @@ function createUpdate(updateAvailable: boolean): UpdateCheckResult {
currentVersion: "2.0.12",
latestVersion: "2.0.12",
latestTag: "v2.0.12",
releaseUrl: "https://github.com/Sucukdeluxe/multi-debrid-downloader/releases/tag/v2.0.12",
releaseUrl: "https://github.com/Sucukdeluxe/Multi-Debrid-Downloader/releases/tag/v2.0.12",
releaseNotes: "Aktuelle Version"
};
}