real-debrid-downloader/tests/update-restart-resume.test.ts
Sucukdeluxe 3ed3877ac9 chore: remove all source code comments and internal artifacts
Strip every comment from the source (parsed with the TypeScript compiler so
strings, template literals, regex literals and JSX are never touched), and drop
internal/working artifacts that do not belong in the public repository
(design mockups, internal analysis docs, a stray backup file and an old log).
No functional change: build is green, the full test suite passes.
2026-06-06 04:53:54 +02:00

173 lines
5.8 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import http from "node:http";
import { once } from "node:events";
import { afterEach, describe, expect, it } from "vitest";
import { DownloadManager } from "../src/main/download-manager";
import { defaultSettings } from "../src/main/constants";
import { createStoragePaths, emptySession, loadSession } from "../src/main/storage";
import { shutdownItemLogs } from "../src/main/item-log";
import { shutdownPackageLogs } from "../src/main/package-log";
const tempDirs: string[] = [];
const originalFetch = globalThis.fetch;
afterEach(async () => {
globalThis.fetch = originalFetch;
shutdownItemLogs();
shutdownPackageLogs();
for (const dir of tempDirs.splice(0)) {
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
fs.rmSync(dir, { recursive: true, force: true });
break;
} catch {
await new Promise((resolve) => setTimeout(resolve, 80));
}
}
}
});
async function waitFor(predicate: () => boolean, timeoutMs = 20000): Promise<void> {
const started = Date.now();
while (!predicate()) {
if (Date.now() - started > timeoutMs) {
throw new Error("waitFor timeout");
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
async function startTricklingServer(): Promise<{ directUrl: string; stop: () => Promise<void> }> {
const openTimers = new Set<NodeJS.Timeout>();
const openResponses = new Set<http.ServerResponse>();
const server = http.createServer((req, res) => {
if ((req.url || "") !== "/direct") {
res.statusCode = 404;
res.end("not-found");
return;
}
res.statusCode = 200;
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Length", String(64 * 1024 * 1024));
openResponses.add(res);
res.write(Buffer.alloc(64 * 1024, 7));
const timer = setInterval(() => {
try {
res.write(Buffer.alloc(16 * 1024, 9));
} catch {
}
}, 100);
openTimers.add(timer);
res.on("close", () => {
clearInterval(timer);
openTimers.delete(timer);
openResponses.delete(res);
});
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("server address unavailable");
}
const directUrl = `http://127.0.0.1:${address.port}/direct`;
const stop = async (): Promise<void> => {
for (const timer of openTimers) {
clearInterval(timer);
}
openTimers.clear();
for (const res of openResponses) {
try {
res.destroy();
} catch {
}
}
openResponses.clear();
server.close();
await once(server, "close");
};
return { directUrl, stop };
}
function mockUnrestrict(directUrl: string): void {
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("/unrestrict/link")) {
return new Response(
JSON.stringify({ download: directUrl, filename: "episode.mkv", filesize: 64 * 1024 * 1024 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return originalFetch(input, init);
};
}
async function driveActiveDownload(root: string): Promise<{ manager: DownloadManager; paths: ReturnType<typeof createStoragePaths>; serverStop: () => Promise<void> }> {
const { directUrl, stop: serverStop } = await startTricklingServer();
mockUnrestrict(directUrl);
const paths = createStoragePaths(path.join(root, "state"));
const manager = new DownloadManager(
{
...defaultSettings(),
token: "rd-token",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
autoReconnect: false,
retryLimit: 0
},
emptySession(),
paths
);
manager.addPackages([{ name: "park", links: ["https://dummy/park"] }]);
await manager.start();
await waitFor(() => {
const item = Object.values(manager.getSnapshot().session.items)[0];
return item?.status === "downloading" && (manager as unknown as { activeTasks: Map<string, unknown> }).activeTasks.size > 0;
});
return { manager, paths, serverStop };
}
describe("update restart resume", () => {
it("characterization: a plain stop() leaves an in-flight item cancelled across a restart", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-update-resume-"));
tempDirs.push(root);
const { manager, paths, serverStop } = await driveActiveDownload(root);
try {
manager.stop();
manager.persistNowSync();
await waitFor(() => (manager as unknown as { activeTasks: Map<string, unknown> }).activeTasks.size === 0);
manager.prepareForShutdown();
const reloaded = loadSession(paths);
const item = Object.values(reloaded.items)[0];
expect(item).toBeTruthy();
expect(item.status).toBe("cancelled");
} finally {
await serverStop();
}
});
it("parks an in-flight item as queued for an update restart so it auto-resumes", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-update-resume-"));
tempDirs.push(root);
const { manager, paths, serverStop } = await driveActiveDownload(root);
try {
manager.stop({ parkForRestart: true });
manager.persistNowSync();
await waitFor(() => (manager as unknown as { activeTasks: Map<string, unknown> }).activeTasks.size === 0);
manager.prepareForShutdown();
const reloaded = loadSession(paths);
const item = Object.values(reloaded.items)[0];
expect(item).toBeTruthy();
expect(Object.keys(reloaded.packages).length).toBe(1);
expect(item.status).toBe("queued");
} finally {
await serverStop();
}
});
});