Files
Multi-Debrid-Downloader/tests/bestdebrid-web.test.ts
Sucukdeluxe f57b513625 Align provider selection and abort observation
Share one pure provider selection plan between real unrestrict routing and cooldown projection, including direct 1Fichier and DDownload paths, disabled Mega aliases, and the secondary-provider exception used when Real-Debrid is cooling down. Attach terminal observers to raw serialized web jobs before evaluating already-aborted signals so late rejections stay handled while the queue remains available. Add RED-to-GREEN regressions for each provider-selection counterexample and pre-aborted Real-Debrid, AllDebrid, and BestDebrid jobs.
2026-08-22 11:05:42 +02:00

248 lines
8.6 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
mockCookiesSet,
mockFetch,
mockClearStorageData,
mockClearCache,
mockFromPartition,
mockSession
} = vi.hoisted(() => {
const cookiesSet = vi.fn();
const fetch = vi.fn();
const clearStorageData = vi.fn();
const clearCache = vi.fn();
const fromPartition = vi.fn();
return {
mockCookiesSet: cookiesSet,
mockFetch: fetch,
mockClearStorageData: clearStorageData,
mockClearCache: clearCache,
mockFromPartition: fromPartition,
mockSession: {
cookies: {
set: cookiesSet
},
fetch,
clearStorageData,
clearCache
}
};
});
vi.mock("electron", () => ({
session: {
fromPartition: mockFromPartition
}
}));
vi.mock("../src/main/logger", () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}
}));
import { BestDebridWebFallback } from "../src/main/bestdebrid-web";
function createCookieFile(contents: string): string {
const filePath = path.join(os.tmpdir(), `bestdebrid-cookies-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
fs.writeFileSync(filePath, contents, "utf8");
return filePath;
}
describe("bestdebrid-web", () => {
const tempFiles: string[] = [];
beforeEach(() => {
mockFromPartition.mockReturnValue(mockSession);
});
afterEach(() => {
vi.clearAllMocks();
mockFromPartition.mockReturnValue(mockSession);
while (tempFiles.length > 0) {
const filePath = tempFiles.pop();
if (!filePath) {
continue;
}
try {
fs.rmSync(filePath, { force: true });
} catch {
}
}
});
it("imports HttpOnly Netscape cookies instead of skipping them as comments", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
"#HttpOnly_.bestdebrid.com\tTRUE\t/\tTRUE\t1803585385\tPHPSESSID\tsecret-session",
".bestdebrid.com\tTRUE\t/\tFALSE\t1806720721\t_ga\ttracking"
].join("\n"));
tempFiles.push(filePath);
const fallback = new BestDebridWebFallback(() => true);
const count = await fallback.importCookiesFromFile(filePath);
expect(count).toBe(2);
expect(mockClearStorageData).toHaveBeenCalledTimes(1);
expect(mockClearStorageData).toHaveBeenCalledWith({
storages: ["cookies", "indexdb", "localstorage", "serviceworkers", "cachestorage"]
});
expect(mockCookiesSet).toHaveBeenCalledTimes(2);
expect(mockCookiesSet).toHaveBeenCalledWith(expect.objectContaining({
name: "PHPSESSID",
domain: ".bestdebrid.com",
httpOnly: true,
secure: true
}));
});
it("deduplicates conflicting session cookies and prefers the HttpOnly variant", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
"bestdebrid.com\tFALSE\t/\tTRUE\t1803585384\tPHPSESSID\tnon-http-only",
"#HttpOnly_.bestdebrid.com\tTRUE\t/\tTRUE\t1803585385\tPHPSESSID\thttp-only"
].join("\n"));
tempFiles.push(filePath);
const fallback = new BestDebridWebFallback(() => true);
const count = await fallback.importCookiesFromFile(filePath);
expect(count).toBe(1);
expect(mockCookiesSet).toHaveBeenCalledTimes(1);
expect(mockCookiesSet).toHaveBeenCalledWith(expect.objectContaining({
name: "PHPSESSID",
value: "http-only",
httpOnly: true,
domain: ".bestdebrid.com"
}));
});
it("rejects cookie files that only contain tracking cookies", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
".bestdebrid.com\tTRUE\t/\tTRUE\t1803585385\t__stripe_mid\tstripe",
".bestdebrid.com\tTRUE\t/\tFALSE\t1806720721\t_ga\ttracking"
].join("\n"));
tempFiles.push(filePath);
const fallback = new BestDebridWebFallback(() => true);
await expect(fallback.importCookiesFromFile(filePath))
.rejects.toThrow("Login-Cookie");
expect(mockCookiesSet).not.toHaveBeenCalled();
});
it("treats BestDebrid free-user errors as logged-out sessions when the account page is guest-only", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
"bestdebrid.com\tFALSE\t/\tTRUE\t1803585385\tPHPSESSID\tsecret-session"
].join("\n"));
tempFiles.push(filePath);
mockFetch
.mockResolvedValueOnce(new Response(JSON.stringify({
error: 1,
message: "Free users are not allowed to download using a VPN or proxy. Please purchase a premium plan."
}), { status: 200 }))
.mockResolvedValueOnce(new Response("<div class=\"font-medium\">Guest</div>", { status: 200 }));
const fallback = new BestDebridWebFallback(() => true);
await fallback.importCookiesFromFile(filePath);
await expect(fallback.unrestrict("https://1fichier.com/?abc"))
.rejects.toThrow("Nicht eingeloggt");
await expect(fallback.unrestrict("https://1fichier.com/?abc"))
.rejects.toThrow("Keine Cookies importiert");
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://bestdebrid.com/api/v1/generateLink");
expect(mockFetch.mock.calls[1]?.[0]).toBe("https://bestdebrid.com/en/downloader/");
});
it("releases an aborted caller while the active web request ignores its signal", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
"bestdebrid.com\tFALSE\t/\tTRUE\t1803585385\tPHPSESSID\tsecret-session"
].join("\n"));
tempFiles.push(filePath);
const fallback = new BestDebridWebFallback(() => true);
await fallback.importCookiesFromFile(filePath);
let rejectRequest!: (error: Error) => void;
mockFetch
.mockReturnValueOnce(new Promise<Response>((_resolve, reject) => {
rejectRequest = reject;
}))
.mockResolvedValueOnce(new Response(JSON.stringify({
error: 0,
link: "https://bestdebrid.direct/second.bin",
filename: "second.bin",
size: "444 B"
}), { status: 200 }));
const controller = new AbortController();
const running = fallback.unrestrict("https://1fichier.com/?abort-race", controller.signal)
.then(() => "resolved" as const, (error) => String(error));
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1));
controller.abort("test-stop");
const outcome = await Promise.race([
running,
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 200))
]);
expect(outcome).toContain("aborted:bestdebrid-web");
const secondOutcome = await Promise.race([
fallback.unrestrict("https://1fichier.com/?second"),
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 200))
]);
expect(secondOutcome).toEqual({
directUrl: "https://bestdebrid.direct/second.bin",
fileName: "second.bin",
fileSize: 444,
retriesUsed: 0
});
expect(mockFetch).toHaveBeenCalledTimes(2);
rejectRequest(new Error("late bestdebrid rejection"));
await Promise.resolve();
});
it("observes the raw rejection when the signal is already aborted and keeps the queue usable", async () => {
const filePath = createCookieFile([
"# Netscape HTTP Cookie File",
"bestdebrid.com\tFALSE\t/\tTRUE\t1803585385\tPHPSESSID\tsecret-session"
].join("\n"));
tempFiles.push(filePath);
const fallback = new BestDebridWebFallback(() => true);
await fallback.importCookiesFromFile(filePath);
mockFetch.mockResolvedValue(new Response(JSON.stringify({
error: 0,
link: "https://bestdebrid.direct/next.bin",
filename: "next.bin",
size: "777 B"
}), { status: 200 }));
const controller = new AbortController();
controller.abort("before-queue");
const unhandled: unknown[] = [];
const onUnhandled = (reason: unknown) => unhandled.push(reason);
process.on("unhandledRejection", onUnhandled);
try {
await expect(fallback.unrestrict("https://1fichier.com/?pre-aborted", controller.signal))
.rejects.toThrow("aborted:bestdebrid-web");
await new Promise((resolve) => setImmediate(resolve));
await expect(fallback.unrestrict("https://1fichier.com/?next")).resolves.toMatchObject({
directUrl: "https://bestdebrid.direct/next.bin",
fileName: "next.bin"
});
expect(unhandled).toEqual([]);
expect(mockFetch).toHaveBeenCalledTimes(1);
} finally {
process.off("unhandledRejection", onUnhandled);
}
});
});