Files
Multi-Debrid-Downloader/tests/online-backup.test.ts

150 lines
7.2 KiB
TypeScript

import http from "node:http";
import { once } from "node:events";
import { afterEach, describe, expect, it } from "vitest";
import type { AppSettings } from "../src/shared/types";
import {
createOnlineBackup,
deleteOnlineBackup,
downloadOnlineBackup,
parseOnlineBackupKey,
restoreOnlineBackup,
uploadOnlineBackup
} from "../src/main/online-backup";
const servers: http.Server[] = [];
afterEach(async () => {
await Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
});
function settings(): AppSettings {
return {
token: "rd-secret-token",
deepbridApiKey: "fixture-deepbrid-online-key-6jK8",
megaLogin: "backup-user",
megaPassword: "backup-password",
outputDir: "D:\\Downloads",
backupIncludeDownloads: true
} as AppSettings;
}
describe("online backup key", () => {
it("creates a compact key and restores every immutable settings snapshot independently", () => {
const first = createOnlineBackup(settings(), "2.0.0", "2026-08-07T00:00:00.000Z");
const second = createOnlineBackup({ ...settings(), outputDir: "E:\\Neu" }, "2.0.0", "2026-08-08T00:00:00.000Z");
expect(first.key).toMatch(/^MDD2-[A-Za-z0-9_-]{70}$/);
expect(first.key).toHaveLength(75);
expect(second.key).not.toBe(first.key);
expect(restoreOnlineBackup(first.key, first.record.blob).settings).toEqual(settings());
expect(restoreOnlineBackup(second.key, second.record.blob).settings.outputDir).toBe("E:\\Neu");
});
it("never places credentials or the decryption secret in the server record", () => {
const proxyList = "proxy-user:proxy-secret@192.0.2.10:8080\n";
const created = createOnlineBackup(settings(), "2.0.0", "2026-08-07T00:00:00.000Z", proxyList);
const serialized = JSON.stringify(created.record);
const parsed = parseOnlineBackupKey(created.key);
expect(serialized).not.toContain("rd-secret-token");
expect(serialized).not.toContain("fixture-deepbrid-online-key-6jK8");
expect(serialized).not.toContain("backup-password");
expect(serialized).not.toContain("proxy-user");
expect(serialized).not.toContain("proxy-secret");
expect(serialized).not.toContain(parsed.masterKey.toString("base64url"));
expect(Object.keys(created.record).sort()).toEqual(["blob", "deleteVerifier", "id"]);
expect(restoreOnlineBackup(created.key, created.record.blob).proxyList).toEqual({ version: 1, content: proxyList });
});
it("rejects corrupted keys and encrypted payloads before returning settings", () => {
const created = createOnlineBackup(settings(), "2.0.0", "2026-08-07T00:00:00.000Z");
const keyTail = created.key.endsWith("A") ? "B" : "A";
const corruptedKey = `${created.key.slice(0, -1)}${keyTail}`;
const blobTail = created.record.blob.endsWith("A") ? "B" : "A";
const corruptedBlob = `${created.record.blob.slice(0, -1)}${blobTail}`;
expect(() => parseOnlineBackupKey(corruptedKey)).toThrow(/Schlüssel/i);
expect(() => restoreOnlineBackup(created.key, corruptedBlob)).toThrow(/entschlüsselt|beschädigt/i);
});
it("rejects plaintext that cannot be restored before returning a key", () => {
const oversized = { ...settings(), archivePasswordList: "x".repeat(600_000) };
expect(() => createOnlineBackup(oversized, "2.0.0")).toThrow(/zu groß/i);
expect(() => createOnlineBackup(settings(), "2.0.0", undefined, "invalid proxy line\n")).toThrow(/keine gültigen/i);
});
});
describe("online backup transport", () => {
it("never reflects service response bodies into client errors", async () => {
const server = http.createServer((_request, response) => {
response.writeHead(500, { "content-type": "application/json" });
response.end(JSON.stringify({ error: "internal", leaked: "server-secret-value" }));
});
servers.push(server);
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") throw new Error("Testserver nicht verfügbar");
const created = createOnlineBackup(settings(), "2.0.0");
await expect(uploadOnlineBackup(created.record, `http://127.0.0.1:${address.port}`)).rejects.not.toThrow(/server-secret-value/);
});
it("uploads and downloads through the real HTTP contract without sending the master key", async () => {
let stored: Record<string, string> | null = null;
let deleteRequest: Record<string, string> | null = null;
const requestedUrls: string[] = [];
const server = http.createServer(async (request, response) => {
requestedUrls.push(String(request.url || ""));
if (request.method === "POST" && request.url === "/v1/backups") {
const chunks: Buffer[] = [];
for await (const chunk of request) chunks.push(Buffer.from(chunk));
stored = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, string>;
response.writeHead(201, { "content-type": "application/json" });
response.end(JSON.stringify({ created: true }));
return;
}
if (request.method === "POST" && stored && request.url === "/v1/backups/restore") {
const chunks: Buffer[] = [];
for await (const chunk of request) chunks.push(Buffer.from(chunk));
const restoreRequest = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, string>;
if (restoreRequest.id !== stored.id) throw new Error("Falsche Restore-ID");
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ blob: stored.blob }));
return;
}
if (request.method === "POST" && stored && request.url === "/v1/backups/delete") {
const chunks: Buffer[] = [];
for await (const chunk of request) chunks.push(Buffer.from(chunk));
deleteRequest = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, string>;
response.writeHead(204);
response.end();
return;
}
response.writeHead(404, { "content-type": "application/json" });
response.end(JSON.stringify({ error: "Nicht gefunden" }));
});
servers.push(server);
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") throw new Error("Testserver nicht verfügbar");
const baseUrl = `http://127.0.0.1:${address.port}`;
const created = createOnlineBackup(settings(), "2.0.0", "2026-08-07T00:00:00.000Z");
await uploadOnlineBackup(created.record, baseUrl);
const restored = await downloadOnlineBackup(created.key, baseUrl);
await deleteOnlineBackup(created.key, baseUrl);
expect(restored.settings).toEqual(settings());
expect(stored).not.toBeNull();
const uploadedRecord = stored as unknown as { id: string; blob: string; deleteVerifier: string };
const capturedDeleteRequest = deleteRequest as unknown as Record<string, string>;
expect(JSON.stringify(uploadedRecord)).not.toContain(parseOnlineBackupKey(created.key).masterKey.toString("base64url"));
expect(capturedDeleteRequest.deleteSecret).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(requestedUrls).toEqual(["/v1/backups", "/v1/backups/restore", "/v1/backups/delete"]);
expect(requestedUrls.join(" ")).not.toContain(uploadedRecord.id);
});
});