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.
249 lines
10 KiB
TypeScript
249 lines
10 KiB
TypeScript
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 { 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" };
|
|
}
|
|
|
|
function debridLinkKey(token = "tok_abcdef"): DebridLinkApiKeyEntry {
|
|
return { id: "dlk_test", token, index: 0, label: "Key 1", masked: "tok***def" };
|
|
}
|
|
|
|
function mockFetchOnce(status: number, body: unknown): void {
|
|
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
vi.stubGlobal("fetch", vi.fn(async () => ({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
text: async () => text
|
|
})) as unknown as typeof fetch);
|
|
}
|
|
|
|
const NOW = 1_700_000_000_000;
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("checkMegaDebridAccount", () => {
|
|
it("reports valid + premium from vip_end (future Unix ts)", async () => {
|
|
const futureSec = Math.floor(NOW / 1000) + 30 * 24 * 60 * 60;
|
|
mockFetchOnce(200, { response_code: "ok", response_text: "User logged", token: "t", vip_end: String(futureSec), email: "a@b.de" });
|
|
const st = await checkMegaDebridAccount(megaAccount(), undefined, NOW);
|
|
expect(st.valid).toBe(true);
|
|
expect(st.isPremium).toBe(true);
|
|
expect(st.premiumUntilMs).toBe(futureSec * 1000);
|
|
expect(st.email).toBe("a@b.de");
|
|
expect(st.message).toMatch(/Premium noch/);
|
|
});
|
|
|
|
it("reports valid but NOT premium when vip_end is in the past", async () => {
|
|
const pastSec = Math.floor(NOW / 1000) - 1000;
|
|
mockFetchOnce(200, { response_code: "ok", token: "t", vip_end: String(pastSec) });
|
|
const st = await checkMegaDebridAccount(megaAccount(), undefined, NOW);
|
|
expect(st.valid).toBe(true);
|
|
expect(st.isPremium).toBe(false);
|
|
});
|
|
|
|
it("reports valid but no premium when vip_end is 0/missing", async () => {
|
|
mockFetchOnce(200, { response_code: "ok", token: "t", vip_end: "0" });
|
|
const st = await checkMegaDebridAccount(megaAccount(), undefined, NOW);
|
|
expect(st.valid).toBe(true);
|
|
expect(st.isPremium).toBe(false);
|
|
expect(st.premiumUntilMs).toBe(0);
|
|
expect(st.message).toMatch(/Kein Premium/);
|
|
});
|
|
|
|
it("reports invalid login when response_code != ok", async () => {
|
|
mockFetchOnce(200, { response_code: "error", response_text: "bad login" });
|
|
const st = await checkMegaDebridAccount(megaAccount(), undefined, NOW);
|
|
expect(st.valid).toBe(false);
|
|
expect(st.isPremium).toBe(false);
|
|
expect(st.message).toMatch(/Ungueltiger Login/);
|
|
});
|
|
|
|
it("reports invalid on HTTP error", async () => {
|
|
mockFetchOnce(500, "server error");
|
|
const st = await checkMegaDebridAccount(megaAccount(), undefined, NOW);
|
|
expect(st.valid).toBe(false);
|
|
});
|
|
|
|
it("never throws on network error — returns a failed status", async () => {
|
|
vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("ECONNRESET"); }) as unknown as typeof fetch);
|
|
const st = await checkMegaDebridAccount(megaAccount(), undefined, NOW);
|
|
expect(st.valid).toBe(false);
|
|
expect(st.message).toMatch(/Pruefung fehlgeschlagen/);
|
|
});
|
|
});
|
|
|
|
describe("checkDebridLinkKey", () => {
|
|
it("reports valid + premium from premiumLeft seconds", async () => {
|
|
const premiumLeft = 60 * 24 * 60 * 60;
|
|
mockFetchOnce(200, { success: true, value: { username: "u", accountType: 1, premiumLeft } });
|
|
const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW);
|
|
expect(st.valid).toBe(true);
|
|
expect(st.isPremium).toBe(true);
|
|
expect(st.premiumUntilMs).toBe(NOW + premiumLeft * 1000);
|
|
});
|
|
|
|
it("reports valid but free (premiumLeft 0, accountType 0)", async () => {
|
|
mockFetchOnce(200, { success: true, value: { username: "u", accountType: 0, premiumLeft: 0 } });
|
|
const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW);
|
|
expect(st.valid).toBe(true);
|
|
expect(st.isPremium).toBe(false);
|
|
expect(st.message).toMatch(/Free/);
|
|
});
|
|
|
|
it("reports invalid key on HTTP 401", async () => {
|
|
mockFetchOnce(401, { success: false, error: "badToken" });
|
|
const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW);
|
|
expect(st.valid).toBe(false);
|
|
expect(st.message).toMatch(/Ungueltiger API-Key/);
|
|
});
|
|
|
|
it("reports invalid key when success=false", async () => {
|
|
mockFetchOnce(200, { success: false, error: "badToken" });
|
|
const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW);
|
|
expect(st.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("checkRealDebridAccount", () => {
|
|
it("uses the browser-session probe and returns a stable service status", async () => {
|
|
const premiumUntilMs = NOW + 30 * 24 * 60 * 60 * 1000;
|
|
const probe = vi.fn(async () => ({
|
|
valid: true,
|
|
isPremium: true,
|
|
premiumUntilMs,
|
|
username: "web-user"
|
|
}));
|
|
|
|
const status = await checkRealDebridAccount({
|
|
token: "",
|
|
realDebridUseWebLogin: true
|
|
} as AppSettings, undefined, NOW, probe);
|
|
|
|
expect(probe).toHaveBeenCalledTimes(1);
|
|
expect(status).toMatchObject({
|
|
accountId: REAL_DEBRID_STATUS_ID,
|
|
provider: "realdebrid",
|
|
valid: true,
|
|
isPremium: true,
|
|
premiumUntilMs,
|
|
email: "web-user"
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("checkAllDebridAccounts", () => {
|
|
it("returns empty array when nothing configured", async () => {
|
|
const settings = { megaCredentials: "", megaPassword: "", debridLinkApiKeys: "" } as unknown as AppSettings;
|
|
const result = await checkAllDebridAccounts(settings);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("includes Real-Debrid web login in the bulk account check", async () => {
|
|
const settings = {
|
|
token: "",
|
|
realDebridUseWebLogin: true,
|
|
megaCredentials: "",
|
|
megaPassword: "",
|
|
debridLinkApiKeys: ""
|
|
} as AppSettings;
|
|
const probe = vi.fn(async () => ({ valid: true, isPremium: true, username: "web-user" }));
|
|
|
|
const result = await checkAllDebridAccounts(settings, undefined, probe);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
accountId: REAL_DEBRID_STATUS_ID,
|
|
provider: "realdebrid",
|
|
valid: true
|
|
});
|
|
});
|
|
|
|
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")) {
|
|
return { ok: true, status: 200, text: async () => JSON.stringify({ response_code: "ok", token: "t", vip_end: String(futureSec) }) };
|
|
}
|
|
return { ok: true, status: 200, text: async () => JSON.stringify({ success: true, value: { accountType: 1, premiumLeft: 1000 } }) };
|
|
}) as unknown as typeof fetch);
|
|
|
|
const settings = {
|
|
megaCredentials: "a@b.de:pw1\nc@d.de:pw2",
|
|
megaPassword: "",
|
|
debridLinkApiKeys: "key1\nkey2\nkey3"
|
|
} as unknown as AppSettings;
|
|
|
|
const result = await checkAllDebridAccounts(settings);
|
|
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);
|
|
});
|
|
|
|
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;
|
|
let maxInFlight = 0;
|
|
vi.stubGlobal("fetch", vi.fn(async () => {
|
|
inFlight += 1;
|
|
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
inFlight -= 1;
|
|
return { ok: true, status: 200, text: async () => JSON.stringify({ success: true, value: { accountType: 1, premiumLeft: 1000 } }) };
|
|
}) as unknown as typeof fetch);
|
|
|
|
const keys = Array.from({ length: 9 }, (_, i) => `key_${i}`).join("\n");
|
|
const settings = { megaCredentials: "", megaPassword: "", debridLinkApiKeys: keys } as unknown as AppSettings;
|
|
|
|
const result = await checkAllDebridAccounts(settings);
|
|
expect(result).toHaveLength(9);
|
|
expect(maxInFlight).toBeLessThanOrEqual(4);
|
|
result.forEach((r, i) => expect(r.label).toBe(`Key ${i + 1}`));
|
|
});
|
|
});
|