Harden renderer account IPC boundary
Move secret-bearing account and settings state behind main-process boundaries for Task 1B. Renderer snapshots now expose RendererSettings plus safe account metadata instead of full AppSettings, with provider tokens, passwords, API keys, archive passwords, and notification URLs excluded from renderer-bound state. Add write-only account create, replace, update-secret, and delete IPC commands, validate renderer settings updates against the safe shape, and keep account command results limited to safe settings, safe accounts, and stable account IDs. Preserve existing account behavior while removing renderer secret access: blank replace secrets retain stored main-process secrets, Mega-Debrid API/Web pools stay mode-specific, Debrid-Link key metadata migrates by stable key ID, and delete/enable operations target stable account or provider identities. Remove obsolete renderer-side account status helpers from the old settings snapshot flow. Add focused coverage for all supported renderer account kinds, secret-free UiSnapshot serialization, preload account command forwarding, malformed payload error sanitization, Mega-Debrid preferApi preservation, Debrid-Link key metadata migration, account edit safety, settings UI, debug server settings payloads, link export, and visual fixtures.
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyAccountCommand, validateAccountCommand } from "../src/main/account-commands";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import type { AppSettings, RendererAccountKind } from "../src/shared/types";
|
||||
|
||||
const ORIGINAL_SECRET = "fixture-original-secret-4qV8";
|
||||
const REPLACEMENT_SECRET = "fixture-replacement-secret-6nC2";
|
||||
const GIB = 1024 * 1024 * 1024;
|
||||
|
||||
const SECRET_RETAIN_CASES: Array<{
|
||||
kind: RendererAccountKind;
|
||||
identity: string;
|
||||
secret: string;
|
||||
retained: (settings: AppSettings) => boolean;
|
||||
}> = [
|
||||
{ kind: "realdebrid-api", identity: "", secret: "fixture-retain-rd-1aC3", retained: (settings) => settings.token === "fixture-retain-rd-1aC3" },
|
||||
{ kind: "megadebrid-api", identity: "retain-mega-api@example.test", secret: "fixture-retain-mega-api-2bD4", retained: (settings) => settings.megaDebridApiCredentials === "retain-mega-api@example.test:fixture-retain-mega-api-2bD4" },
|
||||
{ kind: "megadebrid-web", identity: "retain-mega-web@example.test", secret: "fixture-retain-mega-web-3cE5", retained: (settings) => settings.megaDebridWebCredentials === "retain-mega-web@example.test:fixture-retain-mega-web-3cE5" },
|
||||
{ kind: "bestdebrid-api", identity: "", secret: "fixture-retain-best-4dF6", retained: (settings) => settings.bestToken === "fixture-retain-best-4dF6" },
|
||||
{ kind: "alldebrid-api", identity: "", secret: "fixture-retain-all-5eG7", retained: (settings) => settings.allDebridToken === "fixture-retain-all-5eG7" },
|
||||
{ kind: "ddownload-login", identity: "retain-dd@example.test", secret: "fixture-retain-dd-6fH8", retained: (settings) => settings.ddownloadPassword === "fixture-retain-dd-6fH8" },
|
||||
{ kind: "onefichier-api", identity: "", secret: "fixture-retain-one-7gJ9", retained: (settings) => settings.oneFichierApiKey === "fixture-retain-one-7gJ9" },
|
||||
{ kind: "debridlink-api", identity: "", secret: "fixture-retain-dl-8hK1", retained: (settings) => settings.debridLinkApiKeys === "fixture-retain-dl-8hK1" },
|
||||
{ kind: "linksnappy-login", identity: "retain-ls@example.test", secret: "fixture-retain-ls-9jL2", retained: (settings) => settings.linkSnappyPassword === "fixture-retain-ls-9jL2" }
|
||||
];
|
||||
|
||||
const ACCOUNT_KINDS: RendererAccountKind[] = [
|
||||
"realdebrid-api",
|
||||
"realdebrid-web",
|
||||
"megadebrid-api",
|
||||
"megadebrid-web",
|
||||
"bestdebrid-api",
|
||||
"bestdebrid-web",
|
||||
"alldebrid-api",
|
||||
"alldebrid-web",
|
||||
"ddownload-login",
|
||||
"onefichier-api",
|
||||
"debridlink-api",
|
||||
"linksnappy-login"
|
||||
];
|
||||
|
||||
describe("write-only account commands", () => {
|
||||
it.each([
|
||||
["realdebrid-api", "", "fixture-rd-provider-secret-1fA4", "token"],
|
||||
["realdebrid-web", "", "", "realDebridUseWebLogin"],
|
||||
["bestdebrid-api", "", "fixture-best-provider-secret-2gB5", "bestToken"],
|
||||
["bestdebrid-web", "", "", "bestDebridUseWebLogin"],
|
||||
["alldebrid-api", "", "fixture-ad-provider-secret-3hC6", "allDebridToken"],
|
||||
["alldebrid-web", "", "", "allDebridUseWebLogin"],
|
||||
["ddownload-login", "dd-safe@example.test", "fixture-dd-provider-secret-4jD7", "ddownloadPassword"],
|
||||
["onefichier-api", "", "fixture-one-provider-secret-5kE8", "oneFichierApiKey"],
|
||||
["linksnappy-login", "ls-safe@example.test", "fixture-ls-provider-secret-6mF9", "linkSnappyPassword"]
|
||||
] as const)("preserves create and delete behavior for %s", (kind, identity, secret, configuredKey) => {
|
||||
const created = applyAccountCommand(defaultSettings(), validateAccountCommand({
|
||||
action: "create",
|
||||
kind,
|
||||
identity,
|
||||
secret,
|
||||
dailyLimitBytes: 4_294_967_296
|
||||
}));
|
||||
|
||||
expect(created.settings[configuredKey]).toBe(secret || true);
|
||||
expect(JSON.stringify(created.response)).not.toContain(secret || "fixture-never-present");
|
||||
|
||||
const deleted = applyAccountCommand(created.settings, validateAccountCommand({
|
||||
action: "delete",
|
||||
kind,
|
||||
accountId: created.response.accountId
|
||||
}));
|
||||
|
||||
expect(deleted.settings[configuredKey]).toBe(secret ? "" : false);
|
||||
});
|
||||
|
||||
it("creates an account without returning submitted secrets", () => {
|
||||
const command = validateAccountCommand({
|
||||
action: "create",
|
||||
kind: "megadebrid-api",
|
||||
identity: "new-account@example.test",
|
||||
secret: ORIGINAL_SECRET,
|
||||
dailyLimitBytes: 12_884_901_888
|
||||
});
|
||||
const result = applyAccountCommand(defaultSettings(), command);
|
||||
|
||||
expect(result.settings.megaDebridApiCredentials).toContain(ORIGINAL_SECRET);
|
||||
expect(JSON.stringify(result.response)).not.toContain(ORIGINAL_SECRET);
|
||||
expect(result.response.accountId).toBe(getMegaDebridAccountId("new-account@example.test"));
|
||||
});
|
||||
|
||||
it("adds a Web Mega-Debrid account without copying it into the API pool or changing preferApi", () => {
|
||||
const result = applyAccountCommand({
|
||||
...defaultSettings(),
|
||||
megaCredentials: "api@example.test:fixture-api-secret-1aM2",
|
||||
megaDebridApiCredentials: "api@example.test:fixture-api-secret-1aM2",
|
||||
megaDebridWebCredentials: "",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: false,
|
||||
megaDebridPreferApi: false
|
||||
}, validateAccountCommand({
|
||||
action: "create",
|
||||
kind: "megadebrid-web",
|
||||
identity: "web@example.test",
|
||||
secret: "fixture-web-secret-3bN4",
|
||||
dailyLimitBytes: 0
|
||||
}));
|
||||
|
||||
expect(result.settings.megaDebridApiCredentials).toBe("api@example.test:fixture-api-secret-1aM2");
|
||||
expect(result.settings.megaDebridWebCredentials).toBe("web@example.test:fixture-web-secret-3bN4");
|
||||
expect(result.settings.megaDebridApiEnabled).toBe(true);
|
||||
expect(result.settings.megaDebridWebEnabled).toBe(true);
|
||||
expect(result.settings.megaDebridPreferApi).toBe(false);
|
||||
expect(JSON.stringify(result.response)).not.toContain("fixture-web-secret-3bN4");
|
||||
});
|
||||
|
||||
it("retains a stored secret when replace receives a blank secret", () => {
|
||||
const identity = "existing-account@example.test";
|
||||
const accountId = getMegaDebridAccountId(identity);
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: `${identity}:${ORIGINAL_SECRET}`,
|
||||
megaDebridApiCredentials: `${identity}:${ORIGINAL_SECRET}`,
|
||||
megaDebridApiEnabled: true
|
||||
};
|
||||
const command = validateAccountCommand({
|
||||
action: "replace",
|
||||
kind: "megadebrid-api",
|
||||
accountId,
|
||||
identity: "renamed-account@example.test",
|
||||
secret: "",
|
||||
dailyLimitBytes: 8_589_934_592
|
||||
});
|
||||
const result = applyAccountCommand(settings, command);
|
||||
|
||||
expect(result.settings.megaDebridApiCredentials).toBe(`renamed-account@example.test:${ORIGINAL_SECRET}`);
|
||||
expect(JSON.stringify(result.response)).not.toContain(ORIGINAL_SECRET);
|
||||
});
|
||||
|
||||
it.each(SECRET_RETAIN_CASES)("retains the stored $kind secret when replace receives a blank secret", ({ kind, identity, secret, retained }) => {
|
||||
const created = applyAccountCommand(defaultSettings(), validateAccountCommand({
|
||||
action: "create",
|
||||
kind,
|
||||
identity,
|
||||
secret,
|
||||
dailyLimitBytes: 1
|
||||
}));
|
||||
const replaced = applyAccountCommand(created.settings, validateAccountCommand({
|
||||
action: "replace",
|
||||
kind,
|
||||
accountId: created.response.accountId,
|
||||
identity,
|
||||
secret: "",
|
||||
dailyLimitBytes: 1
|
||||
}));
|
||||
|
||||
expect(retained(replaced.settings)).toBe(true);
|
||||
expect(JSON.stringify(replaced.response)).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("replaces a Mega-Debrid account while preserving sibling accounts and mode-specific state", () => {
|
||||
const firstId = getMegaDebridAccountId("first@example.test");
|
||||
const oldId = getMegaDebridAccountId("second@example.test");
|
||||
const newId = getMegaDebridAccountId("renamed@example.test");
|
||||
const webId = getMegaDebridAccountId("web@example.test");
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: "first@example.test:first-secret\nsecond@example.test:second-secret\nweb@example.test:web-secret",
|
||||
megaDebridApiCredentials: "first@example.test:first-secret\nsecond@example.test:second-secret",
|
||||
megaDebridWebCredentials: "web@example.test:web-secret",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: true,
|
||||
megaDebridPreferApi: false,
|
||||
megaDebridDisabledAccountIds: [oldId, firstId, webId],
|
||||
megaDebridApiDisabledAccountIds: [oldId, firstId],
|
||||
megaDebridWebDisabledAccountIds: [webId],
|
||||
megaDebridAccountDailyLimitBytes: { [oldId]: 15 * GIB, [firstId]: 9 * GIB },
|
||||
megaDebridAccountDailyUsageBytes: { [oldId]: 4 * GIB, [firstId]: 2 * GIB },
|
||||
megaDebridAccountTotalUsageBytes: { [oldId]: 40 * GIB, [firstId]: 20 * GIB },
|
||||
debridAccountStatuses: {
|
||||
[oldId]: { accountId: oldId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***st", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 },
|
||||
[firstId]: { accountId: firstId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***st", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||
}
|
||||
};
|
||||
const result = applyAccountCommand(settings, validateAccountCommand({
|
||||
action: "replace",
|
||||
kind: "megadebrid-api",
|
||||
accountId: oldId,
|
||||
identity: "renamed@example.test",
|
||||
secret: "renamed-secret",
|
||||
dailyLimitBytes: Math.floor(25.5 * GIB)
|
||||
}));
|
||||
|
||||
expect(result.settings.megaDebridApiCredentials).toBe("first@example.test:first-secret\nrenamed@example.test:renamed-secret");
|
||||
expect(result.settings.megaDebridWebCredentials).toBe("web@example.test:web-secret");
|
||||
expect(result.settings.megaDebridPreferApi).toBe(false);
|
||||
expect(result.settings.megaDebridDisabledAccountIds).toEqual([firstId, newId, webId]);
|
||||
expect(result.settings.megaDebridApiDisabledAccountIds).toEqual([firstId, newId]);
|
||||
expect(result.settings.megaDebridWebDisabledAccountIds).toEqual([webId]);
|
||||
expect(result.settings.megaDebridAccountDailyLimitBytes).toEqual({ [firstId]: 9 * GIB, [newId]: Math.floor(25.5 * GIB) });
|
||||
expect(result.settings.megaDebridAccountDailyUsageBytes).toEqual({ [firstId]: 2 * GIB });
|
||||
expect(result.settings.megaDebridAccountTotalUsageBytes).toEqual({ [firstId]: 20 * GIB });
|
||||
expect(result.settings.debridAccountStatuses).toEqual({
|
||||
[firstId]: { accountId: firstId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***st", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||
});
|
||||
expect(JSON.stringify(result.response)).not.toContain("renamed-secret");
|
||||
});
|
||||
|
||||
it("replaces only the selected Debrid-Link key and migrates its own metadata", () => {
|
||||
const keyA = "fixture-dl-key-a-1aB2";
|
||||
const keyB = "fixture-dl-key-b-3cD4";
|
||||
const keyC = "fixture-dl-key-c-5eF6";
|
||||
const newKey = "fixture-dl-key-b-new-7gH8";
|
||||
const idA = getDebridLinkApiKeyId(keyA);
|
||||
const idB = getDebridLinkApiKeyId(keyB);
|
||||
const idNew = getDebridLinkApiKeyId(newKey);
|
||||
const result = applyAccountCommand({
|
||||
...defaultSettings(),
|
||||
debridLinkApiKeys: `${keyA}\n${keyB}\n${keyC}`,
|
||||
debridLinkDisabledKeyIds: [idB, idA],
|
||||
debridLinkApiKeyDailyLimitBytes: { [idA]: 5 * GIB, [idB]: 10 * GIB },
|
||||
debridLinkApiKeyDailyUsageBytes: { [idA]: 2 * GIB, [idB]: 4 * GIB },
|
||||
debridLinkApiKeyTotalUsageBytes: { [idA]: 12 * GIB, [idB]: 24 * GIB }
|
||||
}, validateAccountCommand({
|
||||
action: "replace",
|
||||
kind: "debridlink-api",
|
||||
accountId: idB,
|
||||
secret: newKey,
|
||||
dailyLimitBytes: 12 * GIB
|
||||
}));
|
||||
|
||||
expect(result.settings.debridLinkApiKeys).toBe(`${keyA}\n${newKey}\n${keyC}`);
|
||||
expect(result.settings.debridLinkDisabledKeyIds).toEqual([idA, idNew]);
|
||||
expect(result.settings.debridLinkApiKeyDailyLimitBytes).toEqual({ [idA]: 5 * GIB, [idNew]: 12 * GIB });
|
||||
expect(result.settings.debridLinkApiKeyDailyUsageBytes).toEqual({ [idA]: 2 * GIB });
|
||||
expect(result.settings.debridLinkApiKeyTotalUsageBytes).toEqual({ [idA]: 12 * GIB });
|
||||
expect(JSON.stringify(result.response)).not.toContain(newKey);
|
||||
});
|
||||
|
||||
it("keeps a matching Web identity disabled when its API identity is deleted", () => {
|
||||
const identity = "shared-mode@example.test";
|
||||
const accountId = getMegaDebridAccountId(identity);
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: `${identity}:fixture-api-mode-secret-1mN3`,
|
||||
megaDebridApiCredentials: `${identity}:fixture-api-mode-secret-1mN3`,
|
||||
megaDebridWebCredentials: `${identity}:fixture-web-mode-secret-2nP4`,
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: true,
|
||||
megaDebridDisabledAccountIds: [accountId],
|
||||
megaDebridApiDisabledAccountIds: [accountId],
|
||||
megaDebridWebDisabledAccountIds: [accountId]
|
||||
};
|
||||
|
||||
const deleted = applyAccountCommand(settings, validateAccountCommand({
|
||||
action: "delete",
|
||||
kind: "megadebrid-api",
|
||||
accountId
|
||||
}));
|
||||
|
||||
expect(deleted.settings.megaDebridApiCredentials).toBe("");
|
||||
expect(deleted.settings.megaDebridWebCredentials).toBe(`${identity}:fixture-web-mode-secret-2nP4`);
|
||||
expect(deleted.settings.megaDebridApiDisabledAccountIds).toEqual([]);
|
||||
expect(deleted.settings.megaDebridWebDisabledAccountIds).toEqual([accountId]);
|
||||
expect(deleted.settings.megaDebridDisabledAccountIds).toEqual([accountId]);
|
||||
});
|
||||
|
||||
it("updates only the selected secret and deletes only the selected account", () => {
|
||||
const firstIdentity = "first-account@example.test";
|
||||
const secondIdentity = "second-account@example.test";
|
||||
const firstId = getMegaDebridAccountId(firstIdentity);
|
||||
const secondId = getMegaDebridAccountId(secondIdentity);
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: `${firstIdentity}:${ORIGINAL_SECRET}\n${secondIdentity}:fixture-sibling-secret-3wH7`,
|
||||
megaDebridApiCredentials: `${firstIdentity}:${ORIGINAL_SECRET}\n${secondIdentity}:fixture-sibling-secret-3wH7`,
|
||||
megaDebridApiEnabled: true
|
||||
};
|
||||
const updated = applyAccountCommand(settings, validateAccountCommand({
|
||||
action: "update-secret",
|
||||
kind: "megadebrid-api",
|
||||
accountId: firstId,
|
||||
secret: REPLACEMENT_SECRET
|
||||
}));
|
||||
const deleted = applyAccountCommand(updated.settings, validateAccountCommand({
|
||||
action: "delete",
|
||||
kind: "megadebrid-api",
|
||||
accountId: firstId
|
||||
}));
|
||||
|
||||
expect(updated.settings.megaDebridApiCredentials).toContain(`${firstIdentity}:${REPLACEMENT_SECRET}`);
|
||||
expect(deleted.settings.megaDebridApiCredentials).toBe(`${secondIdentity}:fixture-sibling-secret-3wH7`);
|
||||
expect(deleted.response.accountId).toBe(secondId);
|
||||
expect(JSON.stringify([updated.response, deleted.response])).not.toContain(REPLACEMENT_SECRET);
|
||||
});
|
||||
|
||||
it("creates and deletes a Debrid-Link API key by stable key identity", () => {
|
||||
const secret = "fixture-dl-create-delete-1pQ4";
|
||||
const created = applyAccountCommand(defaultSettings(), validateAccountCommand({
|
||||
action: "create",
|
||||
kind: "debridlink-api",
|
||||
secret,
|
||||
dailyLimitBytes: 3 * GIB
|
||||
}));
|
||||
|
||||
expect(created.response.accountId).toBe(getDebridLinkApiKeyId(secret));
|
||||
expect(created.settings.debridLinkApiKeys).toBe(secret);
|
||||
expect(created.settings.debridLinkApiKeyDailyLimitBytes).toEqual({ [getDebridLinkApiKeyId(secret)]: 3 * GIB });
|
||||
expect(JSON.stringify(created.response)).not.toContain(secret);
|
||||
|
||||
const deleted = applyAccountCommand(created.settings, validateAccountCommand({
|
||||
action: "delete",
|
||||
kind: "debridlink-api",
|
||||
accountId: created.response.accountId
|
||||
}));
|
||||
|
||||
expect(deleted.settings.debridLinkApiKeys).toBe("");
|
||||
expect(deleted.settings.debridLinkApiKeyDailyLimitBytes).toEqual({});
|
||||
expect(deleted.response.accountId).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects malformed payloads without echoing submitted secrets", () => {
|
||||
let errorText = "";
|
||||
try {
|
||||
validateAccountCommand({
|
||||
action: "replace",
|
||||
kind: "megadebrid-api",
|
||||
accountId: 42,
|
||||
secret: REPLACEMENT_SECRET
|
||||
});
|
||||
} catch (error) {
|
||||
errorText = String(error);
|
||||
}
|
||||
|
||||
expect(errorText).toMatch(/ungültig/i);
|
||||
expect(errorText).not.toContain(REPLACEMENT_SECRET);
|
||||
});
|
||||
|
||||
it.each(ACCOUNT_KINDS)("rejects malformed %s payloads without echoing their submitted secret", (kind) => {
|
||||
const secret = `fixture-malformed-${kind}-3qR5`;
|
||||
let errorText = "";
|
||||
try {
|
||||
validateAccountCommand({ action: "replace", kind, accountId: 42, secret });
|
||||
} catch (error) {
|
||||
errorText = String(error);
|
||||
}
|
||||
|
||||
expect(errorText).toMatch(/ungültig/i);
|
||||
expect(errorText).not.toContain(secret);
|
||||
});
|
||||
});
|
||||
@@ -1,81 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyAccountDialogToSettings, createAccountDialogState, AccountDialogState } from "../src/renderer/App";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAccountCreateCommand, createAccountDialogState } from "../src/renderer/App";
|
||||
import { createRendererSettings } from "../src/main/renderer-state";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { isMegaDebridAccountDisabled } from "../src/shared/provider-daily-limits";
|
||||
|
||||
function megaDialog(kind: "megadebrid-api" | "megadebrid-web"): AccountDialogState {
|
||||
return {
|
||||
mode: "edit",
|
||||
kind,
|
||||
service: kind,
|
||||
token: "",
|
||||
login: "",
|
||||
password: "",
|
||||
dailyLimitGb: "",
|
||||
keyDailyLimitGbById: {},
|
||||
megaAccounts: [{ login: "user@x", password: "pw" }],
|
||||
megaNewLogin: "",
|
||||
megaNewPassword: "",
|
||||
megaDisabledIds: []
|
||||
};
|
||||
}
|
||||
|
||||
describe("applyAccountDialogToSettings — keeps the user's Mega preferApi choice", () => {
|
||||
it("does not flip megaDebridPreferApi to true when editing the API account", () => {
|
||||
const settings = { ...defaultSettings(), megaDebridApiEnabled: true, megaDebridWebEnabled: true, megaDebridPreferApi: false };
|
||||
const next = applyAccountDialogToSettings(settings, megaDialog("megadebrid-api"));
|
||||
expect(next.megaDebridApiEnabled).toBe(true);
|
||||
expect(next.megaDebridPreferApi).toBe(false);
|
||||
});
|
||||
|
||||
it("does not flip megaDebridPreferApi to false when editing the Web account", () => {
|
||||
const settings = { ...defaultSettings(), megaDebridApiEnabled: true, megaDebridWebEnabled: true, megaDebridPreferApi: true };
|
||||
const next = applyAccountDialogToSettings(settings, megaDialog("megadebrid-web"));
|
||||
expect(next.megaDebridWebEnabled).toBe(true);
|
||||
expect(next.megaDebridPreferApi).toBe(true);
|
||||
|
||||
describe("account creation dialog", () => {
|
||||
it("creates a mode-specific Mega-Debrid command without existing credentials", () => {
|
||||
const settings = createRendererSettings(defaultSettings());
|
||||
const dialog = {
|
||||
...createAccountDialogState("create", "megadebrid-web", settings),
|
||||
megaNewLogin: "web-safe@example.test",
|
||||
megaNewPassword: "fixture-dialog-secret-7pL2"
|
||||
};
|
||||
|
||||
expect(buildAccountCreateCommand(dialog)).toEqual({
|
||||
action: "create",
|
||||
kind: "megadebrid-web",
|
||||
identity: "web-safe@example.test",
|
||||
secret: "fixture-dialog-secret-7pL2",
|
||||
dailyLimitBytes: 0
|
||||
});
|
||||
});
|
||||
|
||||
it("adds a Web account without copying it into the API account pool", () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: "api@example.test:api-pass",
|
||||
megaDebridApiCredentials: "api@example.test:api-pass",
|
||||
megaDebridWebCredentials: "",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: false
|
||||
};
|
||||
const dialog = createAccountDialogState("create", "megadebrid-web", settings);
|
||||
|
||||
it("keeps all account fields blank before user input", () => {
|
||||
const dialog = createAccountDialogState("create", "debridlink-api", createRendererSettings(defaultSettings()));
|
||||
expect(dialog.token).toBe("");
|
||||
expect(dialog.password).toBe("");
|
||||
expect(dialog.megaAccounts).toEqual([]);
|
||||
|
||||
const next = applyAccountDialogToSettings(settings, {
|
||||
...dialog,
|
||||
megaAccounts: [{ login: "web@example.test", password: "web-pass" }]
|
||||
});
|
||||
|
||||
expect(next.megaDebridApiCredentials).toBe("api@example.test:api-pass");
|
||||
expect(next.megaDebridWebCredentials).toBe("web@example.test:web-pass");
|
||||
expect(next.megaDebridApiEnabled).toBe(true);
|
||||
expect(next.megaDebridWebEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("disables an API account without disabling the matching Web account", () => {
|
||||
const accountId = getMegaDebridAccountId("shared@example.test");
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: "shared@example.test:api-pass",
|
||||
megaDebridApiCredentials: "shared@example.test:api-pass",
|
||||
megaDebridWebCredentials: "shared@example.test:web-pass",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: true
|
||||
};
|
||||
const next = applyAccountDialogToSettings(settings, {
|
||||
...createAccountDialogState("edit", "megadebrid-api", settings),
|
||||
megaDisabledIds: [accountId]
|
||||
});
|
||||
|
||||
expect(isMegaDebridAccountDisabled(next, accountId, "api")).toBe(true);
|
||||
expect(isMegaDebridAccountDisabled(next, accountId, "web")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+81
-304
@@ -1,305 +1,82 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import {
|
||||
applyAccountEdit,
|
||||
buildAccountEditCheckSettings,
|
||||
createAccountEditState,
|
||||
removeAccountTarget,
|
||||
validateAccountEdit,
|
||||
validateAccountEditStatuses,
|
||||
type AccountEditTarget
|
||||
} from "../src/renderer/account-edit";
|
||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
|
||||
const GIB = 1024 * 1024 * 1024;
|
||||
|
||||
function megaTarget(login: string): AccountEditTarget {
|
||||
return {
|
||||
type: "mega",
|
||||
rowKey: `mega-${getMegaDebridAccountId(login)}`,
|
||||
kind: "megadebrid-api",
|
||||
service: "megadebrid-api",
|
||||
accountId: getMegaDebridAccountId(login)
|
||||
};
|
||||
}
|
||||
|
||||
function debridLinkTarget(token: string): AccountEditTarget {
|
||||
return {
|
||||
type: "debridlink",
|
||||
rowKey: `dl-${getDebridLinkApiKeyId(token)}`,
|
||||
kind: "debridlink-api",
|
||||
service: "debridlink",
|
||||
keyId: getDebridLinkApiKeyId(token)
|
||||
};
|
||||
}
|
||||
|
||||
describe("account-specific editing", () => {
|
||||
it("changes only the selected Mega-Debrid account and preserves sibling order and mode settings", () => {
|
||||
const oldId = getMegaDebridAccountId("second@example.com");
|
||||
const newId = getMegaDebridAccountId("renamed@example.com");
|
||||
const firstId = getMegaDebridAccountId("first@example.com");
|
||||
const webId = getMegaDebridAccountId("web@example.com");
|
||||
const settings = {
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { createRendererState } from "../src/main/renderer-state";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import {
|
||||
buildAccountDeleteCommand,
|
||||
buildAccountReplaceCommand,
|
||||
createAccountEditState,
|
||||
validateAccountEdit,
|
||||
type AccountEditTarget
|
||||
} from "../src/renderer/account-edit";
|
||||
|
||||
function megaTarget(identity: string): AccountEditTarget {
|
||||
const accountId = getMegaDebridAccountId(identity);
|
||||
return {
|
||||
type: "mega",
|
||||
rowKey: `mega-megadebrid-api-${accountId}`,
|
||||
kind: "megadebrid-api",
|
||||
service: "megadebrid-api",
|
||||
accountId
|
||||
};
|
||||
}
|
||||
|
||||
describe("renderer-safe account editing", () => {
|
||||
it("opens an existing account without reading its stored secret", () => {
|
||||
const identity = "safe-edit@example.test";
|
||||
const state = createRendererState({
|
||||
...defaultSettings(),
|
||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass\nthird@example.com:third-pass\nweb@example.com:web-pass",
|
||||
megaDebridApiCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass\nthird@example.com:third-pass",
|
||||
megaDebridWebCredentials: "web@example.com:web-pass",
|
||||
megaLogin: "first@example.com",
|
||||
megaPassword: "first-pass",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: true,
|
||||
megaDebridPreferApi: false,
|
||||
megaDebridDisabledAccountIds: [oldId, firstId, webId],
|
||||
megaDebridApiDisabledAccountIds: [oldId, firstId],
|
||||
megaDebridWebDisabledAccountIds: [webId],
|
||||
megaDebridAccountDailyLimitBytes: { [oldId]: 15 * GIB, [firstId]: 9 * GIB },
|
||||
megaDebridAccountDailyUsageBytes: { [oldId]: 4 * GIB, [firstId]: 2 * GIB },
|
||||
megaDebridAccountTotalUsageBytes: { [oldId]: 40 * GIB, [firstId]: 20 * GIB },
|
||||
debridAccountStatuses: {
|
||||
[oldId]: { accountId: oldId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 },
|
||||
[firstId]: { accountId: firstId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
...createAccountEditState(megaTarget("second@example.com"), settings),
|
||||
login: "renamed@example.com",
|
||||
password: "new-pass",
|
||||
dailyLimitGb: "25,5"
|
||||
};
|
||||
|
||||
expect(validateAccountEdit(state, settings)).toBeNull();
|
||||
const next = applyAccountEdit(settings, state);
|
||||
|
||||
expect(next.megaCredentials).toBe("first@example.com:first-pass\nrenamed@example.com:new-pass\nthird@example.com:third-pass\nweb@example.com:web-pass");
|
||||
expect(next.megaDebridApiCredentials).toBe("first@example.com:first-pass\nrenamed@example.com:new-pass\nthird@example.com:third-pass");
|
||||
expect(next.megaDebridWebCredentials).toBe("web@example.com:web-pass");
|
||||
expect(next.megaLogin).toBe("first@example.com");
|
||||
expect(next.megaPassword).toBe("first-pass");
|
||||
expect(next.megaDebridApiEnabled).toBe(true);
|
||||
expect(next.megaDebridWebEnabled).toBe(true);
|
||||
expect(next.megaDebridPreferApi).toBe(false);
|
||||
expect(next.megaDebridDisabledAccountIds).toEqual([firstId, newId, webId]);
|
||||
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [firstId]: 9 * GIB, [newId]: Math.floor(25.5 * GIB) });
|
||||
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [firstId]: 2 * GIB });
|
||||
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [firstId]: 20 * GIB });
|
||||
expect(next.debridAccountStatuses).toEqual({
|
||||
[firstId]: { accountId: firstId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Mega-Debrid usage when only the password changes", () => {
|
||||
const id = getMegaDebridAccountId("user@example.com");
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: "user@example.com:old-pass",
|
||||
megaLogin: "user@example.com",
|
||||
megaPassword: "old-pass",
|
||||
megaDebridAccountDailyLimitBytes: { [id]: 8 * GIB },
|
||||
megaDebridAccountDailyUsageBytes: { [id]: 3 * GIB },
|
||||
megaDebridAccountTotalUsageBytes: { [id]: 33 * GIB }
|
||||
};
|
||||
const state = { ...createAccountEditState(megaTarget("user@example.com"), settings), password: "new-pass" };
|
||||
const next = applyAccountEdit(settings, state);
|
||||
|
||||
expect(next.megaCredentials).toBe("user@example.com:new-pass");
|
||||
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [id]: 3 * GIB });
|
||||
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [id]: 33 * GIB });
|
||||
});
|
||||
|
||||
it("preserves exact account limits when the rounded display value is left unchanged", () => {
|
||||
const megaLogin = "user@example.com";
|
||||
const megaId = getMegaDebridAccountId(megaLogin);
|
||||
const key = "debrid-link-token";
|
||||
const keyId = getDebridLinkApiKeyId(key);
|
||||
const exactLimit = Math.floor(10.05 * GIB);
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: `${megaLogin}:pass`,
|
||||
megaLogin,
|
||||
megaPassword: "pass",
|
||||
megaDebridAccountDailyLimitBytes: { [megaId]: exactLimit },
|
||||
debridLinkApiKeys: key,
|
||||
debridLinkApiKeyDailyLimitBytes: { [keyId]: exactLimit }
|
||||
};
|
||||
|
||||
const megaNext = applyAccountEdit(settings, createAccountEditState(megaTarget(megaLogin), settings));
|
||||
const debridLinkNext = applyAccountEdit(settings, createAccountEditState(debridLinkTarget(key), settings));
|
||||
|
||||
expect(megaNext.megaDebridAccountDailyLimitBytes[megaId]).toBe(exactLimit);
|
||||
expect(debridLinkNext.debridLinkApiKeyDailyLimitBytes[keyId]).toBe(exactLimit);
|
||||
});
|
||||
|
||||
it("rejects whitespace-only Mega-Debrid passwords and empty or mismatched check results", () => {
|
||||
const login = "user@example.com";
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: `${login}:pass`,
|
||||
megaLogin: login,
|
||||
megaPassword: "pass"
|
||||
};
|
||||
const state = { ...createAccountEditState(megaTarget(login), settings), password: " " };
|
||||
|
||||
expect(validateAccountEdit(state, settings)).toMatch(/Passwort/i);
|
||||
expect(validateAccountEditStatuses(state, [])).toMatch(/keinen Account/i);
|
||||
expect(validateAccountEditStatuses(state, [{
|
||||
accountId: "mda_other",
|
||||
provider: "megadebrid",
|
||||
label: "Account 1",
|
||||
maskedLogin: "ot***er",
|
||||
valid: true,
|
||||
isPremium: true,
|
||||
premiumUntilMs: null,
|
||||
message: "OK",
|
||||
checkedAt: 1
|
||||
}])).toMatch(/falschen Account/i);
|
||||
});
|
||||
|
||||
it("rejects duplicate Mega-Debrid logins and missing row targets", () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass"
|
||||
};
|
||||
const duplicate = {
|
||||
...createAccountEditState(megaTarget("second@example.com"), settings),
|
||||
login: "first@example.com"
|
||||
};
|
||||
|
||||
expect(validateAccountEdit(duplicate, settings)).toMatch(/bereits vorhanden/i);
|
||||
expect(() => createAccountEditState(megaTarget("missing@example.com"), settings)).toThrow(/nicht gefunden/i);
|
||||
});
|
||||
|
||||
it("replaces only the selected Debrid-Link key and migrates its own metadata", () => {
|
||||
const keyA = "token-a";
|
||||
const keyB = "token-b";
|
||||
const keyC = "token-c";
|
||||
const newKey = "token-b-new";
|
||||
const idA = getDebridLinkApiKeyId(keyA);
|
||||
const idB = getDebridLinkApiKeyId(keyB);
|
||||
const idNew = getDebridLinkApiKeyId(newKey);
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
debridLinkApiKeys: `${keyA}\n${keyB}\n${keyC}`,
|
||||
debridLinkDisabledKeyIds: [idB, idA],
|
||||
debridLinkApiKeyDailyLimitBytes: { [idA]: 5 * GIB, [idB]: 10 * GIB },
|
||||
debridLinkApiKeyDailyUsageBytes: { [idA]: 2 * GIB, [idB]: 4 * GIB },
|
||||
debridLinkApiKeyTotalUsageBytes: { [idA]: 12 * GIB, [idB]: 24 * GIB }
|
||||
};
|
||||
const state = {
|
||||
...createAccountEditState(debridLinkTarget(keyB), settings),
|
||||
token: newKey,
|
||||
dailyLimitGb: "12"
|
||||
};
|
||||
const next = applyAccountEdit(settings, state);
|
||||
|
||||
expect(next.debridLinkApiKeys).toBe(`${keyA}\n${newKey}\n${keyC}`);
|
||||
expect(next.debridLinkDisabledKeyIds).toEqual([idA, idNew]);
|
||||
expect(next.debridLinkApiKeyDailyLimitBytes).toEqual({ [idA]: 5 * GIB, [idNew]: 12 * GIB });
|
||||
expect(next.debridLinkApiKeyDailyUsageBytes).toEqual({ [idA]: 2 * GIB });
|
||||
expect(next.debridLinkApiKeyTotalUsageBytes).toEqual({ [idA]: 12 * GIB });
|
||||
});
|
||||
|
||||
it("edits a single login without changing unrelated credentials", () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
ddownloadLogin: "old@example.com",
|
||||
ddownloadPassword: "old-pass",
|
||||
linkSnappyLogin: "keep@example.com",
|
||||
linkSnappyPassword: "keep-pass"
|
||||
};
|
||||
const target: AccountEditTarget = {
|
||||
type: "single",
|
||||
rowKey: "svc-ddownload",
|
||||
kind: "ddownload-login",
|
||||
service: "ddownload",
|
||||
provider: "ddownload"
|
||||
};
|
||||
const state = {
|
||||
...createAccountEditState(target, settings),
|
||||
login: "new@example.com",
|
||||
password: "new-pass",
|
||||
dailyLimitGb: "7"
|
||||
};
|
||||
const next = applyAccountEdit(settings, state);
|
||||
|
||||
expect(next.ddownloadLogin).toBe("new@example.com");
|
||||
expect(next.ddownloadPassword).toBe("new-pass");
|
||||
expect(next.linkSnappyLogin).toBe("keep@example.com");
|
||||
expect(next.linkSnappyPassword).toBe("keep-pass");
|
||||
expect(next.providerDailyLimitBytes.ddownload).toBe(7 * GIB);
|
||||
});
|
||||
|
||||
it("rejects whitespace-only passwords for direct login accounts", () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
ddownloadLogin: "user@example.com",
|
||||
ddownloadPassword: "password",
|
||||
linkSnappyLogin: "member@example.com",
|
||||
linkSnappyPassword: "secret"
|
||||
};
|
||||
const ddownloadTarget: AccountEditTarget = {
|
||||
type: "single",
|
||||
rowKey: "svc-ddownload",
|
||||
kind: "ddownload-login",
|
||||
service: "ddownload",
|
||||
provider: "ddownload"
|
||||
};
|
||||
const linkSnappyTarget: AccountEditTarget = {
|
||||
type: "single",
|
||||
rowKey: "svc-linksnappy",
|
||||
kind: "linksnappy-login",
|
||||
service: "linksnappy",
|
||||
provider: "linksnappy"
|
||||
};
|
||||
|
||||
expect(validateAccountEdit({ ...createAccountEditState(ddownloadTarget, settings), password: " " }, settings)).toMatch(/Passwort/i);
|
||||
expect(validateAccountEdit({ ...createAccountEditState(linkSnappyTarget, settings), password: "\t" }, settings)).toMatch(/Passwort/i);
|
||||
});
|
||||
|
||||
it("builds a targeted check snapshot without invalid sibling accounts", () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
|
||||
megaLogin: "first@example.com",
|
||||
megaPassword: "first-pass",
|
||||
debridLinkApiKeys: "one\ntwo"
|
||||
};
|
||||
const target = megaTarget("second@example.com");
|
||||
const state = createAccountEditState(target, settings);
|
||||
const checkSettings = buildAccountEditCheckSettings(settings, state);
|
||||
|
||||
expect(checkSettings.megaCredentials).toBe("second@example.com:second-pass");
|
||||
expect(checkSettings.megaLogin).toBe("second@example.com");
|
||||
expect(checkSettings.megaPassword).toBe("second-pass");
|
||||
expect(checkSettings.debridLinkApiKeys).toBe("");
|
||||
});
|
||||
|
||||
it("removes only the selected Mega-Debrid account and all metadata belonging to it", () => {
|
||||
const removeId = getMegaDebridAccountId("second@example.com");
|
||||
const keepId = getMegaDebridAccountId("first@example.com");
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
|
||||
megaLogin: "first@example.com",
|
||||
megaPassword: "first-pass",
|
||||
megaDebridDisabledAccountIds: [removeId, keepId],
|
||||
megaDebridAccountDailyLimitBytes: { [removeId]: 2, [keepId]: 1 },
|
||||
megaDebridAccountDailyUsageBytes: { [removeId]: 4, [keepId]: 3 },
|
||||
megaDebridAccountTotalUsageBytes: { [removeId]: 6, [keepId]: 5 },
|
||||
debridAccountStatuses: {
|
||||
[removeId]: { accountId: removeId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: false, premiumUntilMs: null, message: "Free", checkedAt: 1 },
|
||||
[keepId]: { accountId: keepId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||
}
|
||||
};
|
||||
const next = removeAccountTarget(settings, megaTarget("second@example.com"));
|
||||
|
||||
expect(next.megaCredentials).toBe("first@example.com:first-pass");
|
||||
expect(next.megaDebridDisabledAccountIds).toEqual([keepId]);
|
||||
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [keepId]: 1 });
|
||||
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [keepId]: 3 });
|
||||
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [keepId]: 5 });
|
||||
expect(next.debridAccountStatuses).toEqual({
|
||||
[keepId]: { accountId: keepId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||
});
|
||||
});
|
||||
});
|
||||
megaCredentials: `${identity}:fixture-stored-secret-4dH8`,
|
||||
megaDebridApiCredentials: `${identity}:fixture-stored-secret-4dH8`,
|
||||
megaDebridApiEnabled: true
|
||||
});
|
||||
|
||||
const edit = createAccountEditState(megaTarget(identity), state.accounts);
|
||||
|
||||
expect(edit.login).toBe(identity);
|
||||
expect(edit.password).toBe("");
|
||||
expect(JSON.stringify(edit)).not.toContain("fixture-stored-secret-4dH8");
|
||||
});
|
||||
|
||||
it("builds a replace command whose blank secret retains the main-process value", () => {
|
||||
const identity = "safe-edit@example.test";
|
||||
const renderer = createRendererState({
|
||||
...defaultSettings(),
|
||||
megaCredentials: `${identity}:fixture-stored-secret-4dH8`,
|
||||
megaDebridApiCredentials: `${identity}:fixture-stored-secret-4dH8`,
|
||||
megaDebridApiEnabled: true
|
||||
});
|
||||
const edit = createAccountEditState(megaTarget(identity), renderer.accounts);
|
||||
|
||||
expect(validateAccountEdit(edit, renderer.accounts)).toBeNull();
|
||||
expect(buildAccountReplaceCommand(edit)).toEqual(expect.objectContaining({
|
||||
action: "replace",
|
||||
accountId: getMegaDebridAccountId(identity),
|
||||
identity,
|
||||
secret: ""
|
||||
}));
|
||||
});
|
||||
|
||||
it("rejects duplicate identities using safe account metadata", () => {
|
||||
const first = "first-safe@example.test";
|
||||
const second = "second-safe@example.test";
|
||||
const renderer = createRendererState({
|
||||
...defaultSettings(),
|
||||
megaCredentials: `${first}:fixture-first-secret-1aB2\n${second}:fixture-second-secret-3cD4`,
|
||||
megaDebridApiCredentials: `${first}:fixture-first-secret-1aB2\n${second}:fixture-second-secret-3cD4`,
|
||||
megaDebridApiEnabled: true
|
||||
});
|
||||
const edit = { ...createAccountEditState(megaTarget(second), renderer.accounts), login: first };
|
||||
|
||||
expect(validateAccountEdit(edit, renderer.accounts)).toMatch(/bereits vorhanden/i);
|
||||
});
|
||||
|
||||
it("builds an identity-only delete command", () => {
|
||||
const target = megaTarget("delete-safe@example.test");
|
||||
expect(buildAccountDeleteCommand(target)).toEqual({
|
||||
action: "delete",
|
||||
kind: "megadebrid-api",
|
||||
accountId: target.type === "mega" ? target.accountId : ""
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { IPC_CHANNELS } from "../src/shared/ipc";
|
||||
import type { ElectronApi } from "../src/shared/preload-api";
|
||||
|
||||
const electron = vi.hoisted(() => ({
|
||||
api: undefined as ElectronApi | undefined,
|
||||
invoke: vi.fn<(...args: unknown[]) => Promise<unknown>>(async () => undefined)
|
||||
}));
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
contextBridge: {
|
||||
exposeInMainWorld: (_name: string, api: ElectronApi) => {
|
||||
electron.api = api;
|
||||
}
|
||||
},
|
||||
ipcRenderer: {
|
||||
invoke: electron.invoke,
|
||||
on: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
send: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
describe("account preload contract", () => {
|
||||
beforeAll(async () => {
|
||||
await import("../src/preload/preload");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
electron.invoke.mockClear();
|
||||
});
|
||||
|
||||
it("forwards submitted secrets only in write-only account commands", async () => {
|
||||
const secret = "fixture-preload-secret-5zK1";
|
||||
electron.invoke.mockResolvedValueOnce({ accountId: "mda_fixture", settings: { language: "de" }, accounts: [] });
|
||||
const result = await electron.api?.createAccount({
|
||||
action: "create",
|
||||
kind: "megadebrid-api",
|
||||
identity: "preload-account@example.test",
|
||||
secret,
|
||||
dailyLimitBytes: 0
|
||||
});
|
||||
|
||||
expect(electron.invoke).toHaveBeenCalledWith(
|
||||
IPC_CHANNELS.CREATE_ACCOUNT,
|
||||
expect.objectContaining({ secret })
|
||||
);
|
||||
expect(JSON.stringify(result)).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("exposes separate replace, update-secret and delete channels", async () => {
|
||||
electron.invoke.mockResolvedValue({ accountId: "mda_fixture", settings: { language: "de" }, accounts: [] });
|
||||
|
||||
await electron.api?.replaceAccount({ action: "replace", kind: "megadebrid-api", accountId: "mda_fixture", identity: "account@example.test", secret: "", dailyLimitBytes: 0 });
|
||||
await electron.api?.updateAccountSecret({ action: "update-secret", kind: "megadebrid-api", accountId: "mda_fixture", secret: "fixture-new-secret-8sP4" });
|
||||
await electron.api?.deleteAccount({ action: "delete", kind: "megadebrid-api", accountId: "mda_fixture" });
|
||||
|
||||
expect(electron.invoke.mock.calls.map((call) => call[0])).toEqual([
|
||||
IPC_CHANNELS.REPLACE_ACCOUNT,
|
||||
IPC_CHANNELS.UPDATE_ACCOUNT_SECRET,
|
||||
IPC_CHANNELS.DELETE_ACCOUNT
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { once } from "node:events";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../src/main/windows-host-diagnostics", () => ({
|
||||
getWindowsHostDiagnostics: () => ({
|
||||
@@ -40,7 +40,9 @@ vi.mock("../src/main/windows-host-diagnostics", () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { configureCredentialProtector } from "../src/main/credential-protection";
|
||||
import { createRendererState } from "../src/main/renderer-state";
|
||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "../src/main/audit-log";
|
||||
import { startDebugServer, stopDebugServer } from "../src/main/debug-server";
|
||||
import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
||||
@@ -70,6 +72,14 @@ const forbiddenSupportMarkers = [
|
||||
["M", "C", "P"].join(""),
|
||||
["K", "I"].join("")
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
configureCredentialProtector({
|
||||
isEncryptionAvailable: () => true,
|
||||
encryptString: (value) => Buffer.from(value, "utf8").reverse(),
|
||||
decryptString: (value) => Buffer.from(value).reverse().toString("utf8")
|
||||
});
|
||||
});
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
const probe = http.createServer();
|
||||
@@ -99,15 +109,16 @@ async function waitForReady(url: string): Promise<void> {
|
||||
throw new Error(`debug server not ready: ${url}`);
|
||||
}
|
||||
|
||||
function buildSnapshot(baseDir: string): UiSnapshot {
|
||||
function buildSnapshot(baseDir: string): UiSnapshot {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
outputDir: path.join(baseDir, "downloads"),
|
||||
extractDir: path.join(baseDir, "extract")
|
||||
};
|
||||
|
||||
return {
|
||||
settings,
|
||||
const renderer = createRendererState(settings);
|
||||
return {
|
||||
...renderer,
|
||||
session: {
|
||||
version: 1,
|
||||
packageOrder: ["pkg-1"],
|
||||
@@ -220,8 +231,10 @@ async function createFixture() {
|
||||
const debridLinkApiKeys = "key-a\nkey-b";
|
||||
const debridLinkKeyIds = getDebridLinkApiKeyIds(debridLinkApiKeys);
|
||||
|
||||
saveSettings(storagePaths, {
|
||||
...snapshot.settings,
|
||||
saveSettings(storagePaths, {
|
||||
...defaultSettings(),
|
||||
outputDir: snapshot.settings.outputDir,
|
||||
extractDir: snapshot.settings.extractDir,
|
||||
token: "rd-secret-token",
|
||||
realDebridUseWebLogin: true,
|
||||
debridLinkApiKeys,
|
||||
|
||||
@@ -4,8 +4,9 @@ import { parseCollectorInput } from "../src/main/link-parser";
|
||||
import type { UiSnapshot } from "../src/shared/types";
|
||||
|
||||
function buildSnapshot(): UiSnapshot {
|
||||
return {
|
||||
settings: {} as UiSnapshot["settings"],
|
||||
return {
|
||||
settings: {} as UiSnapshot["settings"],
|
||||
accounts: [],
|
||||
session: {
|
||||
version: 1,
|
||||
packageOrder: ["pkg-1", "pkg-2"],
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { createRendererState } from "../src/main/renderer-state";
|
||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import type { AppSettings, RendererAccountKind } from "../src/shared/types";
|
||||
|
||||
const SECRETS = {
|
||||
token: "fixture-rd-token-7vQ2",
|
||||
megaPassword: "fixture-mega-password-8kM3",
|
||||
bestToken: "fixture-best-token-5xL9",
|
||||
allDebridToken: "fixture-ad-token-4pR6",
|
||||
ddownloadPassword: "fixture-dd-password-2tN8",
|
||||
oneFichierApiKey: "fixture-onefichier-key-6cW1",
|
||||
debridLinkApiKey: "fixture-debridlink-key-9hS4",
|
||||
linkSnappyPassword: "fixture-linksnappy-password-3mB7",
|
||||
archivePassword: "fixture-archive-password-1jD5",
|
||||
notifyUrl: "https://notify.example.test/hooks/fixture-notify-secret-0fA2"
|
||||
} as const;
|
||||
|
||||
const ACCOUNT_FIXTURES: Array<{
|
||||
kind: RendererAccountKind;
|
||||
secret: string;
|
||||
settings: Partial<AppSettings>;
|
||||
}> = [
|
||||
{ kind: "realdebrid-api", secret: "fixture-rd-api-secret-1aK4", settings: { token: "fixture-rd-api-secret-1aK4" } },
|
||||
{ kind: "realdebrid-web", secret: "fixture-rd-web-session-2bL5", settings: { token: "fixture-rd-web-session-2bL5", realDebridUseWebLogin: true } },
|
||||
{ kind: "megadebrid-api", secret: "fixture-mega-api-secret-3cM6", settings: { megaCredentials: "mega-api@example.test:fixture-mega-api-secret-3cM6", megaDebridApiCredentials: "mega-api@example.test:fixture-mega-api-secret-3cM6", megaDebridApiEnabled: true } },
|
||||
{ kind: "megadebrid-web", secret: "fixture-mega-web-secret-4dN7", settings: { megaCredentials: "mega-web@example.test:fixture-mega-web-secret-4dN7", megaDebridWebCredentials: "mega-web@example.test:fixture-mega-web-secret-4dN7", megaDebridWebEnabled: true } },
|
||||
{ kind: "bestdebrid-api", secret: "fixture-best-api-secret-5eP8", settings: { bestToken: "fixture-best-api-secret-5eP8" } },
|
||||
{ kind: "bestdebrid-web", secret: "fixture-best-web-session-6fQ9", settings: { bestToken: "fixture-best-web-session-6fQ9", bestDebridUseWebLogin: true } },
|
||||
{ kind: "alldebrid-api", secret: "fixture-all-api-secret-7gR1", settings: { allDebridToken: "fixture-all-api-secret-7gR1" } },
|
||||
{ kind: "alldebrid-web", secret: "fixture-all-web-session-8hS2", settings: { allDebridToken: "fixture-all-web-session-8hS2", allDebridUseWebLogin: true } },
|
||||
{ kind: "ddownload-login", secret: "fixture-dd-secret-9jT3", settings: { ddownloadLogin: "dd@example.test", ddownloadPassword: "fixture-dd-secret-9jT3" } },
|
||||
{ kind: "onefichier-api", secret: "fixture-one-secret-0kU4", settings: { oneFichierApiKey: "fixture-one-secret-0kU4" } },
|
||||
{ kind: "debridlink-api", secret: "fixture-dl-secret-1mV5", settings: { debridLinkApiKeys: "fixture-dl-secret-1mV5" } },
|
||||
{ kind: "linksnappy-login", secret: "fixture-ls-secret-2nW6", settings: { linkSnappyLogin: "ls@example.test", linkSnappyPassword: "fixture-ls-secret-2nW6" } }
|
||||
];
|
||||
|
||||
describe("renderer state serialization", () => {
|
||||
it.each(ACCOUNT_FIXTURES)("serializes $kind without its representative secret", ({ kind, secret, settings }) => {
|
||||
const state = createRendererState({ ...defaultSettings(), ...settings });
|
||||
|
||||
expect(state.accounts).toEqual(expect.arrayContaining([expect.objectContaining({ kind, hasSecret: true })]));
|
||||
expect(JSON.stringify(state)).not.toContain(secret);
|
||||
expect(JSON.stringify(state.settings)).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("excludes every provider and settings secret while preserving safe account metadata", () => {
|
||||
const megaLogin = "renderer-fixture@example.test";
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: SECRETS.token,
|
||||
megaLogin,
|
||||
megaPassword: SECRETS.megaPassword,
|
||||
megaCredentials: `${megaLogin}:${SECRETS.megaPassword}`,
|
||||
megaDebridApiCredentials: `${megaLogin}:${SECRETS.megaPassword}`,
|
||||
megaDebridApiEnabled: true,
|
||||
bestToken: SECRETS.bestToken,
|
||||
allDebridToken: SECRETS.allDebridToken,
|
||||
ddownloadLogin: "renderer-dd@example.test",
|
||||
ddownloadPassword: SECRETS.ddownloadPassword,
|
||||
oneFichierApiKey: SECRETS.oneFichierApiKey,
|
||||
debridLinkApiKeys: SECRETS.debridLinkApiKey,
|
||||
linkSnappyLogin: "renderer-linksnappy@example.test",
|
||||
linkSnappyPassword: SECRETS.linkSnappyPassword,
|
||||
archivePasswordList: SECRETS.archivePassword,
|
||||
notifyUrl: SECRETS.notifyUrl
|
||||
};
|
||||
|
||||
const state = createRendererState(settings);
|
||||
const serialized = JSON.stringify(state);
|
||||
|
||||
for (const secret of Object.values(SECRETS)) {
|
||||
expect(serialized).not.toContain(secret);
|
||||
}
|
||||
expect(state.settings.archivePasswordListConfigured).toBe(true);
|
||||
expect(state.settings.notifyUrlConfigured).toBe(true);
|
||||
expect(state.accounts).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "realdebrid-api", hasSecret: true }),
|
||||
expect.objectContaining({ kind: "megadebrid-api", identity: megaLogin, hasSecret: true }),
|
||||
expect.objectContaining({ kind: "debridlink-api", hasSecret: true })
|
||||
]));
|
||||
});
|
||||
|
||||
it("redacts individual secrets embedded in status metadata for multi-account pools", () => {
|
||||
const firstMegaSecret = "fixture-first-mega-pool-secret-3pX7";
|
||||
const secondMegaSecret = "fixture-second-mega-pool-secret-4qY8";
|
||||
const firstKey = "fixture-first-debridlink-pool-secret-5rZ9";
|
||||
const secondKey = "fixture-second-debridlink-pool-secret-6sA1";
|
||||
const secondMegaId = getMegaDebridAccountId("second-pool@example.test");
|
||||
const secondKeyId = getDebridLinkApiKeyId(secondKey);
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
megaCredentials: `first-pool@example.test:${firstMegaSecret}\nsecond-pool@example.test:${secondMegaSecret}`,
|
||||
megaDebridApiCredentials: `first-pool@example.test:${firstMegaSecret}\nsecond-pool@example.test:${secondMegaSecret}`,
|
||||
megaDebridApiEnabled: true,
|
||||
debridLinkApiKeys: `${firstKey}\n${secondKey}`,
|
||||
debridAccountStatuses: {
|
||||
[secondMegaId]: {
|
||||
accountId: secondMegaId,
|
||||
provider: "megadebrid" as const,
|
||||
label: "Account 2",
|
||||
maskedLogin: "se***st",
|
||||
valid: false,
|
||||
isPremium: false,
|
||||
premiumUntilMs: null,
|
||||
message: `Rejected ${secondMegaSecret}`,
|
||||
checkedAt: 1
|
||||
},
|
||||
[secondKeyId]: {
|
||||
accountId: secondKeyId,
|
||||
provider: "debridlink" as const,
|
||||
label: "Key 2",
|
||||
maskedLogin: "fi***A1",
|
||||
valid: false,
|
||||
isPremium: false,
|
||||
premiumUntilMs: null,
|
||||
message: `Rejected ${secondKey}`,
|
||||
checkedAt: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const serialized = JSON.stringify(createRendererState(settings));
|
||||
|
||||
for (const secret of [firstMegaSecret, secondMegaSecret, firstKey, secondKey]) {
|
||||
expect(serialized).not.toContain(secret);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -3,12 +3,9 @@ import { isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { createRendererSettings, createRendererState } from "../src/main/renderer-state";
|
||||
import { buildAccountAddFields, createAccountDialogState } from "../src/renderer/App";
|
||||
import {
|
||||
applyAccountEdit,
|
||||
createAccountEditState,
|
||||
type AccountEditTarget
|
||||
} from "../src/renderer/account-edit";
|
||||
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
|
||||
import {
|
||||
buildBulkAccountEnabledState,
|
||||
buildConfiguredProviderOrder
|
||||
@@ -416,7 +413,6 @@ describe("settings model", () => {
|
||||
const login = "member@example.test";
|
||||
const oldId = getMegaDebridAccountId(login);
|
||||
const newLogin = "renamed@example.test";
|
||||
const newId = getMegaDebridAccountId(newLogin);
|
||||
const exactLimit = Math.floor(10.05 * GIB);
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
@@ -435,17 +431,17 @@ describe("settings model", () => {
|
||||
service: "megadebrid-api",
|
||||
accountId: oldId
|
||||
};
|
||||
const unchanged = applyAccountEdit(settings, createAccountEditState(target, settings));
|
||||
const renamed = applyAccountEdit(settings, {
|
||||
...createAccountEditState(target, settings),
|
||||
const renderer = createRendererState(settings);
|
||||
const unchanged = buildAccountReplaceCommand(createAccountEditState(target, renderer.accounts));
|
||||
const renamed = buildAccountReplaceCommand({
|
||||
...createAccountEditState(target, renderer.accounts),
|
||||
login: newLogin
|
||||
});
|
||||
|
||||
expect(unchanged.megaDebridAccountDailyLimitBytes[oldId]).toBe(exactLimit);
|
||||
expect(renamed.megaDebridDisabledAccountIds).toEqual([newId]);
|
||||
expect(renamed.megaDebridAccountDailyLimitBytes[newId]).toBe(exactLimit);
|
||||
expect(renamed.megaDebridAccountDailyUsageBytes[newId]).toBeUndefined();
|
||||
expect(renamed.megaDebridAccountTotalUsageBytes[newId]).toBeUndefined();
|
||||
expect(unchanged.dailyLimitBytes).toBe(exactLimit);
|
||||
expect(unchanged.secret).toBe("");
|
||||
expect(renamed.identity).toBe(newLogin);
|
||||
expect(renamed.secret).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -468,7 +464,7 @@ describe("settings views", () => {
|
||||
|
||||
it("offers animated language and bounded history retention choices", () => {
|
||||
const form = buildSettingsFormViewModel({
|
||||
settings: defaultSettings(),
|
||||
settings: { ...createRendererSettings(defaultSettings()), archivePasswordList: "", notifyUrl: "" },
|
||||
section: "allgemein",
|
||||
speedLimitInput: "0",
|
||||
scheduleSpeedInputs: {}
|
||||
@@ -912,9 +908,10 @@ describe("settings App integration", () => {
|
||||
megaCredentials: "first@example.test:first-secret\nsecond@example.test:second-secret",
|
||||
debridLinkApiKeys: "existing-debrid-link-key"
|
||||
};
|
||||
const megaDialog = createAccountDialogState("create", "megadebrid-api", settings);
|
||||
const rendererSettings = createRendererSettings(settings);
|
||||
const megaDialog = createAccountDialogState("create", "megadebrid-api", rendererSettings);
|
||||
const megaFields = buildAccountAddFields(megaDialog);
|
||||
const debridLinkFields = buildAccountAddFields(createAccountDialogState("create", "debridlink-api", settings));
|
||||
const debridLinkFields = buildAccountAddFields(createAccountDialogState("create", "debridlink-api", rendererSettings));
|
||||
|
||||
expect(megaDialog.megaNewLogin).toBe("");
|
||||
expect(megaDialog.megaNewPassword).toBe("");
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { type ReactElement } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { App } from "../src/renderer/App";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import type { ElectronApi } from "../src/shared/preload-api";
|
||||
import * as visualFixtures from "./visual/fixtures";
|
||||
import * as visualMain from "./visual/main";
|
||||
@@ -136,11 +134,12 @@ describe("visual fixtures", () => {
|
||||
it("aligns dense account table values with credential-derived account IDs", async () => {
|
||||
const dense = createVisualFixture("dense");
|
||||
const settings = dense.snapshot.settings;
|
||||
const megaAccountId = getMegaDebridAccountId(settings.megaLogin);
|
||||
const debridLinkKeys = parseDebridLinkApiKeys(settings.debridLinkApiKeys);
|
||||
const megaAccount = dense.snapshot.accounts.find((account) => account.kind === "megadebrid-api");
|
||||
const debridLinkKeys = dense.snapshot.accounts.filter((account) => account.kind === "debridlink-api");
|
||||
const megaAccountId = megaAccount?.accountId || "";
|
||||
|
||||
expect(megaAccountId).toBe("mda_2f92guyzhdf6j");
|
||||
expect(debridLinkKeys.map((entry) => entry.id)).toEqual([
|
||||
expect(debridLinkKeys.map((entry) => entry.accountId)).toEqual([
|
||||
"dlk_1ix5qlyx6mtm1",
|
||||
"dlk_1ix5pfvlg4nkg"
|
||||
]);
|
||||
@@ -152,23 +151,23 @@ describe("visual fixtures", () => {
|
||||
);
|
||||
|
||||
for (const entry of debridLinkKeys) {
|
||||
expect(settings.debridAccountStatuses[entry.id]?.valid).toBe(true);
|
||||
expect(settings.debridLinkApiKeyDailyLimitBytes[entry.id]).toBeGreaterThan(0);
|
||||
expect(settings.debridLinkApiKeyDailyUsageBytes[entry.id]).toBeGreaterThan(0);
|
||||
expect(settings.debridLinkApiKeyDailyUsageBytes[entry.id]).toBeLessThan(
|
||||
settings.debridLinkApiKeyDailyLimitBytes[entry.id]
|
||||
expect(settings.debridAccountStatuses[entry.accountId]?.valid).toBe(true);
|
||||
expect(settings.debridLinkApiKeyDailyLimitBytes[entry.accountId]).toBeGreaterThan(0);
|
||||
expect(settings.debridLinkApiKeyDailyUsageBytes[entry.accountId]).toBeGreaterThan(0);
|
||||
expect(settings.debridLinkApiKeyDailyUsageBytes[entry.accountId]).toBeLessThan(
|
||||
settings.debridLinkApiKeyDailyLimitBytes[entry.accountId]
|
||||
);
|
||||
expect(settings.debridLinkApiKeyTotalUsageBytes[entry.id]).toBeGreaterThan(
|
||||
settings.debridLinkApiKeyDailyUsageBytes[entry.id]
|
||||
expect(settings.debridLinkApiKeyTotalUsageBytes[entry.accountId]).toBeGreaterThan(
|
||||
settings.debridLinkApiKeyDailyUsageBytes[entry.accountId]
|
||||
);
|
||||
}
|
||||
|
||||
const debridLinkItem = Object.values(dense.snapshot.session.items).find(
|
||||
(item) => item.provider === "debridlink"
|
||||
);
|
||||
expect(debridLinkItem?.providerAccountId).toBe(debridLinkKeys[0].id);
|
||||
expect(debridLinkItem?.providerAccountId).toBe(debridLinkKeys[0].accountId);
|
||||
const hostLimits = await createVisualElectronApi(dense).getDebridLinkHostLimits();
|
||||
expect(hostLimits[0]?.keyId).toBe(debridLinkKeys[0].id);
|
||||
expect(hostLimits[0]?.keyId).toBe(debridLinkKeys[0].accountId);
|
||||
});
|
||||
|
||||
it("stores every mutable bridge state inside the visual fixture", async () => {
|
||||
|
||||
@@ -7,7 +7,8 @@ import type {
|
||||
UpdateCheckResult
|
||||
} from "../../src/shared/types";
|
||||
import { parseDebridLinkApiKeys } from "../../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../../src/shared/mega-debrid-accounts";
|
||||
import { getMegaDebridAccountId } from "../../src/shared/mega-debrid-accounts";
|
||||
import { createRendererState } from "../../src/main/renderer-state";
|
||||
|
||||
export const VISUAL_SCENARIOS = ["empty", "dense", "update"] as const;
|
||||
|
||||
@@ -228,9 +229,10 @@ function createSettings(): AppSettings {
|
||||
};
|
||||
}
|
||||
|
||||
function createEmptySnapshot(): UiSnapshot {
|
||||
return {
|
||||
settings: createSettings(),
|
||||
function createEmptySnapshot(): UiSnapshot {
|
||||
const renderer = createRendererState(createSettings());
|
||||
return {
|
||||
...renderer,
|
||||
session: {
|
||||
version: 1,
|
||||
packageOrder: [],
|
||||
@@ -274,9 +276,9 @@ function createEmptySnapshot(): UiSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
function createDenseSnapshot(): UiSnapshot {
|
||||
const snapshot = createEmptySnapshot();
|
||||
const debridLinkKeys = parseDebridLinkApiKeys(snapshot.settings.debridLinkApiKeys);
|
||||
function createDenseSnapshot(): UiSnapshot {
|
||||
const snapshot = createEmptySnapshot();
|
||||
const debridLinkKeys = snapshot.accounts.filter((account) => account.kind === "debridlink-api");
|
||||
snapshot.session = {
|
||||
version: 1,
|
||||
packageOrder: ["visual-package-active", "visual-package-complete", "visual-package-failed"],
|
||||
@@ -359,7 +361,7 @@ function createDenseSnapshot(): UiSnapshot {
|
||||
url: "https://ddownload.com/visual-active-2",
|
||||
provider: "debridlink",
|
||||
providerLabel: "Debrid-Link",
|
||||
providerAccountId: debridLinkKeys[0].id,
|
||||
providerAccountId: debridLinkKeys[0].accountId,
|
||||
providerAccountLabel: "Debrid-Link Key 1",
|
||||
status: "queued",
|
||||
retries: 0,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ElectronApi } from "../../src/shared/preload-api";
|
||||
import type { AppSettings, HistoryEntry } from "../../src/shared/types";
|
||||
import { parseDebridLinkApiKeys } from "../../src/shared/debrid-link-keys";
|
||||
import type { HistoryEntry, RendererSettings, RendererSettingsUpdate } from "../../src/shared/types";
|
||||
import type { VisualFixture } from "./fixtures";
|
||||
|
||||
const stableNoopUnsubscribe = (): void => {};
|
||||
@@ -15,8 +14,9 @@ export function createVisualElectronApi(
|
||||
): ElectronApi {
|
||||
const historyState = new URLSearchParams(search).get("history-state");
|
||||
let historyRequestCount = 0;
|
||||
const updateSettings = (settings: Partial<AppSettings>): AppSettings => {
|
||||
Object.assign(fixture.snapshot.settings, settings);
|
||||
const updateSettings = (settings: RendererSettingsUpdate): RendererSettings => {
|
||||
const { archivePasswordList: _archivePasswordList, notifyUrl: _notifyUrl, ...safe } = settings;
|
||||
Object.assign(fixture.snapshot.settings, safe);
|
||||
return clone(fixture.snapshot.settings);
|
||||
};
|
||||
|
||||
@@ -35,6 +35,10 @@ export function createVisualElectronApi(
|
||||
fixture.snapshot.settings.debridLinkApiKeyDailyUsageBytes[keyId] = 0;
|
||||
return clone(fixture.snapshot.settings);
|
||||
},
|
||||
createAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
|
||||
replaceAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
|
||||
updateAccountSecret: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
|
||||
deleteAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
|
||||
addLinks: async () => ({ addedPackages: 0, addedLinks: 0, invalidCount: 0 }),
|
||||
addContainers: async () => ({ addedPackages: 0, addedLinks: 0 }),
|
||||
getStartConflicts: async () => [],
|
||||
@@ -291,10 +295,11 @@ export function createVisualElectronApi(
|
||||
note: "Visual host status"
|
||||
}),
|
||||
getDebridLinkHostLimits: async () => {
|
||||
const primaryKey = parseDebridLinkApiKeys(fixture.snapshot.settings.debridLinkApiKeys)[0];
|
||||
const primaryKey = fixture.snapshot.accounts.find((account) => account.kind === "debridlink-api");
|
||||
if (!primaryKey) return [];
|
||||
return [{
|
||||
keyId: primaryKey.id,
|
||||
keyLabel: primaryKey.label,
|
||||
keyId: primaryKey.accountId,
|
||||
keyLabel: "Key 1",
|
||||
host: "ddownload.com",
|
||||
fetchedAt: 1786312800000,
|
||||
trafficCurrentBytes: 53687091200,
|
||||
@@ -314,12 +319,17 @@ export function createVisualElectronApi(
|
||||
}];
|
||||
},
|
||||
checkDebridAccounts: async () => clone(Object.values(fixture.snapshot.settings.debridAccountStatuses)),
|
||||
checkMegaDebridAccount: async () => {
|
||||
const status = Object.values(fixture.snapshot.settings.debridAccountStatuses).find(
|
||||
(entry) => entry.provider === "megadebrid"
|
||||
);
|
||||
return status ? clone(status) : null;
|
||||
},
|
||||
checkAccountCredentials: async (input) => clone(fixture.snapshot.accounts.find((account) => account.accountId === input.accountId)?.status || {
|
||||
accountId: input.accountId || "visual-account",
|
||||
provider: input.kind === "debridlink-api" ? "debridlink" : "megadebrid",
|
||||
label: "Visual Account",
|
||||
maskedLogin: "vi***al",
|
||||
valid: true,
|
||||
isPremium: true,
|
||||
premiumUntilMs: null,
|
||||
message: "Premium aktiv",
|
||||
checkedAt: 1786312800000
|
||||
}),
|
||||
retryExtraction: async (packageId) => {
|
||||
const entry = fixture.snapshot.session.packages[packageId];
|
||||
if (entry) {
|
||||
|
||||
Reference in New Issue
Block a user