Files
Multi-Debrid-Downloader/tests/account-commands.test.ts
Sucukdeluxe 1b7caba2eb feat: add Deepbrid, daily scheduling, and notification center
Add encrypted Deepbrid API accounts with account validation, provider routing, fallback, usage tracking, safe error handling, and verified 1Fichier downloads. Restore persistent recurring daily starts with local-calendar deduplication and legacy schedule compatibility. Add durable Discord package, run, remaining-volume, stall, and recovery notifications with privacy-safe telemetry and disk-failure recovery.
2026-08-24 07:37:55 +02:00

591 lines
30 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { applyAccountCommand, resolveStoredAccountSecret, setRealDebridAccountEnabled, validateAccountCommand, validateAccountCredentialCheckInput } from "../src/main/account-commands";
import * as accountCommands 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 { parseRealDebridApiAccounts, serializeRealDebridApiAccounts } from "../src/shared/real-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: "deepbrid-api", identity: "", secret: "fixture-retain-deepbrid-6fH8", retained: (settings) => settings.deepbridApiKey === "fixture-retain-deepbrid-6fH8" },
{ 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",
"deepbrid-api",
"ddownload-login",
"onefichier-api",
"debridlink-api",
"linksnappy-login"
];
describe("write-only account commands", () => {
it("manages multiple Real-Debrid API accounts without touching siblings", () => {
const firstToken = "fixture-rd-pool-first-1aB2";
const secondToken = "fixture-rd-pool-second-3cD4";
const replacementToken = "fixture-rd-pool-replacement-5eF6";
const first = applyAccountCommand(defaultSettings(), validateAccountCommand({ action: "create", kind: "realdebrid-api", secret: firstToken, dailyLimitBytes: 2 * GIB }));
const second = applyAccountCommand(first.settings, validateAccountCommand({ action: "create", kind: "realdebrid-api", secret: secondToken, dailyLimitBytes: 3 * GIB }));
expect(parseRealDebridApiAccounts(second.settings.realDebridApiTokens).map((entry) => entry.token)).toEqual([firstToken, secondToken]);
expect(second.response.accountId).toMatch(/^rda_[A-Za-z0-9_-]+$/);
expect(() => applyAccountCommand(second.settings, validateAccountCommand({ action: "create", kind: "realdebrid-api", secret: secondToken }))).toThrow(/ungültig/i);
expect(resolveStoredAccountSecret(second.settings, { kind: "realdebrid-api", accountId: first.response.accountId! })).toBe(firstToken);
expect(resolveStoredAccountSecret(second.settings, { kind: "realdebrid-api", accountId: second.response.accountId! })).toBe(secondToken);
const replaced = applyAccountCommand(second.settings, validateAccountCommand({
action: "replace",
kind: "realdebrid-api",
accountId: second.response.accountId,
secret: replacementToken,
dailyLimitBytes: 4 * GIB
}));
const replacementId = second.response.accountId!;
expect(parseRealDebridApiAccounts(replaced.settings.realDebridApiTokens).map((entry) => entry.token)).toEqual([firstToken, replacementToken]);
expect(replaced.settings.realDebridAccountDailyLimitBytes).toEqual({ [first.response.accountId!]: 2 * GIB, [replacementId]: 4 * GIB });
const deleted = applyAccountCommand(replaced.settings, validateAccountCommand({ action: "delete", kind: "realdebrid-api", accountId: first.response.accountId }));
expect(parseRealDebridApiAccounts(deleted.settings.realDebridApiTokens).map((entry) => entry.token)).toEqual([replacementToken]);
expect(deleted.response.accountId).toBe(replacementId);
});
it("creates, updates and deletes one Real-Debrid Web account by opaque ID", () => {
const first = applyAccountCommand(defaultSettings(), validateAccountCommand({ action: "create", kind: "realdebrid-web", identity: "rdw_first", dailyLimitBytes: GIB }));
const second = applyAccountCommand(first.settings, validateAccountCommand({ action: "create", kind: "realdebrid-web", identity: "rdw_second", dailyLimitBytes: 2 * GIB }));
expect(second.settings.realDebridWebAccountIds).toEqual(["rdw_first", "rdw_second"]);
expect(() => applyAccountCommand(second.settings, validateAccountCommand({ action: "create", kind: "realdebrid-web", identity: "rdw_second" }))).toThrow(/ungültig/i);
const replaced = applyAccountCommand(second.settings, validateAccountCommand({ action: "replace", kind: "realdebrid-web", accountId: "rdw_second", dailyLimitBytes: 3 * GIB }));
expect(replaced.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_first: GIB, rdw_second: 3 * GIB });
const deleted = applyAccountCommand(replaced.settings, validateAccountCommand({ action: "delete", kind: "realdebrid-web", accountId: "rdw_first" }));
expect(deleted.settings.realDebridWebAccountIds).toEqual(["rdw_second"]);
expect(deleted.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_second: 3 * GIB });
});
it("toggles only the selected Real-Debrid account ID", () => {
const firstToken = "fixture-rd-toggle-first";
const secondToken = "fixture-rd-toggle-second";
const firstId = "rda_toggleFirst";
const secondId = "rda_toggleSecond";
const settings = { ...defaultSettings(), realDebridApiTokens: serializeRealDebridApiAccounts([{ id: firstId, token: firstToken }, { id: secondId, token: secondToken }]) };
const disabled = setRealDebridAccountEnabled(settings, secondId, false);
expect(disabled.realDebridDisabledAccountIds).toEqual([secondId]);
expect(parseRealDebridApiAccounts(disabled.realDebridApiTokens).find((account) => account.id === firstId)?.enabled).toBe(true);
const enabled = setRealDebridAccountEnabled(disabled, secondId, true);
expect(enabled.realDebridDisabledAccountIds).toEqual([]);
});
it("reveals only the exact explicitly requested stored account secret", () => {
const api = accountCommands as typeof accountCommands & {
resolveStoredAccountSecret?: (settings: AppSettings, request: { kind: RendererAccountKind; accountId: string }) => string;
validateAccountSecretRequest?: (value: unknown) => { kind: RendererAccountKind; accountId: string };
};
const megaIdentity = "reveal-mega@example.test";
const megaId = getMegaDebridAccountId(megaIdentity);
const settings = {
...defaultSettings(),
token: "fixture-reveal-rd-1aB2",
megaDebridApiCredentials: `${megaIdentity}:fixture-reveal-mega-3cD4`,
debridLinkApiKeys: "fixture-reveal-dl-5eF6",
bestToken: "fixture-reveal-best-7gH8",
allDebridToken: "fixture-reveal-all-9iJ1",
deepbridApiKey: "fixture-reveal-deepbrid-1jK2",
ddownloadLogin: "reveal-dd@example.test",
ddownloadPassword: "fixture-reveal-dd-2kL3",
oneFichierApiKey: "fixture-reveal-one-4mN5",
linkSnappyLogin: "reveal-ls@example.test",
linkSnappyPassword: "fixture-reveal-ls-6pQ7"
};
expect(api.resolveStoredAccountSecret).toBeTypeOf("function");
expect(api.validateAccountSecretRequest).toBeTypeOf("function");
expect(api.resolveStoredAccountSecret?.(settings, { kind: "realdebrid-api", accountId: "svc-realdebrid" })).toBe("fixture-reveal-rd-1aB2");
const pooled = {
...settings,
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_revealOpaque", token: "fixture-reveal-rd-1aB2" }])
};
expect(() => api.resolveStoredAccountSecret?.(pooled, { kind: "realdebrid-api", accountId: "svc-realdebrid" })).toThrow(/nicht gefunden/i);
expect(api.resolveStoredAccountSecret?.(settings, { kind: "megadebrid-api", accountId: megaId })).toBe("fixture-reveal-mega-3cD4");
expect(api.resolveStoredAccountSecret?.(settings, { kind: "debridlink-api", accountId: getDebridLinkApiKeyId("fixture-reveal-dl-5eF6") })).toBe("fixture-reveal-dl-5eF6");
expect(api.resolveStoredAccountSecret?.(settings, { kind: "bestdebrid-api", accountId: "svc-bestdebrid" })).toBe("fixture-reveal-best-7gH8");
expect(api.resolveStoredAccountSecret?.(settings, { kind: "alldebrid-api", accountId: "svc-alldebrid" })).toBe("fixture-reveal-all-9iJ1");
expect(api.resolveStoredAccountSecret?.(settings, { kind: "deepbrid-api", accountId: "svc-deepbrid" })).toBe("fixture-reveal-deepbrid-1jK2");
expect(() => api.resolveStoredAccountSecret?.(settings, { kind: "deepbrid-api", accountId: "svc-realdebrid" })).toThrow(/nicht gefunden/i);
expect(api.resolveStoredAccountSecret?.(settings, { kind: "ddownload-login", accountId: "svc-ddownload" })).toBe("fixture-reveal-dd-2kL3");
expect(api.resolveStoredAccountSecret?.(settings, { kind: "onefichier-api", accountId: "svc-onefichier" })).toBe("fixture-reveal-one-4mN5");
expect(api.resolveStoredAccountSecret?.(settings, { kind: "linksnappy-login", accountId: "svc-linksnappy" })).toBe("fixture-reveal-ls-6pQ7");
expect(() => api.resolveStoredAccountSecret?.(settings, { kind: "megadebrid-api", accountId: "missing" })).toThrow(/nicht gefunden/i);
expect(() => api.validateAccountSecretRequest?.({ kind: "realdebrid-api", accountId: "svc-realdebrid", secret: "not-allowed" })).toThrow(/ungültig/i);
});
it.each(["realdebrid-api", "realdebrid-web"] as const)("accepts %s credential checks at the IPC boundary", (kind) => {
expect(validateAccountCredentialCheckInput({ kind, accountId: "svc-realdebrid" })).toEqual({
kind,
accountId: "svc-realdebrid",
identity: undefined,
secret: undefined
});
});
it("accepts an unsaved or stable Deepbrid account and rejects every other account ID for credential checks", () => {
expect(validateAccountCredentialCheckInput({ kind: "deepbrid-api" })).toEqual({
kind: "deepbrid-api",
accountId: undefined,
identity: undefined,
secret: undefined
});
expect(validateAccountCredentialCheckInput({ kind: "deepbrid-api", accountId: "svc-deepbrid" })).toEqual({
kind: "deepbrid-api",
accountId: "svc-deepbrid",
identity: undefined,
secret: undefined
});
expect(() => validateAccountCredentialCheckInput({ kind: "deepbrid-api", accountId: "svc-realdebrid" })).toThrow(/ungültig/i);
expect(() => validateAccountCredentialCheckInput({ kind: "deepbrid-api", accountId: "deepbrid-other" })).toThrow(/ungültig/i);
});
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"],
["deepbrid-api", "", "fixture-deepbrid-provider-secret-4jD7", "deepbridApiKey"],
["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("manages the single Deepbrid account without exposing or retaining deleted state", () => {
const original = "fixture-deepbrid-original-key-7mN1";
const replacement = "fixture-deepbrid-replacement-key-8pQ2";
const created = applyAccountCommand(defaultSettings(), validateAccountCommand({
action: "create",
kind: "deepbrid-api",
secret: original,
dailyLimitBytes: 2 * GIB
}));
expect(created.response).toEqual({ accountId: "svc-deepbrid" });
expect(created.settings.deepbridApiKey).toBe(original);
expect(created.settings.providerDailyLimitBytes.deepbrid).toBe(2 * GIB);
expect(JSON.stringify(created.response)).not.toContain(original);
expect(() => applyAccountCommand(created.settings, validateAccountCommand({ action: "create", kind: "deepbrid-api", secret: replacement }))).toThrow(/ungültig/i);
const retained = applyAccountCommand(created.settings, validateAccountCommand({
action: "replace",
kind: "deepbrid-api",
accountId: "svc-deepbrid",
secret: "",
dailyLimitBytes: 3 * GIB
}));
expect(retained.settings.deepbridApiKey).toBe(original);
const updated = applyAccountCommand(retained.settings, validateAccountCommand({
action: "update-secret",
kind: "deepbrid-api",
accountId: "svc-deepbrid",
secret: replacement
}));
const populated = {
...updated.settings,
providerDailyUsageBytes: { ...updated.settings.providerDailyUsageBytes, deepbrid: GIB },
providerTotalUsageBytes: { ...updated.settings.providerTotalUsageBytes, deepbrid: 5 * GIB },
debridAccountStatuses: {
...updated.settings.debridAccountStatuses,
"svc-deepbrid": {
accountId: "svc-deepbrid",
provider: "deepbrid" as const,
label: "Deepbrid",
maskedLogin: "••••",
valid: true,
isPremium: true,
premiumUntilMs: null,
message: "OK",
checkedAt: 1
}
}
};
expect(updated.settings.deepbridApiKey).toBe(replacement);
expect(JSON.stringify(updated.response)).not.toContain(replacement);
const deleted = applyAccountCommand(populated, validateAccountCommand({
action: "delete",
kind: "deepbrid-api",
accountId: "svc-deepbrid"
}));
expect(deleted.response).toEqual({ accountId: null });
expect(deleted.settings.deepbridApiKey).toBe("");
expect(deleted.settings.providerDailyLimitBytes).not.toHaveProperty("deepbrid");
expect(deleted.settings.providerDailyUsageBytes).not.toHaveProperty("deepbrid");
expect(deleted.settings.providerTotalUsageBytes).not.toHaveProperty("deepbrid");
expect(deleted.settings.debridAccountStatuses).not.toHaveProperty("svc-deepbrid");
});
it.each([
{ kind: "bestdebrid-api", identity: "", secret: "fixture-status-best-1aB2", provider: "bestdebrid" },
{ kind: "alldebrid-api", identity: "", secret: "fixture-status-all-3cD4", provider: "alldebrid" },
{ kind: "ddownload-login", identity: "status-dd@example.test", secret: "fixture-status-dd-5eF6", provider: "ddownload" },
{ kind: "onefichier-api", identity: "", secret: "fixture-status-one-7gH8", provider: "onefichier" },
{ kind: "linksnappy-login", identity: "status-ls@example.test", secret: "fixture-status-ls-9jK1", provider: "linksnappy" },
{ kind: "deepbrid-api", identity: "", secret: "fixture-status-deepbrid-2mN3", provider: "deepbrid" }
] as const)("deletes only the selected $provider single-account status", ({ kind, identity, secret, provider }) => {
const created = applyAccountCommand(defaultSettings(), validateAccountCommand({ action: "create", kind, identity, secret }));
const accountId = created.response.accountId!;
const foreignStatus = {
accountId: "foreign-account",
provider: "realdebrid" as const,
label: "Foreign",
maskedLogin: "fo***gn",
valid: true,
isPremium: true,
premiumUntilMs: null,
message: "OK",
checkedAt: 2
};
const selectedStatus = {
accountId,
provider,
label: provider,
maskedLogin: "se***ed",
valid: true,
isPremium: true,
premiumUntilMs: null,
message: "OK",
checkedAt: 1
};
const deleted = applyAccountCommand({
...created.settings,
debridAccountStatuses: {
[accountId]: selectedStatus,
"foreign-account": foreignStatus
}
}, validateAccountCommand({ action: "delete", kind, accountId }));
expect(deleted.settings.debridAccountStatuses).toEqual({ "foreign-account": foreignStatus });
});
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);
});
});