feat(settings): expose Real-Debrid multi-account controls
This commit is contained in:
@@ -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)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 }]
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user