fix(accounts): refresh active logins and toggle instantly
Split bulk account checks into active-only and all-configured scopes so disabled providers, Mega-Debrid accounts, and Debrid-Link keys no longer distort the normal result count. Preserve explicit full checks for diagnosing disabled credentials and surface their failed status. Render account enablement from the optimistic settings draft, persist it with revision-safe rollback, and apply the same immediate behavior to individual and bulk switches. Extend the IPC contract, translations, changelog, and regression coverage.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { checkMegaDebridAccount, checkDebridLinkKey, checkAllDebridAccounts, checkRealDebridAccount, REAL_DEBRID_STATUS_ID } from "../src/main/account-check";
|
||||
import type { MegaDebridAccountEntry } from "../src/shared/mega-debrid-accounts";
|
||||
import type { DebridLinkApiKeyEntry } from "../src/shared/debrid-link-keys";
|
||||
import type { AppSettings } from "../src/shared/types";
|
||||
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";
|
||||
|
||||
function megaAccount(login = "user@example.com"): MegaDebridAccountEntry {
|
||||
return { id: "mda_test", login, password: "pw", index: 0, label: "Account 1", maskedLogin: "us**le" };
|
||||
@@ -137,7 +139,7 @@ describe("checkRealDebridAccount", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkAllDebridAccounts", () => {
|
||||
describe("checkAllDebridAccounts", () => {
|
||||
it("returns empty array when nothing configured", async () => {
|
||||
const settings = { megaCredentials: "", megaPassword: "", debridLinkApiKeys: "" } as unknown as AppSettings;
|
||||
const result = await checkAllDebridAccounts(settings);
|
||||
@@ -164,7 +166,7 @@ describe("checkAllDebridAccounts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("checks every configured mega account + debrid-link key", async () => {
|
||||
it("checks every configured mega account + debrid-link key", async () => {
|
||||
const futureSec = Math.floor(Date.now() / 1000) + 1000;
|
||||
vi.stubGlobal("fetch", vi.fn(async (url: string) => {
|
||||
if (String(url).includes("mega-debrid")) {
|
||||
@@ -183,8 +185,46 @@ describe("checkAllDebridAccounts", () => {
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result.filter((r) => r.provider === "megadebrid")).toHaveLength(2);
|
||||
expect(result.filter((r) => r.provider === "debridlink")).toHaveLength(3);
|
||||
expect(result.every((r) => r.valid)).toBe(true);
|
||||
});
|
||||
expect(result.every((r) => r.valid)).toBe(true);
|
||||
});
|
||||
|
||||
it("checks only enabled accounts in active scope and every account in all scope", async () => {
|
||||
const megaCredentials = [
|
||||
"one@example.test:pw1",
|
||||
"two@example.test:pw2",
|
||||
"three@example.test:pw3",
|
||||
"four@example.test:pw4"
|
||||
].join("\n");
|
||||
const settings: AppSettings = {
|
||||
...defaultSettings(),
|
||||
realDebridUseWebLogin: true,
|
||||
megaCredentials,
|
||||
megaDebridWebCredentials: megaCredentials,
|
||||
megaDebridWebEnabled: true,
|
||||
debridLinkApiKeys: "disabled-debrid-link-key",
|
||||
debridLinkDisabledKeyIds: [getDebridLinkApiKeyId("disabled-debrid-link-key")],
|
||||
megaDebridWebDisabledAccountIds: [
|
||||
getMegaDebridAccountId("one@example.test"),
|
||||
getMegaDebridAccountId("two@example.test"),
|
||||
getMegaDebridAccountId("three@example.test"),
|
||||
getMegaDebridAccountId("four@example.test")
|
||||
]
|
||||
};
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ response_code: "ok", token: "t", vip_end: "4102444800" })
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
const probe = vi.fn(async () => ({ valid: true, isPremium: true, username: "rd-user" }));
|
||||
|
||||
const active = await checkAllDebridAccounts(settings, undefined, probe, "active");
|
||||
const all = await checkAllDebridAccounts(settings, undefined, probe, "all");
|
||||
|
||||
expect(active.map((status) => status.accountId)).toEqual([REAL_DEBRID_STATUS_ID]);
|
||||
expect(all).toHaveLength(6);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("caps concurrency (never more than 4 in flight) and preserves result order", async () => {
|
||||
let inFlight = 0;
|
||||
|
||||
@@ -61,4 +61,16 @@ describe("account preload contract", () => {
|
||||
IPC_CHANNELS.DELETE_ACCOUNT
|
||||
]);
|
||||
});
|
||||
|
||||
it("forwards the selected bulk account-check scope", async () => {
|
||||
electron.invoke.mockResolvedValue([]);
|
||||
|
||||
await electron.api?.checkDebridAccounts("active");
|
||||
await electron.api?.checkDebridAccounts("all");
|
||||
|
||||
expect(electron.invoke.mock.calls).toEqual([
|
||||
[IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, "active"],
|
||||
[IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, "all"]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,6 +188,8 @@ describe("renderer localization", () => {
|
||||
["Zwischenablage: 3 Link(s) erkannt", "Clipboard: 3 link(s) detected"],
|
||||
["2/4 API-Keys deaktiviert.", "2/4 API keys disabled."],
|
||||
["Account-Check: 3/4 Login gültig, 2 mit Premium.", "Account check: 3/4 logins valid, 2 with premium."],
|
||||
["Aktive Accounts: 1/1 Login gültig, 1 mit Premium.", "Active accounts: 1/1 logins valid, 1 with premium."],
|
||||
["Alle Accounts: 2/5 Login gültig, 2 mit Premium.", "All accounts: 2/5 logins valid, 2 with premium."],
|
||||
["Soll RapidGator wirklich aus der Accountliste entfernt werden?", "Remove RapidGator from the account list?"],
|
||||
["Konflikte gelöst: 2 überschrieben, 3 übersprungen", "Conflicts resolved: 2 overwritten, 3 skipped"],
|
||||
["DLC importiert: 2 Paket(e), 5 Link(s)", "DLC imported: 2 package(s), 5 link(s)"],
|
||||
|
||||
@@ -8,7 +8,9 @@ import { buildAccountAddFields, createAccountDialogState } from "../src/renderer
|
||||
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
|
||||
import {
|
||||
buildBulkAccountEnabledState,
|
||||
buildConfiguredProviderOrder
|
||||
buildConfiguredProviderOrder,
|
||||
resolveAccountStatusState,
|
||||
runOptimisticAccountUpdate
|
||||
} from "../src/renderer/account-ui";
|
||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
@@ -304,6 +306,7 @@ function workspaceActions(overrides: Partial<AccountWorkspaceActions> = {}): Acc
|
||||
onCopyIdentity: () => {},
|
||||
onAdd: () => {},
|
||||
onRemoveSelected: () => {},
|
||||
onCheckActive: () => {},
|
||||
onCheckAll: () => {},
|
||||
...overrides
|
||||
};
|
||||
@@ -727,6 +730,15 @@ describe("account workspace", () => {
|
||||
expect(html).not.toContain("role=\"toolbar\"");
|
||||
});
|
||||
|
||||
it("offers separate checks for active accounts and every configured account", () => {
|
||||
const html = renderToStaticMarkup(<AccountWorkspace actions={workspaceActions()} model={workspaceModel()} />);
|
||||
|
||||
expect(html).toContain("Aktive aktualisieren");
|
||||
expect(html).toContain("Alle aktualisieren");
|
||||
expect(html).toContain('title="Prüft nur aktivierte Accounts."');
|
||||
expect(html).toContain('title="Prüft alle angelegten Accounts, auch deaktivierte."');
|
||||
});
|
||||
|
||||
it("keeps row selection, enable toggles, edit and context actions separate", () => {
|
||||
const calls: string[] = [];
|
||||
const tree = AccountWorkspace({
|
||||
@@ -946,6 +958,34 @@ describe("account workspace", () => {
|
||||
});
|
||||
|
||||
describe("settings App integration", () => {
|
||||
it("shows a failed all-account check even when the account is disabled", () => {
|
||||
expect(resolveAccountStatusState(true, { valid: false, isPremium: false })).toBe("invalid");
|
||||
expect(resolveAccountStatusState(true, { valid: true, isPremium: true })).toBe("disabled");
|
||||
expect(resolveAccountStatusState(false, undefined)).toBe("unchecked");
|
||||
});
|
||||
|
||||
it("applies account switches before persistence settles and rolls back failed saves", async () => {
|
||||
const events: string[] = [];
|
||||
let resolvePersist: (value: string) => void = () => { throw new Error("persist resolver missing"); };
|
||||
const pending = runOptimisticAccountUpdate(
|
||||
() => events.push("apply"),
|
||||
() => new Promise<string>((resolve) => { resolvePersist = resolve; }),
|
||||
() => events.push("rollback")
|
||||
);
|
||||
|
||||
expect(events).toEqual(["apply"]);
|
||||
resolvePersist("saved");
|
||||
await expect(pending).resolves.toBe("saved");
|
||||
expect(events).toEqual(["apply"]);
|
||||
|
||||
await expect(runOptimisticAccountUpdate(
|
||||
() => events.push("apply-failed"),
|
||||
async () => { throw new Error("save failed"); },
|
||||
() => events.push("rollback")
|
||||
)).rejects.toThrow("save failed");
|
||||
expect(events.slice(-2)).toEqual(["apply-failed", "rollback"]);
|
||||
});
|
||||
|
||||
it("keeps new Mega-Debrid credentials empty and never exposes stored accounts as an API key", () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
@@ -974,7 +1014,7 @@ describe("settings App integration", () => {
|
||||
|
||||
it("keeps unchecked single accounts honest without a positive status", () => {
|
||||
const block = sourceBlock(appSource, "const accountSources", "const selectedAccountViewId");
|
||||
expect(block).toMatch(/:\s*!checkedStatus\s*\?\s*"unchecked"/s);
|
||||
expect(block).toContain("resolveAccountStatusState(row.disabled, checkedStatus)");
|
||||
});
|
||||
|
||||
it("stores only the stable account row id in context-menu state", () => {
|
||||
|
||||
Reference in New Issue
Block a user