Move secret-bearing account and settings state behind main-process boundaries for Task 1B. Renderer snapshots now expose RendererSettings plus safe account metadata instead of full AppSettings, with provider tokens, passwords, API keys, archive passwords, and notification URLs excluded from renderer-bound state. Add write-only account create, replace, update-secret, and delete IPC commands, validate renderer settings updates against the safe shape, and keep account command results limited to safe settings, safe accounts, and stable account IDs. Preserve existing account behavior while removing renderer secret access: blank replace secrets retain stored main-process secrets, Mega-Debrid API/Web pools stay mode-specific, Debrid-Link key metadata migrates by stable key ID, and delete/enable operations target stable account or provider identities. Remove obsolete renderer-side account status helpers from the old settings snapshot flow. Add focused coverage for all supported renderer account kinds, secret-free UiSnapshot serialization, preload account command forwarding, malformed payload error sanitization, Mega-Debrid preferApi preservation, Debrid-Link key metadata migration, account edit safety, settings UI, debug server settings payloads, link export, and visual fixtures.
579 lines
24 KiB
TypeScript
579 lines
24 KiB
TypeScript
import fs from "node:fs";
|
|
import http from "node:http";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { once } from "node:events";
|
|
import AdmZip from "adm-zip";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../src/main/windows-host-diagnostics", () => ({
|
|
getWindowsHostDiagnostics: () => ({
|
|
collectedAt: "2026-03-09T00:00:03.000Z",
|
|
supported: true,
|
|
platform: "win32",
|
|
crashControl: {
|
|
crashDumpEnabled: 3,
|
|
minidumpDir: "C:\\Windows\\Minidumps",
|
|
dumpFile: "C:\\Windows\\MEMORY.DMP",
|
|
overwrite: 1,
|
|
logEvent: 1,
|
|
autoReboot: 1
|
|
},
|
|
recentKernelPower: [
|
|
{
|
|
timeCreated: "2026-03-09T00:00:04.000Z",
|
|
id: 41,
|
|
providerName: "Microsoft-Windows-Kernel-Power",
|
|
levelDisplayName: "Critical",
|
|
message: "unexpected restart",
|
|
bugcheckCode: "0",
|
|
bugcheckCodeHex: "",
|
|
reportId: ""
|
|
}
|
|
],
|
|
recentWerKernel: [],
|
|
recentKernelDump: [],
|
|
recentAppCrashes: [],
|
|
recentMinidumps: [],
|
|
assessmentHints: ["watchdog hint"],
|
|
errors: []
|
|
})
|
|
}));
|
|
|
|
import { defaultSettings } from "../src/main/constants";
|
|
import { configureCredentialProtector } from "../src/main/credential-protection";
|
|
import { createRendererState } from "../src/main/renderer-state";
|
|
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "../src/main/audit-log";
|
|
import { startDebugServer, stopDebugServer } from "../src/main/debug-server";
|
|
import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
|
import { configureLogger, getLogFilePath, logger } from "../src/main/logger";
|
|
import { ensurePackageLog, initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
|
import { getRenameLogPath, initRenameLog, logRenameEvent, shutdownRenameLog } from "../src/main/rename-log";
|
|
import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log";
|
|
import { createStoragePaths, saveHistory, saveSettings } from "../src/main/storage";
|
|
import { getTraceConfigPath, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "../src/main/trace-log";
|
|
import { getDebridLinkApiKeyIds } from "../src/shared/debrid-link-keys";
|
|
import type { DownloadManager } from "../src/main/download-manager";
|
|
import type { UiSnapshot } from "../src/shared/types";
|
|
|
|
const tempDirs: string[] = [];
|
|
const legacyManifestFile = ["debug_", "a", "i", "_manifest.json"].join("");
|
|
const legacyManifestField = ["a", "i", "Manifest"].join("");
|
|
const forbiddenSupportMarkers = [
|
|
["A", "I"].join(""),
|
|
["assist", "ant"].join(""),
|
|
["assist", "ants"].join(""),
|
|
["ag", "ent"].join(""),
|
|
["ag", "ents"].join(""),
|
|
["Clau", "de"].join(""),
|
|
["Anthro", "pic"].join(""),
|
|
["Co", "dex"].join(""),
|
|
["Open", "AI"].join(""),
|
|
["M", "C", "P"].join(""),
|
|
["K", "I"].join("")
|
|
];
|
|
|
|
beforeEach(() => {
|
|
configureCredentialProtector({
|
|
isEncryptionAvailable: () => true,
|
|
encryptString: (value) => Buffer.from(value, "utf8").reverse(),
|
|
decryptString: (value) => Buffer.from(value).reverse().toString("utf8")
|
|
});
|
|
});
|
|
|
|
async function getFreePort(): Promise<number> {
|
|
const probe = http.createServer();
|
|
probe.listen(0, "127.0.0.1");
|
|
await once(probe, "listening");
|
|
const address = probe.address();
|
|
if (!address || typeof address === "string") {
|
|
throw new Error("port probe failed");
|
|
}
|
|
probe.close();
|
|
await once(probe, "close");
|
|
return address.port;
|
|
}
|
|
|
|
async function waitForReady(url: string): Promise<void> {
|
|
const deadline = Date.now() + 5000;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const response = await fetch(url);
|
|
if (response.ok) {
|
|
return;
|
|
}
|
|
} catch {
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
throw new Error(`debug server not ready: ${url}`);
|
|
}
|
|
|
|
function buildSnapshot(baseDir: string): UiSnapshot {
|
|
const settings = {
|
|
...defaultSettings(),
|
|
outputDir: path.join(baseDir, "downloads"),
|
|
extractDir: path.join(baseDir, "extract")
|
|
};
|
|
|
|
const renderer = createRendererState(settings);
|
|
return {
|
|
...renderer,
|
|
session: {
|
|
version: 1,
|
|
packageOrder: ["pkg-1"],
|
|
packages: {
|
|
"pkg-1": {
|
|
id: "pkg-1",
|
|
name: "server-package",
|
|
outputDir: path.join(baseDir, "downloads", "server-package"),
|
|
extractDir: path.join(baseDir, "extract", "server-package"),
|
|
status: "downloading",
|
|
itemIds: ["item-1", "item-2"],
|
|
cancelled: false,
|
|
enabled: true,
|
|
priority: "normal",
|
|
postProcessLabel: "",
|
|
createdAt: Date.now() - 30_000,
|
|
updatedAt: Date.now()
|
|
}
|
|
},
|
|
items: {
|
|
"item-1": {
|
|
id: "item-1",
|
|
packageId: "pkg-1",
|
|
url: "https://hoster.example/file-1",
|
|
provider: "realdebrid",
|
|
providerLabel: "Real-Debrid",
|
|
status: "downloading",
|
|
retries: 1,
|
|
speedBps: 8 * 1024 * 1024,
|
|
downloadedBytes: 64 * 1024 * 1024,
|
|
totalBytes: 256 * 1024 * 1024,
|
|
progressPercent: 25,
|
|
fileName: "episode.part1.rar",
|
|
targetPath: path.join(baseDir, "downloads", "server-package", "episode.part1.rar"),
|
|
resumable: true,
|
|
attempts: 1,
|
|
lastError: "",
|
|
fullStatus: "Download läuft (Real-Debrid)",
|
|
createdAt: Date.now() - 30_000,
|
|
updatedAt: Date.now()
|
|
},
|
|
"item-2": {
|
|
id: "item-2",
|
|
packageId: "pkg-1",
|
|
url: "https://hoster.example/file-2",
|
|
provider: "realdebrid",
|
|
providerLabel: "Real-Debrid",
|
|
status: "failed",
|
|
retries: 3,
|
|
speedBps: 0,
|
|
downloadedBytes: 0,
|
|
totalBytes: null,
|
|
progressPercent: 0,
|
|
fileName: "episode.part2.rar",
|
|
targetPath: path.join(baseDir, "downloads", "server-package", "episode.part2.rar"),
|
|
resumable: false,
|
|
attempts: 3,
|
|
lastError: "hoster unavailable",
|
|
fullStatus: "Fehler: hoster unavailable",
|
|
createdAt: Date.now() - 30_000,
|
|
updatedAt: Date.now()
|
|
}
|
|
},
|
|
runStartedAt: Date.now() - 30_000,
|
|
totalDownloadedBytes: 64 * 1024 * 1024,
|
|
summaryText: "",
|
|
reconnectUntil: 0,
|
|
reconnectReason: "",
|
|
paused: false,
|
|
running: true,
|
|
updatedAt: Date.now()
|
|
},
|
|
summary: null,
|
|
stats: {
|
|
totalDownloaded: 64 * 1024 * 1024,
|
|
totalDownloadedAllTime: 128 * 1024 * 1024,
|
|
totalFilesSession: 0,
|
|
totalFilesAllTime: 0,
|
|
totalPackages: 1,
|
|
sessionStartedAt: Date.now() - 30_000,
|
|
appSessionStartedAt: Date.now() - 60_000,
|
|
sessionRuntimeMs: 60_000,
|
|
totalRuntimeMs: 3 * 60_000,
|
|
runtimeMeasuredAt: Date.now()
|
|
},
|
|
speedText: "8.0 MB/s",
|
|
etaText: "ETA: 00:25",
|
|
canStart: false,
|
|
canStop: true,
|
|
canPause: true,
|
|
clipboardActive: false,
|
|
reconnectSeconds: 0,
|
|
packageSpeedBps: {
|
|
"pkg-1": 8 * 1024 * 1024
|
|
}
|
|
};
|
|
}
|
|
|
|
async function createFixture() {
|
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-debug-"));
|
|
tempDirs.push(baseDir);
|
|
const token = "debug-secret";
|
|
const port = await getFreePort();
|
|
const snapshot = buildSnapshot(baseDir);
|
|
const storagePaths = createStoragePaths(baseDir);
|
|
|
|
fs.writeFileSync(path.join(baseDir, "debug_token.txt"), token, "utf8");
|
|
fs.writeFileSync(path.join(baseDir, "debug_port.txt"), String(port), "utf8");
|
|
fs.writeFileSync(path.join(baseDir, "debug_host.txt"), "0.0.0.0", "utf8");
|
|
const debridLinkApiKeys = "key-a\nkey-b";
|
|
const debridLinkKeyIds = getDebridLinkApiKeyIds(debridLinkApiKeys);
|
|
|
|
saveSettings(storagePaths, {
|
|
...defaultSettings(),
|
|
outputDir: snapshot.settings.outputDir,
|
|
extractDir: snapshot.settings.extractDir,
|
|
token: "rd-secret-token",
|
|
realDebridUseWebLogin: true,
|
|
debridLinkApiKeys,
|
|
debridLinkDisabledKeyIds: debridLinkKeyIds[1] ? [debridLinkKeyIds[1]] : [],
|
|
totalDownloadedAllTime: 128 * 1024 * 1024,
|
|
totalCompletedFilesAllTime: 12,
|
|
totalRuntimeAllTimeMs: 5 * 60_000
|
|
});
|
|
saveHistory(storagePaths, [
|
|
{
|
|
id: "hist-1",
|
|
name: "server-package",
|
|
totalBytes: 123,
|
|
downloadedBytes: 123,
|
|
fileCount: 2,
|
|
provider: "realdebrid",
|
|
completedAt: Date.now() - 5_000,
|
|
durationSeconds: 42,
|
|
status: "completed",
|
|
outputDir: path.join(baseDir, "downloads", "server-package"),
|
|
urls: ["https://hoster.example/file-1"]
|
|
}
|
|
]);
|
|
|
|
configureLogger(baseDir);
|
|
fs.writeFileSync(getLogFilePath(), "2026-03-09T00:00:00.000Z [INFO] MAIN-LINE\n", "utf8");
|
|
initAuditLog(baseDir);
|
|
const auditLogPath = getAuditLogPath();
|
|
if (!auditLogPath) {
|
|
throw new Error("audit log path missing");
|
|
}
|
|
logAuditEvent("INFO", "AUDIT-LINE", { scope: "settings" });
|
|
|
|
initRenameLog(baseDir);
|
|
logRenameEvent("INFO", "RENAME-LINE", { stage: "auto-rename", sourcePath: "C:\\extract\\old.mkv" });
|
|
|
|
initTraceLog(baseDir);
|
|
setTraceEnabled(true, "test-fixture");
|
|
logTraceEvent("INFO", "support", "TRACE-EVENT", { scope: "fixture" });
|
|
|
|
initSessionLog(baseDir);
|
|
const sessionLogPath = getSessionLogPath();
|
|
if (!sessionLogPath) {
|
|
throw new Error("session log path missing");
|
|
}
|
|
fs.appendFileSync(sessionLogPath, "2026-03-09T00:00:01.000Z [INFO] SESSION-LINE\n", "utf8");
|
|
logger.info("TRACE-MAIN-LINE");
|
|
|
|
initPackageLogs(baseDir);
|
|
initItemLogs(baseDir);
|
|
const packageLogPath = ensurePackageLog({
|
|
packageId: "pkg-1",
|
|
name: "server-package",
|
|
outputDir: snapshot.session.packages["pkg-1"]!.outputDir,
|
|
extractDir: snapshot.session.packages["pkg-1"]!.extractDir
|
|
});
|
|
if (!packageLogPath) {
|
|
throw new Error("package log path missing");
|
|
}
|
|
fs.appendFileSync(packageLogPath, "2026-03-09T00:00:02.000Z [INFO] PACKAGE-LINE\n", "utf8");
|
|
const itemLogPath = ensureItemLog({
|
|
itemId: "item-2",
|
|
packageId: "pkg-1",
|
|
packageName: "server-package",
|
|
fileName: "episode.part2.rar",
|
|
targetPath: snapshot.session.items["item-2"]!.targetPath
|
|
});
|
|
if (!itemLogPath) {
|
|
throw new Error("item log path missing");
|
|
}
|
|
fs.appendFileSync(itemLogPath, "2026-03-09T00:00:03.000Z [ERROR] ITEM-LINE\n", "utf8");
|
|
|
|
const manager = {
|
|
getSnapshot: () => snapshot,
|
|
getPackageLogPath: (packageId: string) => packageId === "pkg-1" ? packageLogPath : null,
|
|
getItemLogPath: (itemId: string) => itemId === "item-2" ? itemLogPath : null
|
|
} as unknown as DownloadManager;
|
|
|
|
startDebugServer(manager, baseDir);
|
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
await waitForReady(`${baseUrl}/health?token=${token}`);
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
|
|
return {
|
|
baseUrl,
|
|
token,
|
|
baseDir
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
stopDebugServer();
|
|
shutdownSessionLog();
|
|
shutdownPackageLogs();
|
|
shutdownItemLogs();
|
|
shutdownRenameLog();
|
|
shutdownTraceLog();
|
|
shutdownAuditLog();
|
|
while (tempDirs.length > 0) {
|
|
const dir = tempDirs.pop();
|
|
if (!dir) {
|
|
continue;
|
|
}
|
|
try {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
} catch {
|
|
}
|
|
}
|
|
});
|
|
|
|
describe("debug-server", () => {
|
|
it("serves diagnostics with main, session, and package log tails", async () => {
|
|
const fixture = await createFixture();
|
|
const response = await fetch(`${fixture.baseUrl}/diagnostics?token=${fixture.token}&package=server-package&lines=20`);
|
|
expect(response.ok).toBe(true);
|
|
const payload = await response.json() as Record<string, any>;
|
|
|
|
expect(payload.meta?.appVersion).toBeTruthy();
|
|
expect(payload.meta?.debugServer?.host).toBe("0.0.0.0");
|
|
expect(payload.status?.running).toBe(true);
|
|
expect(payload.host?.platform).toBe("win32");
|
|
expect(payload.host?.recentKernelPower?.[0]?.id).toBe(41);
|
|
expect(payload.selectedPackage?.name).toBe("server-package");
|
|
expect((payload.logs?.main?.lines || []).join("\n")).toContain("MAIN-LINE");
|
|
expect((payload.logs?.audit?.lines || []).join("\n")).toContain("AUDIT-LINE");
|
|
expect((payload.logs?.rename?.lines || []).join("\n")).toContain("RENAME-LINE");
|
|
expect((payload.logs?.trace?.lines || []).join("\n")).toContain("TRACE-EVENT");
|
|
expect((payload.logs?.session?.lines || []).join("\n")).toContain("SESSION-LINE");
|
|
expect((payload.logs?.package?.lines || []).join("\n")).toContain("PACKAGE-LINE");
|
|
expect(payload.accounts?.realDebrid?.configured).toBe(true);
|
|
expect(payload.history?.total).toBe(1);
|
|
});
|
|
|
|
it("writes a machine-readable support manifest into the runtime folder", async () => {
|
|
const fixture = await createFixture();
|
|
const manifestPath = path.join(fixture.baseDir, "debug_support_manifest.json");
|
|
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
expect(fs.existsSync(path.join(fixture.baseDir, legacyManifestFile))).toBe(false);
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record<string, any>;
|
|
expect(manifest.purpose).toBe("Machine-readable manifest for support tooling and remote troubleshooting.");
|
|
expect(JSON.stringify(manifest)).not.toMatch(new RegExp(`\\b(?:${forbiddenSupportMarkers.join("|")})\\b`, "i"));
|
|
expect(JSON.stringify(manifest)).not.toContain(fixture.token);
|
|
expect(manifest.debugServer?.port).toBeGreaterThan(0);
|
|
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
|
expect(manifest.remoteAccessRequirements).toContain("A reachable server IP or DNS name.");
|
|
expect(manifest.setupCheckEndpoint).toBe("/debug/setup");
|
|
expect(manifest.selfCheckEndpoint).toBe("/self-check");
|
|
expect(manifest.runtimeFiles?.tokenFile).toContain("debug_token.txt");
|
|
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/diagnostics")).toBe(true);
|
|
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/logs/main")).toBe(true);
|
|
|
|
const metaResponse = await fetch(`${fixture.baseUrl}/meta?token=${fixture.token}`);
|
|
expect(metaResponse.ok).toBe(true);
|
|
const metaPayload = await metaResponse.json() as Record<string, any>;
|
|
expect(metaPayload.supportFiles?.supportManifest).toBe(manifestPath);
|
|
expect(metaPayload.supportFiles?.[legacyManifestField]).toBeUndefined();
|
|
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
|
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
|
|
expect(metaPayload.logPaths?.rename).toBe(getRenameLogPath());
|
|
expect(metaPayload.supportChecks?.setup).toBe("/debug/setup");
|
|
expect(metaPayload.supportChecks?.selfCheck).toBe("/self-check");
|
|
});
|
|
|
|
it("serves a debug setup check with trace expiry details", async () => {
|
|
const fixture = await createFixture();
|
|
const response = await fetch(`${fixture.baseUrl}/debug/setup?token=${fixture.token}`);
|
|
expect(response.ok).toBe(true);
|
|
const payload = await response.json() as Record<string, any>;
|
|
|
|
expect(payload.enabled).toBe(true);
|
|
expect(Array.isArray(payload.warnings)).toBe(true);
|
|
expect(payload.status).toBe(payload.warnings.length > 0 ? "warn" : "ok");
|
|
expect(payload.runtimeBaseDir).toBe(fixture.baseDir);
|
|
expect(payload.host).toBe("0.0.0.0");
|
|
expect(payload.localOnly).toBe(false);
|
|
expect(payload.tokenConfigured).toBe(true);
|
|
expect(payload.supportManifestPresent).toBe(true);
|
|
expect(payload[`${legacyManifestField}Present`]).toBeUndefined();
|
|
expect(payload.supportManifestPath).toBe(path.join(fixture.baseDir, "debug_support_manifest.json"));
|
|
expect(payload.traceEnabled).toBe(true);
|
|
expect(payload.traceAutoDisableAt).toBeTruthy();
|
|
expect(payload.diskSpace?.runtime?.freeBytes).toBeGreaterThan(0);
|
|
expect(payload.diskSpace?.output?.freeBytes).toBeGreaterThan(0);
|
|
expect(payload.diskSpace?.extract?.freeBytes).toBeGreaterThan(0);
|
|
expect(payload.logSummary?.totalBytes).toBeGreaterThan(0);
|
|
expect(payload.logSummary?.rename?.bytes).toBeGreaterThan(0);
|
|
expect(payload.logSummary?.packageLogs?.fileCount).toBe(1);
|
|
expect(payload.logSummary?.itemLogs?.fileCount).toBe(1);
|
|
expect(payload.supportBundle?.estimatedBytes).toBeGreaterThan(0);
|
|
expect(payload.remoteUrlTemplates?.health).toContain("<SERVER_IP_OR_DNS>");
|
|
expect(Array.isArray(payload.notes)).toBe(true);
|
|
});
|
|
|
|
it("serves the self-check alias", async () => {
|
|
const fixture = await createFixture();
|
|
const response = await fetch(`${fixture.baseUrl}/self-check?token=${fixture.token}`);
|
|
expect(response.ok).toBe(true);
|
|
const payload = await response.json() as Record<string, any>;
|
|
expect(Array.isArray(payload.warnings)).toBe(true);
|
|
expect(payload.status).toBe(payload.warnings.length > 0 ? "warn" : "ok");
|
|
expect(payload.supportBundle?.estimatedEntries).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("writes the client IP into the debug trace log", async () => {
|
|
const fixture = await createFixture();
|
|
const response = await fetch(`${fixture.baseUrl}/health?token=${fixture.token}`, {
|
|
headers: {
|
|
"X-Forwarded-For": "203.0.113.46"
|
|
}
|
|
});
|
|
expect(response.ok).toBe(true);
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
const traceLogPath = getTraceLogPath();
|
|
expect(traceLogPath).toBeTruthy();
|
|
const traceText = fs.readFileSync(traceLogPath!, "utf8");
|
|
expect(traceText).toContain("clientIp=203.0.113.46");
|
|
});
|
|
|
|
it("serves package details and package log by package query", async () => {
|
|
const fixture = await createFixture();
|
|
|
|
const packagesResponse = await fetch(`${fixture.baseUrl}/packages?token=${fixture.token}&package=server&includeItems=1`);
|
|
expect(packagesResponse.ok).toBe(true);
|
|
const packagesPayload = await packagesResponse.json() as Record<string, any>;
|
|
expect(packagesPayload.count).toBe(1);
|
|
expect(packagesPayload.packages?.[0]?.items?.length).toBe(2);
|
|
|
|
const logResponse = await fetch(`${fixture.baseUrl}/logs/package?token=${fixture.token}&package=server-package&lines=20`);
|
|
expect(logResponse.ok).toBe(true);
|
|
const logPayload = await logResponse.json() as Record<string, any>;
|
|
expect(logPayload.package?.name).toBe("server-package");
|
|
expect((logPayload.lines || []).join("\n")).toContain("PACKAGE-LINE");
|
|
});
|
|
|
|
it("serves item log by item query", async () => {
|
|
const fixture = await createFixture();
|
|
|
|
const response = await fetch(`${fixture.baseUrl}/logs/item?token=${fixture.token}&item=episode.part2.rar&lines=20`);
|
|
expect(response.ok).toBe(true);
|
|
const payload = await response.json() as Record<string, any>;
|
|
expect(payload.item?.id).toBe("item-2");
|
|
expect(payload.item?.fileName).toBe("episode.part2.rar");
|
|
expect((payload.lines || []).join("\n")).toContain("ITEM-LINE");
|
|
});
|
|
|
|
it("serves host diagnostics separately", async () => {
|
|
const fixture = await createFixture();
|
|
const response = await fetch(`${fixture.baseUrl}/host/diagnostics?token=${fixture.token}`);
|
|
expect(response.ok).toBe(true);
|
|
const payload = await response.json() as Record<string, any>;
|
|
expect(payload.platform).toBe("win32");
|
|
expect(payload.crashControl?.crashDumpEnabled).toBe(3);
|
|
expect(payload.assessmentHints?.[0]).toContain("watchdog");
|
|
});
|
|
|
|
it("serves audit log, settings, accounts, stats, and history", async () => {
|
|
const fixture = await createFixture();
|
|
|
|
const auditResponse = await fetch(`${fixture.baseUrl}/logs/audit?token=${fixture.token}&lines=20`);
|
|
expect(auditResponse.ok).toBe(true);
|
|
const auditPayload = await auditResponse.json() as Record<string, any>;
|
|
expect((auditPayload.lines || []).join("\n")).toContain("AUDIT-LINE");
|
|
|
|
const renameResponse = await fetch(`${fixture.baseUrl}/logs/rename?token=${fixture.token}&lines=20`);
|
|
expect(renameResponse.ok).toBe(true);
|
|
const renamePayload = await renameResponse.json() as Record<string, any>;
|
|
expect((renamePayload.lines || []).join("\n")).toContain("RENAME-LINE");
|
|
|
|
const traceResponse = await fetch(`${fixture.baseUrl}/logs/trace?token=${fixture.token}&lines=50`);
|
|
expect(traceResponse.ok).toBe(true);
|
|
const tracePayload = await traceResponse.json() as Record<string, any>;
|
|
expect((tracePayload.lines || []).join("\n")).toContain("TRACE-EVENT");
|
|
expect((tracePayload.lines || []).join("\n")).toContain("TRACE-MAIN-LINE");
|
|
|
|
const traceConfigResponse = await fetch(`${fixture.baseUrl}/trace/config?token=${fixture.token}&enable=0¬e=test`);
|
|
expect(traceConfigResponse.ok).toBe(true);
|
|
const traceConfigPayload = await traceConfigResponse.json() as Record<string, any>;
|
|
expect(traceConfigPayload.config?.enabled).toBe(false);
|
|
|
|
const settingsResponse = await fetch(`${fixture.baseUrl}/settings?token=${fixture.token}`);
|
|
expect(settingsResponse.ok).toBe(true);
|
|
const settingsPayload = await settingsResponse.json() as Record<string, any>;
|
|
expect(settingsPayload.accounts?.realDebrid?.configured).toBe(true);
|
|
expect(settingsPayload.extraction?.archivePasswordCount).toBe(0);
|
|
expect(JSON.stringify(settingsPayload)).not.toContain("rd-secret-token");
|
|
expect(JSON.stringify(settingsPayload)).not.toContain("key-a");
|
|
expect(JSON.stringify(settingsPayload)).not.toContain("key-b");
|
|
|
|
const accountsResponse = await fetch(`${fixture.baseUrl}/accounts?token=${fixture.token}`);
|
|
expect(accountsResponse.ok).toBe(true);
|
|
const accountsPayload = await accountsResponse.json() as Record<string, any>;
|
|
expect(accountsPayload.debridLink?.keyCount).toBe(2);
|
|
expect(accountsPayload.debridLink?.disabledKeyCount).toBe(1);
|
|
|
|
const statsResponse = await fetch(`${fixture.baseUrl}/stats?token=${fixture.token}`);
|
|
expect(statsResponse.ok).toBe(true);
|
|
const statsPayload = await statsResponse.json() as Record<string, any>;
|
|
expect(statsPayload.session?.totalDownloaded).toBeGreaterThan(0);
|
|
expect(statsPayload.allTime?.totalDownloadedAllTime).toBeGreaterThan(0);
|
|
|
|
const historyResponse = await fetch(`${fixture.baseUrl}/history?token=${fixture.token}&limit=10`);
|
|
expect(historyResponse.ok).toBe(true);
|
|
const historyPayload = await historyResponse.json() as Record<string, any>;
|
|
expect(historyPayload.total).toBe(1);
|
|
expect(historyPayload.entries?.[0]?.name).toBe("server-package");
|
|
expect(historyPayload.entries?.[0]?.urlCount).toBe(1);
|
|
});
|
|
|
|
it("downloads a support bundle zip", async () => {
|
|
const fixture = await createFixture();
|
|
fs.writeFileSync(path.join(fixture.baseDir, legacyManifestFile), JSON.stringify({ purpose: "legacy" }), "utf8");
|
|
const response = await fetch(`${fixture.baseUrl}/support/bundle?token=${fixture.token}`);
|
|
expect(response.ok).toBe(true);
|
|
expect(response.headers.get("content-type")).toContain("application/zip");
|
|
|
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
const zip = new AdmZip(buffer);
|
|
const entries = zip.getEntries().map((entry) => entry.entryName);
|
|
expect(entries).toContain("overview/settings.json");
|
|
expect(entries).toContain("overview/accounts.json");
|
|
expect(entries).toContain("overview/debug-setup.json");
|
|
expect(entries).toContain("overview/self-check.json");
|
|
expect(entries).toContain("overview/trace-config.json");
|
|
expect(entries).toContain("logs/audit.log");
|
|
expect(entries).toContain("logs/rename.log");
|
|
expect(entries).toContain("logs/trace.log");
|
|
expect(entries).toContain("runtime/debug_support_manifest.json");
|
|
expect(entries).toContain("overview/support-manifest.json");
|
|
expect(entries).not.toContain(`runtime/${legacyManifestFile}`);
|
|
expect(entries).not.toContain(["overview/", "a", "i-manifest.json"].join(""));
|
|
expect(entries).not.toContain("runtime/debug_token.txt");
|
|
});
|
|
|
|
it("rejects unauthenticated requests", async () => {
|
|
const fixture = await createFixture();
|
|
const response = await fetch(`${fixture.baseUrl}/status`);
|
|
expect(response.status).toBe(401);
|
|
});
|
|
});
|