feat: add per-provider account priority rules
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { configureCredentialProtector } from "../src/main/credential-protection";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { applyAccountCommand } from "../src/main/account-commands";
|
||||
import { createRendererSettings } from "../src/main/renderer-state";
|
||||
import { validateRendererSettingsUpdate } from "../src/main/renderer-settings";
|
||||
import { createStoragePaths, loadSettings, normalizeSettings, saveSettings } from "../src/main/storage";
|
||||
import { normalizeAccountUsageRules, orderAccountsByRule, reconcileAccountOrder } from "../src/shared/account-usage-rules";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import type { AccountRuleProvider } from "../src/shared/account-usage-rules";
|
||||
import type { RendererAccountKind } from "../src/shared/types";
|
||||
|
||||
const directories: string[] = [];
|
||||
beforeEach(() => configureCredentialProtector({ isEncryptionAvailable: () => true, encryptString: (value) => Buffer.from(value).reverse(), decryptString: (value) => Buffer.from(value).reverse().toString() }));
|
||||
afterEach(() => { for (const directory of directories.splice(0)) fs.rmSync(directory, { recursive: true, force: true }); });
|
||||
|
||||
describe("account usage priorities", () => {
|
||||
it("preserves automatic defaults and filters corrupt configuration", () => {
|
||||
expect(defaultSettings().accountUsageRules).toEqual({});
|
||||
expect(normalizeSettings({ ...defaultSettings(), accountUsageRules: undefined as never }).accountUsageRules).toEqual({});
|
||||
expect(normalizeAccountUsageRules({ wrong: {}, realdebrid: { mode: "bad", accountIds: ["b", "b", 4, ""] } })).toEqual({ realdebrid: { mode: "automatic", accountIds: ["b"] } });
|
||||
});
|
||||
|
||||
it("restores known IDs, removes stale and duplicate IDs, and appends new accounts", () => {
|
||||
expect(reconcileAccountOrder(["b", "gone", "b", "a"], ["a", "b", "new"])).toEqual(["b", "a", "new"]);
|
||||
const accounts = [{ id: "a" }, { id: "b" }, { id: "new" }];
|
||||
expect(orderAccountsByRule(accounts, { mode: "priority", accountIds: ["b", "a"] }).map((a) => a.id)).toEqual(["b", "a", "new"]);
|
||||
expect(orderAccountsByRule(accounts, { mode: "automatic", accountIds: ["b", "a"] })).toEqual(accounts);
|
||||
});
|
||||
|
||||
it("validates nested rules without allowing arbitrary providers or malformed IDs", () => {
|
||||
const current = defaultSettings();
|
||||
for (const rule of [{ mode: "random", accountIds: [] }, { mode: "priority", accountIds: [3] }, { mode: "priority", accountIds: [], secret: "no" }]) {
|
||||
expect(() => validateRendererSettingsUpdate({ accountUsageRules: { realdebrid: rule } }, current)).toThrow();
|
||||
}
|
||||
expect(() => validateRendererSettingsUpdate({ accountUsageRules: { unknown: { mode: "priority", accountIds: [] } } }, current)).toThrow();
|
||||
const accountUsageRules = { realdebrid: { mode: "priority" as const, accountIds: ["a"] } };
|
||||
expect(validateRendererSettingsUpdate({ accountUsageRules }, current)).toEqual({ accountUsageRules });
|
||||
const projected = createRendererSettings({ ...current, accountUsageRules });
|
||||
projected.accountUsageRules.realdebrid!.accountIds.push("b");
|
||||
expect(accountUsageRules.realdebrid.accountIds).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["debridlink-api", "debridlink"], ["megadebrid-api", "megadebrid-api"], ["megadebrid-web", "megadebrid-web"], ["realdebrid-api", "realdebrid"]
|
||||
] as [RendererAccountKind, AccountRuleProvider][])("preserves position across replace, append, delete and reload for %s", (kind, provider) => {
|
||||
let settings = defaultSettings();
|
||||
const add = (identity: string, secret: string) => {
|
||||
const result = applyAccountCommand(settings, { action: "create", kind, identity, secret });
|
||||
settings = result.settings;
|
||||
return result.response.accountId!;
|
||||
};
|
||||
const first = add("first@example.test", "fixture-first-key");
|
||||
const second = add("second@example.test", "fixture-second-key");
|
||||
settings.accountUsageRules = { [provider]: { mode: "priority", accountIds: [second, first] } };
|
||||
const replacement = applyAccountCommand(settings, { action: "replace", kind, accountId: second, identity: "renamed@example.test", secret: "fixture-renamed-key" });
|
||||
settings = replacement.settings;
|
||||
const replacementId = replacement.response.accountId!;
|
||||
expect(settings.accountUsageRules[provider]?.accountIds).toEqual([replacementId, first]);
|
||||
const third = add("third@example.test", "fixture-third-key");
|
||||
expect(settings.accountUsageRules[provider]?.accountIds).toEqual([replacementId, first, third]);
|
||||
settings = applyAccountCommand(settings, { action: "delete", kind, accountId: first }).settings;
|
||||
expect(settings.accountUsageRules[provider]?.accountIds).toEqual([replacementId, third]);
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-account-priority-"));
|
||||
directories.push(directory);
|
||||
const paths = createStoragePaths(directory);
|
||||
saveSettings(paths, settings);
|
||||
expect(loadSettings(paths).accountUsageRules).toEqual(settings.accountUsageRules);
|
||||
});
|
||||
|
||||
it("prunes references to accounts missing from imported configuration", () => {
|
||||
const debridLinkApiKeys = "fixture-key-one\nfixture-key-two";
|
||||
const [first, second] = parseDebridLinkApiKeys(debridLinkApiKeys);
|
||||
const settings = normalizeSettings({ ...defaultSettings(), debridLinkApiKeys, accountUsageRules: { debridlink: { mode: "priority", accountIds: ["deleted", second.id] } } });
|
||||
expect(settings.accountUsageRules.debridlink?.accountIds).toEqual([second.id, first.id]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user