Apply account enablement changes optimistically and persist rapid toggles sequentially without losing clicks. Refresh Mega-Debrid API and Web account pools during active downloads, isolate per-account cancellation budgets, rotate within the same conversion attempt, and keep pause, stop, resume, reset, and no-account gates consistent. Use the native Electron clipboard under Remote Desktop and make support bundle export bounded, redacted, responsive, guarded, visible, and atomic. Add end-to-end regressions for live account switching, persisted partial recovery, support export, native copy, availability preservation, and the 500 ms telemetry cadence.
629 lines
30 KiB
TypeScript
629 lines
30 KiB
TypeScript
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("does not restore an invalidated session when an older login finishes later", async () => {
|
|
let finishLogin: (cookie: string) => void = () => {};
|
|
const loginResult = new Promise<string>((resolve) => {
|
|
finishLogin = resolve;
|
|
});
|
|
const fallback = new MegaWebFallback(() => ({ login: "old-user", password: "old-pass" }));
|
|
const internals = fallback as unknown as {
|
|
ensureSession: (key: string, login: string, password: string) => Promise<string>;
|
|
login: (login: string, password: string) => Promise<string>;
|
|
sessions: Map<string, { cookie: string; setAt: number }>;
|
|
};
|
|
vi.spyOn(internals, "login").mockReturnValue(loginResult);
|
|
|
|
const pending = internals.ensureSession("old-user", "old-user", "old-pass");
|
|
await Promise.resolve();
|
|
fallback.invalidateSession();
|
|
finishLogin("stale-old-cookie");
|
|
|
|
await expect(pending).resolves.toBe("stale-old-cookie");
|
|
expect(internals.sessions.size).toBe(0);
|
|
});
|
|
|
|
it("does not cache an old session when an invalidated request retries login", async () => {
|
|
const fallback = new MegaWebFallback(() => ({ login: "old-user", password: "old-pass" }));
|
|
const internals = fallback as unknown as {
|
|
login: (login: string, password: string) => Promise<string>;
|
|
generate: (link: string, cookie: string) => Promise<{ directUrl: string; fileName: string } | null>;
|
|
sessions: Map<string, { cookie: string; setAt: number }>;
|
|
};
|
|
const login = vi.spyOn(internals, "login")
|
|
.mockResolvedValueOnce("first-stale-cookie")
|
|
.mockResolvedValueOnce("second-stale-cookie");
|
|
vi.spyOn(internals, "generate")
|
|
.mockImplementationOnce(async () => {
|
|
fallback.invalidateSession();
|
|
return null;
|
|
})
|
|
.mockResolvedValueOnce({ directUrl: "https://mega.direct/retry", fileName: "retry.bin" });
|
|
|
|
await expect(fallback.unrestrict("https://mega.debrid/retry", undefined, { login: "old-user", password: "old-pass" }))
|
|
.rejects.toThrow(/aborted/i);
|
|
|
|
expect(login).toHaveBeenCalledTimes(1);
|
|
expect(internals.sessions.size).toBe(0);
|
|
});
|
|
|
|
it("keeps a fresh session when an invalidated older request finishes later", async () => {
|
|
let releaseOldGenerate: () => void = () => {};
|
|
let markOldGenerateStarted: () => void = () => {};
|
|
const oldGenerateGate = new Promise<void>((resolve) => {
|
|
releaseOldGenerate = resolve;
|
|
});
|
|
const oldGenerateStarted = new Promise<void>((resolve) => {
|
|
markOldGenerateStarted = resolve;
|
|
});
|
|
const fallback = new MegaWebFallback(() => ({ login: "same-user", password: "same-pass" }));
|
|
const internals = fallback as unknown as {
|
|
login: (login: string, password: string, signal?: AbortSignal) => Promise<string>;
|
|
generate: (link: string, cookie: string, signal?: AbortSignal) => Promise<{ directUrl: string; fileName: string } | null>;
|
|
sessions: Map<string, { cookie: string; setAt: number }>;
|
|
};
|
|
const login = vi.spyOn(internals, "login")
|
|
.mockResolvedValueOnce("old-cookie")
|
|
.mockResolvedValueOnce("fresh-cookie");
|
|
vi.spyOn(internals, "generate").mockImplementation(async (link) => {
|
|
if (link.includes("old")) {
|
|
markOldGenerateStarted();
|
|
await oldGenerateGate;
|
|
return null;
|
|
}
|
|
return { directUrl: "https://mega.direct/fresh", fileName: "fresh.bin" };
|
|
});
|
|
|
|
const oldRequest = fallback.unrestrict("https://mega.debrid/old", undefined, { login: "same-user", password: "same-pass" });
|
|
const oldSettled = oldRequest.catch((error: unknown) => error);
|
|
await oldGenerateStarted;
|
|
fallback.invalidateSession();
|
|
const freshRequest = fallback.unrestrict("https://mega.debrid/fresh", undefined, { login: "same-user", password: "same-pass" });
|
|
|
|
try {
|
|
await expect(freshRequest).resolves.toMatchObject({ directUrl: "https://mega.direct/fresh" });
|
|
expect(internals.sessions.get("same-user")?.cookie).toBe("fresh-cookie");
|
|
} finally {
|
|
releaseOldGenerate();
|
|
}
|
|
|
|
await oldSettled;
|
|
expect(internals.sessions.get("same-user")?.cookie).toBe("fresh-cookie");
|
|
expect(login).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("does not cache old credentials from a queued request after invalidation", async () => {
|
|
let releaseFirstLogin: () => void = () => {};
|
|
let markFirstLoginStarted: () => void = () => {};
|
|
const firstLoginGate = new Promise<void>((resolve) => {
|
|
releaseFirstLogin = resolve;
|
|
});
|
|
const firstLoginStarted = new Promise<void>((resolve) => {
|
|
markFirstLoginStarted = resolve;
|
|
});
|
|
const fallback = new MegaWebFallback(() => ({ login: "old-user", password: "old-pass" }));
|
|
const internals = fallback as unknown as {
|
|
login: (login: string, password: string) => Promise<string>;
|
|
generate: (link: string, cookie: string) => Promise<{ directUrl: string; fileName: string }>;
|
|
sessions: Map<string, { cookie: string; setAt: number }>;
|
|
};
|
|
let loginCount = 0;
|
|
vi.spyOn(internals, "login").mockImplementation(async () => {
|
|
loginCount += 1;
|
|
if (loginCount === 1) {
|
|
markFirstLoginStarted();
|
|
await firstLoginGate;
|
|
}
|
|
return `old-cookie-${loginCount}`;
|
|
});
|
|
vi.spyOn(internals, "generate").mockResolvedValue({ directUrl: "https://mega.direct/queued", fileName: "queued.bin" });
|
|
|
|
const first = fallback.unrestrict("https://mega.debrid/first", undefined, { login: "old-user", password: "old-pass" });
|
|
const firstSettled = first.catch((error: unknown) => error);
|
|
await firstLoginStarted;
|
|
const queued = fallback.unrestrict("https://mega.debrid/queued", undefined, { login: "old-user", password: "old-pass" });
|
|
const queuedSettled = queued.catch((error: unknown) => error);
|
|
fallback.invalidateSession();
|
|
releaseFirstLogin();
|
|
|
|
await expect(firstSettled).resolves.toBeInstanceOf(Error);
|
|
await expect(queuedSettled).resolves.toBeInstanceOf(Error);
|
|
expect(loginCount).toBe(1);
|
|
expect(internals.sessions.size).toBe(0);
|
|
});
|
|
|
|
it("does not let a blocked old account job block a new request after session invalidation", async () => {
|
|
let releaseOldLogin: () => void = () => {};
|
|
let markOldLoginStarted: () => void = () => {};
|
|
const oldLoginGate = new Promise<void>((resolve) => {
|
|
releaseOldLogin = resolve;
|
|
});
|
|
const oldLoginStarted = new Promise<void>((resolve) => {
|
|
markOldLoginStarted = resolve;
|
|
});
|
|
const fallback = new MegaWebFallback(() => ({ login: "same-user", password: "same-pass" }));
|
|
const internals = fallback as unknown as {
|
|
login: (login: string, password: string, signal?: AbortSignal) => Promise<string>;
|
|
generate: (link: string, cookie: string, signal?: AbortSignal) => Promise<{ directUrl: string; fileName: string }>;
|
|
};
|
|
const login = vi.spyOn(internals, "login")
|
|
.mockImplementationOnce(async () => {
|
|
markOldLoginStarted();
|
|
await oldLoginGate;
|
|
return "old-cookie";
|
|
})
|
|
.mockResolvedValueOnce("new-cookie");
|
|
vi.spyOn(internals, "generate").mockImplementation(async (link) => ({
|
|
directUrl: link.includes("fresh") ? "https://mega.direct/fresh" : "https://mega.direct/old",
|
|
fileName: link.includes("fresh") ? "fresh.bin" : "old.bin"
|
|
}));
|
|
|
|
const oldRequest = fallback.unrestrict("https://mega.debrid/old", undefined, { login: "same-user", password: "same-pass" });
|
|
const oldSettled = oldRequest.catch((error: unknown) => error);
|
|
await oldLoginStarted;
|
|
fallback.invalidateSession();
|
|
const freshRequest = fallback.unrestrict("https://mega.debrid/fresh", undefined, { login: "same-user", password: "same-pass" });
|
|
|
|
try {
|
|
await vi.waitFor(() => expect(login).toHaveBeenCalledTimes(2), { timeout: 500, interval: 5 });
|
|
await expect(freshRequest).resolves.toMatchObject({
|
|
directUrl: "https://mega.direct/fresh",
|
|
fileName: "fresh.bin"
|
|
});
|
|
} finally {
|
|
releaseOldLogin();
|
|
await Promise.allSettled([oldSettled, freshRequest]);
|
|
}
|
|
});
|
|
|
|
it("does not execute provider work for a queued request aborted before it starts", async () => {
|
|
let releaseFirstLogin: () => void = () => {};
|
|
let markFirstLoginStarted: () => void = () => {};
|
|
const firstLoginGate = new Promise<void>((resolve) => {
|
|
releaseFirstLogin = resolve;
|
|
});
|
|
const firstLoginStarted = new Promise<void>((resolve) => {
|
|
markFirstLoginStarted = resolve;
|
|
});
|
|
const generatedLinks: string[] = [];
|
|
const fallback = new MegaWebFallback(() => ({ login: "same-user", password: "same-pass" }));
|
|
const internals = fallback as unknown as {
|
|
login: (login: string, password: string, signal?: AbortSignal) => Promise<string>;
|
|
generate: (link: string, cookie: string, signal?: AbortSignal) => Promise<{ directUrl: string; fileName: string }>;
|
|
queues: Map<string, Promise<unknown>>;
|
|
};
|
|
const login = vi.spyOn(internals, "login").mockImplementation(async () => {
|
|
markFirstLoginStarted();
|
|
await firstLoginGate;
|
|
return "first-cookie";
|
|
});
|
|
vi.spyOn(internals, "generate").mockImplementation(async (link) => {
|
|
generatedLinks.push(link);
|
|
return { directUrl: "https://mega.direct/first", fileName: "first.bin" };
|
|
});
|
|
|
|
const firstRequest = fallback.unrestrict("https://mega.debrid/first", undefined, { login: "same-user", password: "same-pass" });
|
|
await firstLoginStarted;
|
|
const queuedController = new AbortController();
|
|
const queuedRequest = fallback.unrestrict("https://mega.debrid/aborted", queuedController.signal, { login: "same-user", password: "same-pass" });
|
|
const queueTail = internals.queues.get("same-user");
|
|
queuedController.abort("cancel-before-start");
|
|
|
|
await expect(queuedRequest).rejects.toThrow(/queue.?timeout/i);
|
|
releaseFirstLogin();
|
|
await expect(firstRequest).resolves.toMatchObject({ directUrl: "https://mega.direct/first" });
|
|
await queueTail;
|
|
|
|
expect(login).toHaveBeenCalledTimes(1);
|
|
expect(generatedLinks).toEqual(["https://mega.debrid/first"]);
|
|
});
|
|
|
|
it("starts a new request normally after invalidating a queue with an aborted waiter", async () => {
|
|
let releaseOldLogin: () => void = () => {};
|
|
let markOldLoginStarted: () => void = () => {};
|
|
const oldLoginGate = new Promise<void>((resolve) => {
|
|
releaseOldLogin = resolve;
|
|
});
|
|
const oldLoginStarted = new Promise<void>((resolve) => {
|
|
markOldLoginStarted = resolve;
|
|
});
|
|
const generatedLinks: string[] = [];
|
|
const fallback = new MegaWebFallback(() => ({ login: "same-user", password: "same-pass" }));
|
|
const internals = fallback as unknown as {
|
|
login: (login: string, password: string, signal?: AbortSignal) => Promise<string>;
|
|
generate: (link: string, cookie: string, signal?: AbortSignal) => Promise<{ directUrl: string; fileName: string }>;
|
|
queues: Map<string, Promise<unknown>>;
|
|
};
|
|
const login = vi.spyOn(internals, "login")
|
|
.mockImplementationOnce(async () => {
|
|
markOldLoginStarted();
|
|
await oldLoginGate;
|
|
return "old-cookie";
|
|
})
|
|
.mockResolvedValueOnce("fresh-cookie");
|
|
vi.spyOn(internals, "generate").mockImplementation(async (link) => {
|
|
generatedLinks.push(link);
|
|
return {
|
|
directUrl: link.includes("fresh") ? "https://mega.direct/fresh" : "https://mega.direct/old",
|
|
fileName: link.includes("fresh") ? "fresh.bin" : "old.bin"
|
|
};
|
|
});
|
|
|
|
const oldRequest = fallback.unrestrict("https://mega.debrid/old", undefined, { login: "same-user", password: "same-pass" });
|
|
const oldSettled = oldRequest.catch((error: unknown) => error);
|
|
await oldLoginStarted;
|
|
const queuedController = new AbortController();
|
|
const queuedRequest = fallback.unrestrict("https://mega.debrid/aborted", queuedController.signal, { login: "same-user", password: "same-pass" });
|
|
const staleQueueTail = internals.queues.get("same-user");
|
|
queuedController.abort("cancel-before-start");
|
|
await expect(queuedRequest).rejects.toThrow(/queue.?timeout/i);
|
|
|
|
fallback.invalidateSession();
|
|
const freshRequest = fallback.unrestrict("https://mega.debrid/fresh", undefined, { login: "same-user", password: "same-pass" });
|
|
|
|
try {
|
|
await vi.waitFor(() => expect(login).toHaveBeenCalledTimes(2), { timeout: 500, interval: 5 });
|
|
await expect(freshRequest).resolves.toMatchObject({
|
|
directUrl: "https://mega.direct/fresh",
|
|
fileName: "fresh.bin"
|
|
});
|
|
} finally {
|
|
releaseOldLogin();
|
|
await Promise.allSettled([oldSettled, freshRequest, staleQueueTail]);
|
|
}
|
|
|
|
expect(generatedLinks).toEqual(["https://mega.debrid/fresh"]);
|
|
});
|
|
|
|
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('<form id="debridForm"></form>', { status: 200 });
|
|
}
|
|
|
|
if (urlStr.includes("form=debrid")) {
|
|
return new Response(`
|
|
<div class="acp-box">
|
|
<h3>Link: https://mega.debrid/link1</h3>
|
|
<a href="javascript:processDebrid(1,'secretcode123',0)">Download</a>
|
|
</div>
|
|
`, { 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('<form id="debridForm"></form>', { status: 200 });
|
|
}
|
|
if (urlStr.includes("form=debrid")) {
|
|
return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l1</h3><a href="javascript:processDebrid(1,'code1',0)">d</a></div>`, { 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('<form id="debridForm"></form>', { status: 200 });
|
|
}
|
|
if (urlStr.includes("form=debrid")) {
|
|
return new Response('<div class="error">Erreur : Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal.</div>', { 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('<form id="debridForm"></form>', { status: 200 });
|
|
}
|
|
if (urlStr.includes("form=debrid")) {
|
|
return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l1</h3><a href="javascript:processDebrid(1,'code1',0)">d</a></div>`, { 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('<html><body>Nothing here</body></html>', { 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('<form id="debridForm"></form>', { status: 200 });
|
|
}
|
|
if (urlStr.includes("form=debrid")) {
|
|
return new Response(`<div>No links here</div>`, { 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('<form id="debridForm"></form>', { status: 200 });
|
|
if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { 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<void>((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('<form id="debridForm"></form>', { status: 200 });
|
|
if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { 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<Response> => {
|
|
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('<form id="debridForm"></form>', { status: 200 }));
|
|
}
|
|
|
|
if (urlStr.includes("form=debrid")) {
|
|
return Promise.resolve(new Response(`
|
|
<div class="acp-box">
|
|
<h3>Link: https://mega.debrid/link2</h3>
|
|
<a href="javascript:processDebrid(1,'secretcode456',0)">Download</a>
|
|
</div>
|
|
`, { status: 200 }));
|
|
}
|
|
|
|
if (urlStr.includes("ajax=debrid")) {
|
|
return new Promise<Response>((_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<void>((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('<form id="debridForm"></form>', { status: 200 });
|
|
if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { 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);
|
|
});
|
|
});
|