feat(accounts): manage Real-Debrid account pool
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { checkMegaDebridAccount, checkDebridLinkKey, checkAllDebridAccounts, checkRealDebridAccount, REAL_DEBRID_STATUS_ID } from "../src/main/account-check";
|
||||
import { checkMegaDebridAccount, checkDebridLinkKey, checkAllDebridAccounts, checkRealDebridAccount, REAL_DEBRID_STATUS_ID, retainConfiguredRealDebridStatuses } from "../src/main/account-check";
|
||||
import type { MegaDebridAccountEntry } from "../src/shared/mega-debrid-accounts";
|
||||
import { getDebridLinkApiKeyId, type DebridLinkApiKeyEntry } from "../src/shared/debrid-link-keys";
|
||||
import type { AppSettings } from "../src/shared/types";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { getRealDebridAccounts, serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||
|
||||
function megaAccount(login = "user@example.com"): MegaDebridAccountEntry {
|
||||
return { id: "mda_test", login, password: "pw", index: 0, label: "Account 1", maskedLogin: "us**le" };
|
||||
@@ -122,6 +123,13 @@ describe("checkDebridLinkKey", () => {
|
||||
});
|
||||
|
||||
describe("checkRealDebridAccount", () => {
|
||||
it("keys every API account status by its concrete pool identity", async () => {
|
||||
const token = "rd-api-pool-token";
|
||||
mockFetchOnce(200, { username: "api-user", type: "premium", expiration: new Date(NOW + 100_000).toISOString() });
|
||||
const account = getRealDebridAccounts({ realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_checkOpaque", token }]) })[0];
|
||||
const status = await checkRealDebridAccount(account, undefined, NOW);
|
||||
expect(status.accountId).toBe("rda_checkOpaque");
|
||||
});
|
||||
it("keeps browser-session username and email in separate status fields", async () => {
|
||||
const premiumUntilMs = NOW + 30 * 24 * 60 * 60 * 1000;
|
||||
const probe = vi.fn(async () => ({
|
||||
@@ -173,6 +181,36 @@ describe("checkRealDebridAccount", () => {
|
||||
});
|
||||
|
||||
describe("checkAllDebridAccounts", () => {
|
||||
it("discards a late Real-Debrid result after its account was removed", () => {
|
||||
const removedId = "rda_removedAfterCheck";
|
||||
const lateStatus = { accountId: removedId, provider: "realdebrid" as const, label: "API-Token 1", maskedLogin: "Geschützt", valid: true, isPremium: true, premiumUntilMs: null, message: "Premium aktiv", checkedAt: NOW };
|
||||
expect(retainConfiguredRealDebridStatuses(defaultSettings(), [lateStatus])).toEqual([]);
|
||||
});
|
||||
it("checks only enabled Real-Debrid pool entries in active scope and all entries in all scope", async () => {
|
||||
const firstToken = "rd-pool-active";
|
||||
const secondToken = "rd-pool-disabled";
|
||||
const activeId = "rda_poolActive";
|
||||
const disabledId = "rda_poolDisabled";
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: activeId, token: firstToken }, { id: disabledId, token: secondToken }]),
|
||||
realDebridWebAccountIds: ["rdw_first", "rdw_second"],
|
||||
realDebridDisabledAccountIds: [disabledId, "rdw_second"]
|
||||
};
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, status: 200, text: async () => JSON.stringify({ username: "api", type: "premium" }) })) as unknown as typeof fetch);
|
||||
const probedAccountIds: string[] = [];
|
||||
const probe = vi.fn(async (accountId: string) => {
|
||||
probedAccountIds.push(accountId);
|
||||
return { valid: true, isPremium: true, username: "web" };
|
||||
});
|
||||
|
||||
const active = await checkAllDebridAccounts(settings, undefined, probe, "active");
|
||||
const all = await checkAllDebridAccounts(settings, undefined, probe, "all");
|
||||
|
||||
expect(active.map((status) => status.accountId)).toEqual([activeId, "rdw_first"]);
|
||||
expect(all.map((status) => status.accountId)).toEqual([activeId, disabledId, "rdw_first", "rdw_second"]);
|
||||
expect(probedAccountIds).toEqual(["rdw_first", "rdw_first", "rdw_second"]);
|
||||
});
|
||||
it("returns empty array when nothing configured", async () => {
|
||||
const settings = { megaCredentials: "", megaPassword: "", debridLinkApiKeys: "" } as unknown as AppSettings;
|
||||
const result = await checkAllDebridAccounts(settings);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyAccountCommand, validateAccountCommand, validateAccountCredentialCheckInput } from "../src/main/account-commands";
|
||||
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";
|
||||
@@ -43,6 +44,63 @@ const ACCOUNT_KINDS: RendererAccountKind[] = [
|
||||
];
|
||||
|
||||
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;
|
||||
@@ -67,6 +125,11 @@ describe("write-only account commands", () => {
|
||||
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");
|
||||
|
||||
@@ -1,67 +1,57 @@
|
||||
import crypto from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getRealDebridAccountIds,
|
||||
getRealDebridAccounts,
|
||||
getRealDebridApiAccountId,
|
||||
parseRealDebridApiAccounts,
|
||||
serializeRealDebridApiAccounts
|
||||
} from "../src/shared/real-debrid-accounts";
|
||||
|
||||
describe("Real-Debrid account pool", () => {
|
||||
it("parses distinct API tokens and removes exact duplicates", () => {
|
||||
const accounts = parseRealDebridApiAccounts("first-secret-token\r\nsecond-secret-token\nfirst-secret-token");
|
||||
it("reads persisted opaque API identities and removes duplicate tokens", () => {
|
||||
const raw = serializeRealDebridApiAccounts([
|
||||
{ id: "rda_firstOpaque", token: "first-secret-token" },
|
||||
{ id: "rda_secondOpaque", token: "second-secret-token" },
|
||||
{ id: "rda_duplicateOpaque", token: "first-secret-token" }
|
||||
]);
|
||||
const accounts = parseRealDebridApiAccounts(raw);
|
||||
|
||||
expect(accounts).toHaveLength(2);
|
||||
expect(accounts.map((entry) => entry.id)).toEqual(["rda_firstOpaque", "rda_secondOpaque"]);
|
||||
expect(accounts.map((entry) => entry.token)).toEqual(["first-secret-token", "second-secret-token"]);
|
||||
expect(accounts.map((entry) => entry.label)).toEqual(["API-Token 1", "API-Token 2"]);
|
||||
});
|
||||
|
||||
it("creates stable opaque IDs without secret material", () => {
|
||||
it("keeps opaque IDs independent from token material", () => {
|
||||
const token = "private-real-debrid-token";
|
||||
const first = getRealDebridApiAccountId(token);
|
||||
const second = getRealDebridApiAccountId(` ${token} `);
|
||||
const raw = serializeRealDebridApiAccounts([{ id: "rda_opaqueAccount42", token }]);
|
||||
const account = parseRealDebridApiAccounts(raw)[0];
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(first).toMatch(/^rda_[a-z0-9]+$/);
|
||||
expect(first).not.toContain(token);
|
||||
expect(parseRealDebridApiAccounts(token)[0]).toMatchObject({ id: first, kind: "api" });
|
||||
expect(parseRealDebridApiAccounts(token)[0].label).not.toContain(token);
|
||||
expect(parseRealDebridApiAccounts(token)[0].maskedLogin).not.toContain(token);
|
||||
expect(account.id).toBe("rda_opaqueAccount42");
|
||||
expect(account.id).not.toContain(token);
|
||||
expect(account.label).not.toContain(token);
|
||||
expect(account.maskedLogin).not.toContain(token);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"cryptographic-real-debrid-token",
|
||||
"üñîçødé-real-debrid-token",
|
||||
"x".repeat(160)
|
||||
])("derives API account IDs from a cryptographic SHA-256 fingerprint", (token) => {
|
||||
const digest = crypto.createHash("sha256").update(token, "utf8").digest("hex").slice(0, 32);
|
||||
it("serializes the versioned pool idempotently", () => {
|
||||
const serialized = serializeRealDebridApiAccounts([
|
||||
{ id: "rda_firstOpaque", token: " first-secret-token " },
|
||||
{ id: "rda_secondOpaque", token: "second-secret-token" }
|
||||
]);
|
||||
|
||||
expect(getRealDebridApiAccountId(token)).toBe(`rda_${digest}`);
|
||||
});
|
||||
|
||||
it("serializes normalized unique API tokens one per line", () => {
|
||||
expect(serializeRealDebridApiAccounts([
|
||||
" first-secret-token ",
|
||||
"",
|
||||
"second-secret-token",
|
||||
"first-secret-token"
|
||||
])).toBe("first-secret-token\nsecond-secret-token");
|
||||
expect(serializeRealDebridApiAccounts(parseRealDebridApiAccounts(serialized))).toBe(serialized);
|
||||
});
|
||||
|
||||
it("combines API and opaque Web accounts and marks disabled entries", () => {
|
||||
const apiId = getRealDebridApiAccountId("api-secret");
|
||||
const settings = {
|
||||
realDebridApiTokens: "api-secret",
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_apiOpaque", token: "api-secret" }]),
|
||||
realDebridWebAccountIds: ["rdw_legacy", "rdw_second"],
|
||||
realDebridDisabledAccountIds: [apiId, "rdw_second"]
|
||||
realDebridDisabledAccountIds: ["rda_apiOpaque", "rdw_second"]
|
||||
};
|
||||
|
||||
expect(getRealDebridAccounts(settings)).toEqual([
|
||||
expect.objectContaining({ id: apiId, kind: "api", enabled: false }),
|
||||
expect.objectContaining({ id: "rda_apiOpaque", kind: "api", enabled: false }),
|
||||
expect.objectContaining({ id: "rdw_legacy", kind: "web", enabled: true }),
|
||||
expect.objectContaining({ id: "rdw_second", kind: "web", enabled: false })
|
||||
]);
|
||||
expect(getRealDebridAccountIds(settings)).toEqual([apiId, "rdw_legacy", "rdw_second"]);
|
||||
expect(getRealDebridAccountIds(settings)).toEqual(["rda_apiOpaque", "rdw_legacy", "rdw_second"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import crypto from "node:crypto";
|
||||
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 { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||
import type { AppSettings, RendererAccountKind } from "../src/shared/types";
|
||||
|
||||
const SECRETS = {
|
||||
@@ -38,6 +40,35 @@ const ACCOUNT_FIXTURES: Array<{
|
||||
];
|
||||
|
||||
describe("renderer state serialization", () => {
|
||||
it("projects distinct Real-Debrid API and Web rows with per-account state", () => {
|
||||
const firstToken = "fixture-renderer-rd-first-1aB2";
|
||||
const secondToken = "fixture-renderer-rd-second-3cD4";
|
||||
const firstId = "rda_rendererFirst";
|
||||
const secondId = "rda_rendererSecond";
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: firstId, token: firstToken }, { id: secondId, token: secondToken }]),
|
||||
realDebridWebAccountIds: ["rdw_first"],
|
||||
realDebridDisabledAccountIds: [secondId],
|
||||
realDebridAccountDailyLimitBytes: { [firstId]: 100 },
|
||||
realDebridAccountDailyUsageBytes: { [firstId]: 25 },
|
||||
realDebridAccountTotalUsageBytes: { [firstId]: 500 },
|
||||
debridAccountStatuses: {
|
||||
[firstId]: { accountId: firstId, provider: "realdebrid" as const, label: "API-Token 1", maskedLogin: "Geschützter API-Token", valid: true, isPremium: true, premiumUntilMs: null, message: "Premium aktiv", checkedAt: 1 }
|
||||
}
|
||||
};
|
||||
const state = createRendererState(settings);
|
||||
const realDebridRows = state.accounts.filter((account) => account.provider === "realdebrid");
|
||||
|
||||
expect(realDebridRows).toEqual([
|
||||
expect.objectContaining({ accountId: firstId, kind: "realdebrid-api", enabled: true, dailyLimitBytes: 100, dailyUsageBytes: 25, totalUsageBytes: 500, status: expect.objectContaining({ accountId: firstId }) }),
|
||||
expect.objectContaining({ accountId: secondId, kind: "realdebrid-api", enabled: false }),
|
||||
expect.objectContaining({ accountId: "rdw_first", kind: "realdebrid-web", enabled: true })
|
||||
]);
|
||||
expect(JSON.stringify(state)).not.toContain(firstToken);
|
||||
expect(JSON.stringify(state)).not.toContain(secondToken);
|
||||
expect(realDebridRows[0].accountId).not.toBe(`rda_${crypto.createHash("sha256").update(firstToken).digest("hex").slice(0, 32)}`);
|
||||
});
|
||||
it.each(ACCOUNT_FIXTURES)("serializes $kind without its representative secret", ({ kind, secret, settings }) => {
|
||||
const state = createRendererState({ ...defaultSettings(), ...settings });
|
||||
|
||||
|
||||
@@ -3,8 +3,30 @@ import { defaultSettings } from "../src/main/constants";
|
||||
import { overlayLiveUsageCounters } from "../src/main/settings-live-overlay";
|
||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||
|
||||
describe("live settings overlay", () => {
|
||||
it("keeps only live Real-Debrid status and usage for accounts still configured", () => {
|
||||
const keepToken = "fixture-overlay-rd-keep";
|
||||
const removedToken = "fixture-overlay-rd-removed";
|
||||
const keepId = "rda_overlayKeep";
|
||||
const removedId = "rda_overlayRemoved";
|
||||
const target = { ...defaultSettings(), realDebridApiTokens: serializeRealDebridApiAccounts([{ id: keepId, token: keepToken }]) };
|
||||
const status = (accountId: string) => ({ accountId, provider: "realdebrid" as const, label: "Real-Debrid", maskedLogin: "Geschützt", valid: true, isPremium: true, premiumUntilMs: null, message: "Premium aktiv", checkedAt: 1 });
|
||||
const live = {
|
||||
...defaultSettings(),
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: keepId, token: keepToken }, { id: removedId, token: removedToken }]),
|
||||
realDebridAccountDailyUsageBytes: { [keepId]: 10, [removedId]: 20 },
|
||||
realDebridAccountTotalUsageBytes: { [keepId]: 100, [removedId]: 200 },
|
||||
debridAccountStatuses: { [keepId]: status(keepId), [removedId]: status(removedId) }
|
||||
};
|
||||
|
||||
overlayLiveUsageCounters(target, live, 1);
|
||||
|
||||
expect(target.realDebridAccountDailyUsageBytes).toEqual({ [keepId]: 10 });
|
||||
expect(target.realDebridAccountTotalUsageBytes).toEqual({ [keepId]: 100 });
|
||||
expect(target.debridAccountStatuses).toEqual({ [keepId]: status(keepId) });
|
||||
});
|
||||
it("keeps current Mega-Debrid counters and drops data for identities no longer configured", () => {
|
||||
const keepMegaId = getMegaDebridAccountId("keep@example.com");
|
||||
const removedMegaId = getMegaDebridAccountId("removed@example.com");
|
||||
|
||||
+41
-10
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||
import { getRealDebridApiAccountId } from "../src/shared/real-debrid-accounts";
|
||||
import { parseRealDebridApiAccounts, serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||
import { AppSettings } from "../src/shared/types";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { configureCredentialProtector } from "../src/main/credential-protection";
|
||||
@@ -663,7 +663,6 @@ describe("settings storage", () => {
|
||||
it("migrates legacy Real-Debrid API and Web accounts with their existing status", () => {
|
||||
const checkedAt = Date.now();
|
||||
const apiToken = "legacy-real-debrid-token";
|
||||
const apiId = getRealDebridApiAccountId(apiToken);
|
||||
const legacyApi = {
|
||||
...defaultSettings(),
|
||||
token: apiToken,
|
||||
@@ -685,8 +684,11 @@ describe("settings storage", () => {
|
||||
delete legacyApi.realDebridApiTokens;
|
||||
delete legacyApi.realDebridWebAccountIds;
|
||||
const normalizedApi = normalizeSettings(legacyApi as AppSettings);
|
||||
const [migratedApiAccount] = parseRealDebridApiAccounts(normalizedApi.realDebridApiTokens);
|
||||
const apiId = migratedApiAccount.id;
|
||||
|
||||
expect(normalizedApi.realDebridApiTokens).toBe(apiToken);
|
||||
expect(migratedApiAccount.token).toBe(apiToken);
|
||||
expect(apiId).toMatch(/^rda_[A-Za-z0-9_-]+$/);
|
||||
expect(normalizedApi.realDebridWebAccountIds).toEqual([]);
|
||||
expect(normalizedApi.debridAccountStatuses[apiId]).toMatchObject({
|
||||
accountId: apiId,
|
||||
@@ -739,9 +741,30 @@ describe("settings storage", () => {
|
||||
expect(normalized.realDebridWebAccountIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps two opaque Real-Debrid API IDs across normalize, save and load", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-opaque-rd-"));
|
||||
tempDirs.push(dir);
|
||||
const paths = createStoragePaths(dir);
|
||||
const expected = [
|
||||
{ id: "rda_persistedFirst", token: "persisted-first-token" },
|
||||
{ id: "rda_persistedSecond", token: "persisted-second-token" }
|
||||
];
|
||||
const normalized = normalizeSettings({
|
||||
...defaultSettings(),
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts(expected)
|
||||
});
|
||||
|
||||
saveSettings(paths, normalized);
|
||||
const loaded = loadSettings(paths);
|
||||
|
||||
expect(parseRealDebridApiAccounts(normalized.realDebridApiTokens).map(({ id, token }) => ({ id, token }))).toEqual(expected);
|
||||
expect(parseRealDebridApiAccounts(loaded.realDebridApiTokens).map(({ id, token }) => ({ id, token }))).toEqual(expected);
|
||||
expect(normalizeSettings(loaded)).toEqual(loaded);
|
||||
});
|
||||
|
||||
it("prefers a concrete Real-Debrid status over the legacy status regardless of object order", () => {
|
||||
const token = "status-order-token";
|
||||
const accountId = getRealDebridApiAccountId(token);
|
||||
const accountId = "rda_statusOrder";
|
||||
const checkedAt = Date.now();
|
||||
const legacyStatus = {
|
||||
accountId: "svc-realdebrid",
|
||||
@@ -767,7 +790,7 @@ describe("settings storage", () => {
|
||||
};
|
||||
const normalizeWithOrder = (entries: [string, typeof legacyStatus][]) => normalizeSettings({
|
||||
...defaultSettings(),
|
||||
realDebridApiTokens: token,
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: accountId, token }]),
|
||||
debridAccountStatuses: Object.fromEntries(entries)
|
||||
}).debridAccountStatuses[accountId];
|
||||
|
||||
@@ -782,11 +805,16 @@ describe("settings storage", () => {
|
||||
});
|
||||
|
||||
it("normalizes the Real-Debrid pool idempotently and prunes stale account maps", () => {
|
||||
const apiId = getRealDebridApiAccountId("api-token");
|
||||
const apiId = "rda_apiPrimary";
|
||||
const secondApiId = "rda_apiSecond";
|
||||
const today = getProviderUsageDayKey();
|
||||
const once = normalizeSettings({
|
||||
...defaultSettings(),
|
||||
realDebridApiTokens: "api-token\napi-token\nsecond-token",
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts([
|
||||
{ id: apiId, token: "api-token" },
|
||||
{ id: "rda_duplicate", token: "api-token" },
|
||||
{ id: secondApiId, token: "second-token" }
|
||||
]),
|
||||
realDebridWebAccountIds: ["rdw_legacy", "rdw_second", "broken", "rdw_second"],
|
||||
realDebridDisabledAccountIds: [apiId, "rdw_second", "stale"],
|
||||
realDebridAccountDailyLimitBytes: { [apiId]: 1000, rdw_second: 2000, stale: 3000 },
|
||||
@@ -796,7 +824,10 @@ describe("settings storage", () => {
|
||||
});
|
||||
const twice = normalizeSettings(once);
|
||||
|
||||
expect(once.realDebridApiTokens).toBe("api-token\nsecond-token");
|
||||
expect(parseRealDebridApiAccounts(once.realDebridApiTokens).map((account) => ({ id: account.id, token: account.token }))).toEqual([
|
||||
{ id: apiId, token: "api-token" },
|
||||
{ id: secondApiId, token: "second-token" }
|
||||
]);
|
||||
expect(once.realDebridWebAccountIds).toEqual(["rdw_legacy", "rdw_second"]);
|
||||
expect(once.realDebridDisabledAccountIds).toEqual([apiId, "rdw_second"]);
|
||||
expect(once.realDebridAccountDailyLimitBytes).toEqual({ [apiId]: 1000, rdw_second: 2000 });
|
||||
@@ -806,10 +837,10 @@ describe("settings storage", () => {
|
||||
});
|
||||
|
||||
it("resets stale per-account Real-Debrid daily usage", () => {
|
||||
const apiId = getRealDebridApiAccountId("api-token");
|
||||
const apiId = "rda_staleUsage";
|
||||
const normalized = normalizeSettings({
|
||||
...defaultSettings(),
|
||||
realDebridApiTokens: "api-token",
|
||||
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: apiId, token: "api-token" }]),
|
||||
providerDailyUsageDay: "2001-01-01",
|
||||
realDebridAccountDailyUsageBytes: { [apiId]: 4000 },
|
||||
realDebridAccountTotalUsageBytes: { [apiId]: 9000 }
|
||||
|
||||
Reference in New Issue
Block a user