fix(realdebrid): persist web status and report relevant provider errors
Check Real-Debrid API and browser sessions through the account status flow, retain the service status across settings updates, and refresh the account row when a browser login is detected. Exclude unavailable providers that were never attempted from conversion failures and prevent aggregated fallback text from being mislabeled as a Debrid-Link terminal error. Bump the development version to 2.0.37 and add focused regression coverage.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { checkMegaDebridAccount, checkDebridLinkKey, checkAllDebridAccounts } from "../src/main/account-check";
|
||||
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";
|
||||
@@ -78,7 +78,7 @@ describe("checkMegaDebridAccount", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkDebridLinkKey", () => {
|
||||
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 } });
|
||||
@@ -108,14 +108,61 @@ describe("checkDebridLinkKey", () => {
|
||||
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 () => {
|
||||
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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyAccountCommand, validateAccountCommand } from "../src/main/account-commands";
|
||||
import { applyAccountCommand, validateAccountCommand, validateAccountCredentialCheckInput } 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";
|
||||
@@ -42,6 +42,15 @@ const ACCOUNT_KINDS: RendererAccountKind[] = [
|
||||
];
|
||||
|
||||
describe("write-only account commands", () => {
|
||||
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.each([
|
||||
["realdebrid-api", "", "fixture-rd-provider-secret-1fA4", "token"],
|
||||
["realdebrid-web", "", "", "realDebridUseWebLogin"],
|
||||
|
||||
+48
-3
@@ -72,7 +72,7 @@ describe("debrid service", () => {
|
||||
expect(megaWeb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not fallback when auto fallback is disabled", async () => {
|
||||
it("does not fallback when auto fallback is disabled", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
@@ -102,8 +102,53 @@ describe("debrid service", () => {
|
||||
|
||||
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||
await expect(service.unrestrictLink("https://rapidgator.net/file/example.part2.rar.html")).rejects.toThrow();
|
||||
expect(megaWeb).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
expect(megaWeb).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("reports only providers that were actually attempted", async () => {
|
||||
const megaLogin = "disabled@example.test";
|
||||
const debridLinkKeys = parseDebridLinkApiKeys("disabled-dl-key");
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
megaDebridApiCredentials: `${megaLogin}:password`,
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridApiDisabledAccountIds: [getMegaDebridAccountId(megaLogin)],
|
||||
debridLinkApiKeys: "disabled-dl-key",
|
||||
debridLinkApiKeyDailyLimitBytes: { [debridLinkKeys[0].id]: 1 },
|
||||
debridLinkApiKeyDailyUsageBytes: { [debridLinkKeys[0].id]: 1 },
|
||||
providerDailyUsageDay: getProviderUsageDayKey(),
|
||||
providerOrder: ["megadebrid-api", "debridlink", "realdebrid"] as const,
|
||||
providerPrimary: "megadebrid-api" as const,
|
||||
providerSecondary: "debridlink" as const,
|
||||
providerTertiary: "realdebrid" as const,
|
||||
autoProviderFallback: true
|
||||
};
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) {
|
||||
return new Response(JSON.stringify({ error: "traffic_exhausted" }), {
|
||||
status: 429,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const service = new DebridService(settings);
|
||||
let message = "";
|
||||
try {
|
||||
await service.unrestrictLink("https://hoster.example/realdebrid-limit.bin");
|
||||
} catch (error) {
|
||||
message = String(error);
|
||||
}
|
||||
|
||||
expect(message).toContain("Real-Debrid");
|
||||
expect(message).toContain("traffic_exhausted");
|
||||
expect(message).not.toContain("Mega-Debrid nicht verfuegbar");
|
||||
expect(message).not.toContain("Debrid-Link nicht verfuegbar");
|
||||
});
|
||||
|
||||
it("skips a provider whose daily limit is already reached and uses the next provider", async () => {
|
||||
const calledUrls: string[] = [];
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseDebridLinkTerminalFailure } from "../src/main/download-manager";
|
||||
|
||||
describe("provider error classification", () => {
|
||||
it("does not relabel an aggregated Real-Debrid failure as Debrid-Link", () => {
|
||||
const message = "Error: Unrestrict fehlgeschlagen: Mega-Debrid nicht verfuegbar (alle aktiven Accounts deaktiviert oder ausgeschopft) | Debrid-Link nicht verfuegbar (alle aktiven API-Keys deaktiviert oder ausgeschopft) | Real-Debrid: traffic_exhausted";
|
||||
|
||||
expect(parseDebridLinkTerminalFailure(message)).toBeNull();
|
||||
});
|
||||
|
||||
it("still recognizes a direct Debrid-Link terminal failure", () => {
|
||||
expect(parseDebridLinkTerminalFailure("Debrid-Link nicht verfuegbar: kein aktiver API-Key")).toMatchObject({
|
||||
kind: "no_active_key"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -120,7 +120,7 @@ describe("realdebrid-web", () => {
|
||||
.toBe("ghi789");
|
||||
});
|
||||
|
||||
it("uses the already logged-in browser window to warm the token cache before unrestricting", async () => {
|
||||
it("uses the already logged-in browser window to warm the token cache before unrestricting", async () => {
|
||||
const apiFetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({
|
||||
download: "https://cdn.real-debrid.example/file.bin",
|
||||
filename: "file.bin",
|
||||
@@ -160,6 +160,46 @@ describe("realdebrid-web", () => {
|
||||
expect(mockSessionFetch).not.toHaveBeenCalled();
|
||||
expect(apiFetch).toHaveBeenCalledTimes(1);
|
||||
expect(apiFetch.mock.calls[0]?.[0]).toBe("https://api.real-debrid.com/rest/1.0/unrestrict/link");
|
||||
expect(mockBrowserWindow.webContents.executeJavaScript).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(mockBrowserWindow.webContents.executeJavaScript).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("checks the logged-in browser account without exposing its token", async () => {
|
||||
mockExecuteJavaScript.mockResolvedValue("token-from-window");
|
||||
const apiFetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({
|
||||
username: "web-user",
|
||||
email: "web-user@example.test",
|
||||
type: "premium",
|
||||
expiration: "2030-01-02T03:04:05.000Z"
|
||||
}), { status: 200 }));
|
||||
vi.stubGlobal("fetch", apiFetch);
|
||||
|
||||
const fallback = new RealDebridWebFallback(() => true);
|
||||
await fallback.openLoginWindow();
|
||||
const status = await fallback.probeLoginState();
|
||||
|
||||
expect(status).toEqual({
|
||||
valid: true,
|
||||
username: "web-user",
|
||||
email: "web-user@example.test",
|
||||
isPremium: true,
|
||||
premiumUntilMs: Date.parse("2030-01-02T03:04:05.000Z"),
|
||||
message: "Premium aktiv"
|
||||
});
|
||||
expect(JSON.stringify(status)).not.toContain("token-from-window");
|
||||
expect(apiFetch).toHaveBeenCalledWith(
|
||||
"https://api.real-debrid.com/rest/1.0/user",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: "Bearer token-from-window" })
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("notifies the controller when a new browser token is detected", async () => {
|
||||
mockExecuteJavaScript.mockResolvedValue("new-browser-token");
|
||||
const onAuthenticated = vi.fn();
|
||||
const fallback = new RealDebridWebFallback(() => true, onAuthenticated);
|
||||
|
||||
await fallback.openLoginWindow();
|
||||
await vi.waitFor(() => expect(onAuthenticated).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,4 +44,31 @@ describe("live settings overlay", () => {
|
||||
expect(Object.keys(target.debridAccountStatuses).sort()).toEqual([keepKeyId, keepMegaId].sort());
|
||||
expect(target.totalRuntimeAllTimeMs).toBe(9_000);
|
||||
});
|
||||
|
||||
it("keeps the Real-Debrid service status while the browser account remains configured", () => {
|
||||
const target = {
|
||||
...defaultSettings(),
|
||||
realDebridUseWebLogin: true
|
||||
};
|
||||
const live = {
|
||||
...target,
|
||||
debridAccountStatuses: {
|
||||
"svc-realdebrid": {
|
||||
accountId: "svc-realdebrid",
|
||||
provider: "realdebrid" as const,
|
||||
label: "Real-Debrid",
|
||||
maskedLogin: "Browser-Login",
|
||||
valid: true,
|
||||
isPremium: true,
|
||||
premiumUntilMs: null,
|
||||
message: "Premium aktiv",
|
||||
checkedAt: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
overlayLiveUsageCounters(target, live, 9_000);
|
||||
|
||||
expect(target.debridAccountStatuses["svc-realdebrid"]).toEqual(live.debridAccountStatuses["svc-realdebrid"]);
|
||||
});
|
||||
});
|
||||
|
||||
+32
-3
@@ -635,7 +635,7 @@ describe("settings storage", () => {
|
||||
expect(normalized.archivePasswordList).toBe("one\ntwo\nthree");
|
||||
});
|
||||
|
||||
it("defaults Real-Debrid web login to disabled and normalizes the flag", () => {
|
||||
it("defaults Real-Debrid web login to disabled and normalizes the flag", () => {
|
||||
expect(defaultSettings().realDebridUseWebLogin).toBe(false);
|
||||
|
||||
const normalizedEnabled = normalizeSettings({
|
||||
@@ -648,8 +648,37 @@ describe("settings storage", () => {
|
||||
...defaultSettings(),
|
||||
realDebridUseWebLogin: 0 as unknown as boolean
|
||||
});
|
||||
expect(normalizedDisabled.realDebridUseWebLogin).toBe(false);
|
||||
});
|
||||
expect(normalizedDisabled.realDebridUseWebLogin).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the Real-Debrid service status while web login remains configured", () => {
|
||||
const checkedAt = Date.now();
|
||||
const normalized = normalizeSettings({
|
||||
...defaultSettings(),
|
||||
realDebridUseWebLogin: true,
|
||||
debridAccountStatuses: {
|
||||
"svc-realdebrid": {
|
||||
accountId: "svc-realdebrid",
|
||||
provider: "realdebrid",
|
||||
label: "Real-Debrid",
|
||||
maskedLogin: "Browser-Login",
|
||||
valid: true,
|
||||
isPremium: true,
|
||||
premiumUntilMs: checkedAt + 1000,
|
||||
email: "web-user",
|
||||
message: "Premium aktiv",
|
||||
checkedAt
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(normalized.debridAccountStatuses["svc-realdebrid"]).toMatchObject({
|
||||
accountId: "svc-realdebrid",
|
||||
provider: "realdebrid",
|
||||
valid: true,
|
||||
checkedAt
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults AllDebrid web login to disabled and normalizes the flag", () => {
|
||||
expect(defaultSettings().allDebridUseWebLogin).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user