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:
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
@@ -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", () => {
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user