import { afterEach, describe, expect, it, vi } from "vitest"; import { MegaWebFallback } from "../src/main/mega-web-fallback"; const originalFetch = globalThis.fetch; describe("mega-web-fallback", () => { afterEach(() => { globalThis.fetch = originalFetch; vi.restoreAllMocks(); }); describe("MegaWebFallback class", () => { it("returns null when credentials are empty", async () => { const fallback = new MegaWebFallback(() => ({ login: "", password: "" })); const result = await fallback.unrestrict("https://mega.debrid/test"); expect(result).toBeNull(); }); it("logs in, fetches HTML, parses code, and polls AJAX for direct url", async () => { let fetchCallCount = 0; globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const urlStr = String(url); fetchCallCount += 1; if (urlStr.includes("form=login")) { const headers = new Headers(); headers.append("set-cookie", "session=goodcookie; path=/"); return new Response("", { headers, status: 200 }); } if (urlStr.includes("page=debrideur")) { return new Response('
', { status: 200 }); } if (urlStr.includes("form=debrid")) { return new Response(`

Link: https://mega.debrid/link1

Download
`, { status: 200 }); } if (urlStr.includes("ajax=debrid")) { return new Response(JSON.stringify({ link: "https://mega.direct/123" }), { status: 200 }); } return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" })); const result = await fallback.unrestrict("https://mega.debrid/link1"); expect(result).not.toBeNull(); expect(result?.directUrl).toBe("https://mega.direct/123"); expect(result?.fileName).toBe("link1"); expect(fetchCallCount).toBe(4); }); it("fails fast on 'Kein Server für diesen Hoster' (account hoster quota) instead of re-login + re-poll", async () => { let ajaxCalls = 0; globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const urlStr = String(url); if (urlStr.includes("form=login")) { const headers = new Headers(); headers.append("set-cookie", "session=goodcookie; path=/"); return new Response("", { headers, status: 200 }); } if (urlStr.includes("page=debrideur")) { return new Response('
', { status: 200 }); } if (urlStr.includes("form=debrid")) { return new Response(`

Link: https://mega.debrid/l1

d
`, { status: 200 }); } if (urlStr.includes("ajax=debrid")) { ajaxCalls += 1; return new Response(JSON.stringify({ link: "", text: "Erreur : Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal." }), { status: 200 }); } return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" })); await expect(fallback.unrestrict("https://mega.debrid/l1")).rejects.toThrow(/kein server für diesen hoster/i); expect(ajaxCalls).toBe(1); }); it("surfaces 'Kein Server für diesen Hoster' from the debrid PAGE (daily limit, no debrid code) instead of empty", async () => { let ajaxCalls = 0; globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const urlStr = String(url); if (urlStr.includes("form=login")) { const headers = new Headers(); headers.append("set-cookie", "session=goodcookie; path=/"); return new Response("", { headers, status: 200 }); } if (urlStr.includes("page=debrideur")) { return new Response('
', { status: 200 }); } if (urlStr.includes("form=debrid")) { return new Response('
Erreur : Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal.
', { status: 200 }); } if (urlStr.includes("ajax=debrid")) { ajaxCalls += 1; return new Response(JSON.stringify({ link: "https://should.not/happen" }), { status: 200 }); } return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" })); await expect(fallback.unrestrict("https://mega.debrid/l1")).rejects.toThrow(/kein server für diesen hoster/i); expect(ajaxCalls).toBe(0); }); it("logs in with the per-account credentials passed to unrestrict, not the default", async () => { const loginsUsed: string[] = []; globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: { body?: unknown }) => { const urlStr = String(url); if (urlStr.includes("form=login")) { const params = new URLSearchParams(String(opts?.body ?? "")); loginsUsed.push(params.get("login") || ""); const headers = new Headers(); headers.append("set-cookie", "session=goodcookie; path=/"); return new Response("", { headers, status: 200 }); } if (urlStr.includes("page=debrideur")) { return new Response('
', { status: 200 }); } if (urlStr.includes("form=debrid")) { return new Response(`

Link: https://mega.debrid/l1

d
`, { status: 200 }); } if (urlStr.includes("ajax=debrid")) { return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 }); } return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "defaultacc", password: "defpw" })); const result = await fallback.unrestrict("https://mega.debrid/l1", undefined, { login: "account2", password: "pw2" }); expect(result?.directUrl).toBe("https://mega.direct/ok"); expect(loginsUsed).toContain("account2"); expect(loginsUsed).not.toContain("defaultacc"); }); it("throws if login fails to set cookie", async () => { globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const urlStr = String(url); if (urlStr.includes("form=login")) { const headers = new Headers(); return new Response("", { headers, status: 200 }); } return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "bad", password: "bad" })); await expect(fallback.unrestrict("http://mega.debrid/file")) .rejects.toThrow("Mega-Web Login liefert kein Session-Cookie"); }); it("throws if login verify check fails (no form found)", async () => { globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const urlStr = String(url); if (urlStr.includes("form=login")) { const headers = new Headers(); headers.append("set-cookie", "session=goodcookie; path=/"); return new Response("", { headers, status: 200 }); } if (urlStr.includes("page=debrideur")) { return new Response('Nothing here', { status: 200 }); } return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "a", password: "b" })); await expect(fallback.unrestrict("http://mega.debrid/file")) .rejects.toThrow("Mega-Web Login ungültig oder Session blockiert"); }); it("returns null if generation fails to find a code", async () => { let callCount = 0; globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const urlStr = String(url); callCount++; if (urlStr.includes("form=login")) { const headers = new Headers(); headers.append("set-cookie", "session=goodcookie; path=/"); return new Response("", { headers, status: 200 }); } if (urlStr.includes("page=debrideur")) { return new Response('
', { status: 200 }); } if (urlStr.includes("form=debrid")) { return new Response(`
No links here
`, { status: 200 }); } return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "a", password: "b" })); const result = await fallback.unrestrict("http://mega.debrid/file"); expect(result).toBeNull(); }); it("serialisiert gleichzeitige Umwandlungen auf DEMSELBEN Account (kein Doppel-Login)", async () => { let loginCount = 0; globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const u = String(url); if (u.includes("form=login")) { loginCount += 1; await new Promise((r) => setTimeout(r, 15)); const headers = new Headers(); headers.append("set-cookie", "session=c; path=/"); return new Response("", { headers, status: 200 }); } if (u.includes("page=debrideur")) return new Response('
', { status: 200 }); if (u.includes("form=debrid")) return new Response(`

Link: https://mega.debrid/l

d
`, { status: 200 }); if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 }); return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "same", password: "pw" })); const [r1, r2] = await Promise.all([ fallback.unrestrict("https://mega.debrid/a", undefined, { login: "same", password: "pw" }), fallback.unrestrict("https://mega.debrid/b", undefined, { login: "same", password: "pw" }) ]); expect(r1?.directUrl).toBe("https://mega.direct/ok"); expect(r2?.directUrl).toBe("https://mega.direct/ok"); // Serialisiert auf demselben Account → der zweite nutzt die gecachte Session, kein zweiter Login. expect(loginCount).toBe(1); }); it("wandelt auf VERSCHIEDENEN Accounts parallel um (Logins laufen gleichzeitig)", async () => { let activeLogins = 0; let maxActiveLogins = 0; const bothInFlight = new Promise((resolve) => { const check = setInterval(() => { if (activeLogins >= 2) { clearInterval(check); resolve(); } }, 5); }); globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const u = String(url); if (u.includes("form=login")) { activeLogins += 1; maxActiveLogins = Math.max(maxActiveLogins, activeLogins); await bothInFlight; activeLogins -= 1; const headers = new Headers(); headers.append("set-cookie", "session=c; path=/"); return new Response("", { headers, status: 200 }); } if (u.includes("page=debrideur")) return new Response('
', { status: 200 }); if (u.includes("form=debrid")) return new Response(`

Link: https://mega.debrid/l

d
`, { status: 200 }); if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 }); return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "d", password: "p" })); const [r1, r2] = await Promise.all([ fallback.unrestrict("https://mega.debrid/a", undefined, { login: "acc1", password: "p" }), fallback.unrestrict("https://mega.debrid/b", undefined, { login: "acc2", password: "p" }) ]); expect(r1?.directUrl).toBe("https://mega.direct/ok"); expect(r2?.directUrl).toBe("https://mega.direct/ok"); // Verschiedene Accounts → beide Logins gleichzeitig in-flight (sonst haengt es am bothInFlight-Barrier). expect(maxActiveLogins).toBe(2); }, 10000); it("aborts pending Mega-Web polling when signal is cancelled", async () => { globalThis.fetch = vi.fn((url: string | URL | Request, init?: RequestInit): Promise => { const urlStr = String(url); if (urlStr.includes("form=login")) { const headers = new Headers(); headers.append("set-cookie", "session=goodcookie; path=/"); return Promise.resolve(new Response("", { headers, status: 200 })); } if (urlStr.includes("page=debrideur")) { return Promise.resolve(new Response('
', { status: 200 })); } if (urlStr.includes("form=debrid")) { return Promise.resolve(new Response(`

Link: https://mega.debrid/link2

Download
`, { status: 200 })); } if (urlStr.includes("ajax=debrid")) { return new Promise((_resolve, reject) => { const signal = init?.signal; const onAbort = (): void => reject(new Error("aborted:ajax")); if (signal?.aborted) { onAbort(); return; } signal?.addEventListener("abort", onAbort, { once: true }); }); } return Promise.resolve(new Response("Not found", { status: 404 })); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" })); const controller = new AbortController(); const timer = setTimeout(() => { controller.abort("test"); }, 200); try { await expect(fallback.unrestrict("https://mega.debrid/link2", controller.signal)).rejects.toThrow(/aborted/i); } finally { clearTimeout(timer); } }); it("klassifiziert einen Abbruch WAEHREND in der Queue als Queue-Timeout (nicht harter Abbruch), damit der belegte Account nicht bestraft wird", async () => { let releaseLogin: () => void = () => {}; const loginGate = new Promise((resolve) => { releaseLogin = resolve; }); globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const u = String(url); if (u.includes("form=login")) { await loginGate; const headers = new Headers(); headers.append("set-cookie", "session=c; path=/"); return new Response("", { headers, status: 200 }); } if (u.includes("page=debrideur")) return new Response('
', { status: 200 }); if (u.includes("form=debrid")) return new Response(`

Link: https://mega.debrid/l

d
`, { status: 200 }); if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 }); return new Response("Not found", { status: 404 }); }) as unknown as typeof fetch; const fallback = new MegaWebFallback(() => ({ login: "same", password: "pw" })); const firstCtrl = new AbortController(); const first = fallback.unrestrict("https://mega.debrid/a", firstCtrl.signal, { login: "same", password: "pw" }); await new Promise((r) => setTimeout(r, 25)); const secondCtrl = new AbortController(); const second = fallback.unrestrict("https://mega.debrid/b", secondCtrl.signal, { login: "same", password: "pw" }); await new Promise((r) => setTimeout(r, 25)); secondCtrl.abort("caller-timeout"); await expect(second).rejects.toThrow(/queue.?timeout/i); releaseLogin(); await first.catch(() => null); await new Promise((r) => setTimeout(r, 20)); }, 10000); }); });