Fix ~70 issues across the entire codebase including security fixes, error handling improvements, test stabilization, and code quality. - Fix TLS race condition with reference-counted acquire/release - Bind debug server to 127.0.0.1 instead of 0.0.0.0 - Add overall timeout to MegaWebFallback - Stream update installer to disk instead of RAM buffering - Add path traversal protection in JVM extractor - Cache DdownloadClient with credential-based invalidation - Add .catch() to all fire-and-forget IPC calls - Wrap app startup, clipboard, session-log in try/catch - Add timeouts to container.ts fetch calls - Fix variable shadowing, tsconfig path, line endings - Stabilize tests with proper cleanup and timing tolerance - Fix installer privileges, scripts, and afterPack null checks - Delete obsolete _upload_release.mjs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
6.9 KiB
TypeScript
179 lines
6.9 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("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")) {
|
|
// The POST to generate the code
|
|
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")) {
|
|
// Polling endpoint
|
|
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");
|
|
// Calls: 1. Login POST, 2. Verify GET, 3. Generate POST, 4. Polling POST
|
|
expect(fetchCallCount).toBe(4);
|
|
});
|
|
|
|
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(); // No cookie
|
|
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")) {
|
|
// Missing form!
|
|
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")) {
|
|
// The generate POST returns HTML without any codes
|
|
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");
|
|
|
|
// Generation fails -> resets cookie -> tries again -> fails again -> returns null
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
});
|