feat(settings): expose Real-Debrid multi-account controls

This commit is contained in:
Sucukdeluxe
2026-08-15 21:34:28 +02:00
parent 7e65195057
commit d720c5912e
15 changed files with 316 additions and 61 deletions
+15 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildAccountCreateCommand, createAccountDialogState } from "../src/renderer/App";
import { buildAccountCreateCommand, buildRealDebridWebCreateLoginRequest, createAccountDialogState } from "../src/renderer/App";
import { createRendererSettings } from "../src/main/renderer-state";
import { defaultSettings } from "../src/main/constants";
@@ -27,4 +27,18 @@ describe("account creation dialog", () => {
expect(dialog.password).toBe("");
expect(dialog.megaAccounts).toEqual([]);
});
it("forwards the selected daily limit with a newly reserved Real-Debrid Web account", () => {
const settings = createRendererSettings(defaultSettings());
const dialog = {
...createAccountDialogState("create", "realdebrid-web", settings),
dailyLimitGb: "2,5"
};
expect(buildRealDebridWebCreateLoginRequest(dialog, "rdw_reservedopaqueid")).toEqual({
accountId: "rdw_reservedopaqueid",
create: true,
dailyLimitBytes: Math.floor(2.5 * 1024 * 1024 * 1024)
});
});
});
+33
View File
@@ -22,6 +22,39 @@ function megaTarget(identity: string): AccountEditTarget {
}
describe("renderer-safe account editing", () => {
it("targets one concrete Real-Debrid API account for replace and delete", () => {
const settings = defaultSettings();
settings.realDebridApiTokens = JSON.stringify({
version: 1,
accounts: [
{ id: "rda_first", token: "fixture-token-first" },
{ id: "rda_second", token: "fixture-token-second" }
]
});
const renderer = createRendererState(settings);
const target: AccountEditTarget = {
type: "single",
rowKey: "rd-rda_second",
kind: "realdebrid-api",
service: "realdebrid",
provider: "realdebrid",
accountId: "rda_second"
};
const edit = createAccountEditState(target, renderer.accounts);
expect(buildAccountReplaceCommand({ ...edit, token: "replacement-token" })).toEqual(expect.objectContaining({
action: "replace",
kind: "realdebrid-api",
accountId: "rda_second",
secret: "replacement-token"
}));
expect(buildAccountDeleteCommand(target)).toEqual({
action: "delete",
kind: "realdebrid-api",
accountId: "rda_second"
});
});
it("opens an existing account without reading its stored secret", () => {
const identity = "safe-edit@example.test";
const state = createRendererState({
+2 -2
View File
@@ -76,11 +76,11 @@ describe("account preload contract", () => {
it("forwards account-bound existing and create browser logins", async () => {
await electron.api?.openRealDebridLogin({ accountId: "rdw_existing" });
await electron.api?.openRealDebridLogin({ accountId: "rdw_reserved", create: true });
await electron.api?.openRealDebridLogin({ accountId: "rdw_reserved", create: true, dailyLimitBytes: 10_000 });
expect(electron.invoke.mock.calls).toEqual([
[IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_existing" }],
[IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_reserved", create: true }]
[IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_reserved", create: true, dailyLimitBytes: 10_000 }]
]);
});
+37 -2
View File
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest";
import {
buildConfiguredProviderOrder,
filterAccountDialogOptions,
getAvailableAccountOptions,
getAccountDialogSelectableOptions,
isAccountRowSelectionKey,
matchesAccountModeFilter,
pruneAccountRowSelection,
resolveAccountUsername,
resolveVisibleAccountKind
resolveVisibleAccountKind,
sortAccountServices
} from "../src/renderer/account-ui";
import * as accountUi from "../src/renderer/account-ui";
@@ -32,7 +34,40 @@ describe("account dialog filter", () => {
it("combines an exact service choice with an access-type search", () => {
expect(filterAccountDialogOptions(options, "web", "Real-Debrid").map((option) => option.id)).toEqual(["rd-web"]);
expect(filterAccountDialogOptions(options, "api", "all").map((option) => option.id)).toEqual(["rd-api", "md-api"]);
expect(filterAccountDialogOptions(options, "api", "all").map((option) => option.id)).toEqual(["md-api", "rd-api"]);
});
it("sorts services alphabetically in German without duplicates", () => {
expect(sortAccountServices([
"Real-Debrid",
"Mega-Debrid",
"BestDebrid",
"Debrid-Link",
"1Fichier",
"AllDebrid",
"DDownload",
"LinkSnappy",
"Mega-Debrid"
])).toEqual([
"1Fichier",
"AllDebrid",
"BestDebrid",
"DDownload",
"Debrid-Link",
"LinkSnappy",
"Mega-Debrid",
"Real-Debrid"
]);
});
it("keeps both Real-Debrid access types addable after existing Real-Debrid accounts", () => {
const choices = [
{ kind: "realdebrid-api", service: "realdebrid" },
{ kind: "realdebrid-web", service: "realdebrid" },
{ kind: "bestdebrid-api", service: "bestdebrid" }
];
expect(getAvailableAccountOptions(choices, ["realdebrid", "bestdebrid"]).map((option) => option.kind))
.toEqual(["realdebrid-api", "realdebrid-web"]);
});
});
+1
View File
@@ -174,6 +174,7 @@ describe("renderer localization", () => {
["Schätzwert", "Estimate"],
["ist verfügbar. Installierte Version:", "is available. Installed version:"],
["Tageslimit erreicht. Neue Links wechseln auf den nächsten Hoster.", "Daily limit reached. New links will switch to the next hoster."],
["Der ausgewählte Real-Debrid-Account wurde nicht gefunden.", "The selected Real-Debrid account was not found."],
["Nur lokal", "Local only"],
["Name kopiert", "Name copied"],
["Link kopiert", "Link copied"],
+5 -2
View File
@@ -15,11 +15,14 @@ function eventFor(url: string) {
describe("ipc-security", () => {
it("accepts only opaque Real-Debrid browser account login requests", () => {
expect(validateRealDebridLoginRequest({ accountId: "rdw_existing" })).toEqual({ accountId: "rdw_existing", create: false });
expect(validateRealDebridLoginRequest({ accountId: "rdw_reserved", create: true })).toEqual({ accountId: "rdw_reserved", create: true });
expect(validateRealDebridLoginRequest({ accountId: "rdw_existing" })).toEqual({ accountId: "rdw_existing", create: false, dailyLimitBytes: 0 });
expect(validateRealDebridLoginRequest({ accountId: "rdw_reserved", create: true, dailyLimitBytes: 12_345.9 })).toEqual({ accountId: "rdw_reserved", create: true, dailyLimitBytes: 12_345 });
expect(() => validateRealDebridLoginRequest({ accountId: "../shared", create: true })).toThrow(/Account-Payload/i);
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: "yes" })).toThrow(/Account-Payload/i);
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: false, token: "secret" })).toThrow(/Account-Payload/i);
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: true, dailyLimitBytes: -1 })).toThrow(/Account-Payload/i);
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: true, dailyLimitBytes: Number.MAX_SAFE_INTEGER + 1 })).toThrow(/Account-Payload/i);
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_existing", dailyLimitBytes: 1 })).toThrow(/Account-Payload/i);
});
it("accepts IPC from the configured Vite development renderer origin", () => {
+7 -4
View File
@@ -255,7 +255,7 @@ describe("realdebrid-web", () => {
dispose: vi.fn()
};
controller.settings = defaultSettings();
controller.pendingRealDebridWebAccountIds = new Map([["rdw_reserved", 0]]);
controller.pendingRealDebridWebAccountIds = new Map([["rdw_reserved", { generation: 0, dailyLimitBytes: 987_654 }]]);
controller.realDebridWebGenerations = new Map([["rdw_reserved", 0]]);
controller.realDebridWebFallbacks = new Map([["rdw_reserved", fallback]]);
controller.manager = { applyDebridAccountStatuses: applyStatuses };
@@ -272,6 +272,7 @@ describe("realdebrid-web", () => {
expect(updateSettings).toHaveBeenCalledTimes(1);
expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_reserved"]);
expect(controller.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_reserved: 987_654 });
expect(applyStatuses).toHaveBeenCalledWith([
expect.objectContaining({ accountId: "rdw_reserved", valid: true, username: "fixture-user" })
]);
@@ -334,7 +335,7 @@ describe("realdebrid-web", () => {
controller.realDebridWebAuthenticationTasks = new Map();
controller.audit = vi.fn();
await expect(controller.openRealDebridLoginWindow({ accountId: "rdw_failed", create: true })).rejects.toThrow("load failed");
await expect(controller.openRealDebridLoginWindow({ accountId: "rdw_failed", create: true, dailyLimitBytes: 123_456 })).rejects.toThrow("load failed");
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
expect(controller.realDebridWebFallbacks.size).toBe(0);
@@ -353,11 +354,12 @@ describe("realdebrid-web", () => {
controller.realDebridWebAuthenticationTasks = new Map();
controller.audit = vi.fn();
await controller.openRealDebridLoginWindow({ accountId: "rdw_closed", create: true });
await controller.openRealDebridLoginWindow({ accountId: "rdw_closed", create: true, dailyLimitBytes: 123_456 });
mockBrowserWindow.close();
await vi.waitFor(() => expect(controller.realDebridWebFallbacks.size).toBe(0));
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
expect(controller.settings.realDebridAccountDailyLimitBytes).toEqual({});
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_closed");
expect(mockFromPartition).toHaveBeenCalledWith("realdebrid-web-rdw_closed");
});
@@ -404,13 +406,14 @@ describe("realdebrid-web", () => {
});
controller.audit = vi.fn();
await controller.openRealDebridLoginWindow({ accountId: "rdw_close_auth", create: true });
await controller.openRealDebridLoginWindow({ accountId: "rdw_close_auth", create: true, dailyLimitBytes: 123_456 });
await vi.waitFor(() => expect(mockExecuteJavaScript).toHaveBeenCalledTimes(1));
mockBrowserWindow.close();
resolveClosingToken("close-time-token");
await vi.waitFor(() => expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_close_auth"]));
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
expect(controller.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_close_auth: 123_456 });
expect(mockFromPartition).not.toHaveBeenCalledWith("persist:realdebrid-web-rdw_close_auth");
expect(controller.manager.applyDebridAccountStatuses).toHaveBeenCalledWith([
expect.objectContaining({ accountId: "rdw_close_auth", valid: true, username: "close-user" })
+21
View File
@@ -159,4 +159,25 @@ describe("renderer state serialization", () => {
expect(serialized).not.toContain(secret);
}
});
it("exposes only non-secret Real-Debrid account controls to the renderer", () => {
const settings = {
...defaultSettings(),
realDebridApiTokens: JSON.stringify({ version: 1, accounts: [{ id: "rda_visible", token: "fixture-rd-secret" }] }),
realDebridDisabledAccountIds: ["rda_visible"],
realDebridAccountDailyLimitBytes: { rda_visible: 10_000 },
realDebridAccountDailyUsageBytes: { rda_visible: 2_000 },
realDebridAccountTotalUsageBytes: { rda_visible: 8_000 }
};
const renderer = createRendererState(settings).settings;
expect(renderer.realDebridDisabledAccountIds).toEqual(["rda_visible"]);
expect(renderer.realDebridAccountDailyLimitBytes).toEqual({ rda_visible: 10_000 });
expect(renderer.realDebridAccountDailyUsageBytes).toEqual({ rda_visible: 2_000 });
expect(renderer.realDebridAccountTotalUsageBytes).toEqual({ rda_visible: 8_000 });
expect(JSON.stringify(renderer)).not.toContain("fixture-rd-secret");
expect(renderer).not.toHaveProperty("realDebridApiTokens");
expect(renderer).not.toHaveProperty("realDebridWebAccountIds");
});
});