real-debrid-downloader/tests/storage.test.ts
Sucukdeluxe 63fd402083 Release v1.4.20 with comprehensive audit fixes (140 issues) and expanded test coverage
- Speed calculation: raised minimum elapsed floor to 0.5s preventing unrealistic spikes
- Reconnect: exponential backoff with consecutive counter, clock regression protection
- Download engine: retry byte tracking (itemContributedBytes), mkdir before createWriteStream, content-length validation
- Fire-and-forget promises: all void promises now have .catch() error handlers
- Session recovery: normalize stale active statuses to queued on crash recovery, clear speedBps
- Storage: config backup (.bak) before overwrite, EXDEV cross-device rename fallback with type guard
- IPC security: input validation on all string/array IPC handlers, CSP headers in production
- Main process: clipboard memory limit (50KB), installer timing increased to 800ms
- Debrid: attribute-order-independent meta tag regex for Rapidgator filename extraction
- Constants: named constants for magic numbers (MAX_MANIFEST_FILE_BYTES, MAX_LINK_ARTIFACT_BYTES, etc.)
- Extractor/integrity: use shared constants, document password visibility and TOCTOU limitations
- Tests: 103 tests total (55 new), covering utils, storage, integrity, cleanup, extractor, debrid, update

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 06:23:24 +01:00

335 lines
11 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { AppSettings } from "../src/shared/types";
import { defaultSettings } from "../src/main/constants";
import { createStoragePaths, emptySession, loadSession, loadSettings, normalizeSettings, saveSession, saveSettings } from "../src/main/storage";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("settings storage", () => {
it("does not persist provider credentials when rememberToken is disabled", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
tempDirs.push(dir);
const paths = createStoragePaths(dir);
saveSettings(paths, {
...defaultSettings(),
rememberToken: false,
token: "rd-token",
megaLogin: "mega-user",
megaPassword: "mega-pass",
bestToken: "best-token",
allDebridToken: "all-token"
});
const raw = JSON.parse(fs.readFileSync(paths.configFile, "utf8")) as Record<string, unknown>;
expect(raw.token).toBe("");
expect(raw.megaLogin).toBe("");
expect(raw.megaPassword).toBe("");
expect(raw.bestToken).toBe("");
expect(raw.allDebridToken).toBe("");
const loaded = loadSettings(paths);
expect(loaded.rememberToken).toBe(false);
expect(loaded.token).toBe("");
expect(loaded.megaLogin).toBe("");
expect(loaded.megaPassword).toBe("");
expect(loaded.bestToken).toBe("");
expect(loaded.allDebridToken).toBe("");
});
it("persists provider credentials when rememberToken is enabled", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
tempDirs.push(dir);
const paths = createStoragePaths(dir);
saveSettings(paths, {
...defaultSettings(),
rememberToken: true,
token: "rd-token",
megaLogin: "mega-user",
megaPassword: "mega-pass",
bestToken: "best-token",
allDebridToken: "all-token"
});
const loaded = loadSettings(paths);
expect(loaded.token).toBe("rd-token");
expect(loaded.megaLogin).toBe("mega-user");
expect(loaded.megaPassword).toBe("mega-pass");
expect(loaded.bestToken).toBe("best-token");
expect(loaded.allDebridToken).toBe("all-token");
});
it("normalizes invalid enum and numeric values", () => {
const normalized = normalizeSettings({
...defaultSettings(),
providerPrimary: "invalid-provider" as unknown as AppSettings["providerPrimary"],
providerSecondary: "invalid-provider" as unknown as AppSettings["providerSecondary"],
providerTertiary: "invalid-provider" as unknown as AppSettings["providerTertiary"],
cleanupMode: "broken" as unknown as AppSettings["cleanupMode"],
extractConflictMode: "broken" as unknown as AppSettings["extractConflictMode"],
completedCleanupPolicy: "broken" as unknown as AppSettings["completedCleanupPolicy"],
speedLimitMode: "broken" as unknown as AppSettings["speedLimitMode"],
maxParallel: 0,
reconnectWaitSeconds: 9999,
speedLimitKbps: -1,
outputDir: " ",
extractDir: " ",
updateRepo: " "
});
expect(normalized.providerPrimary).toBe("realdebrid");
expect(normalized.providerSecondary).toBe("none");
expect(normalized.providerTertiary).toBe("none");
expect(normalized.cleanupMode).toBe("none");
expect(normalized.extractConflictMode).toBe("overwrite");
expect(normalized.completedCleanupPolicy).toBe("never");
expect(normalized.speedLimitMode).toBe("global");
expect(normalized.maxParallel).toBe(1);
expect(normalized.reconnectWaitSeconds).toBe(600);
expect(normalized.speedLimitKbps).toBe(0);
expect(normalized.outputDir).toBe(defaultSettings().outputDir);
expect(normalized.extractDir).toBe(defaultSettings().extractDir);
expect(normalized.updateRepo).toBe(defaultSettings().updateRepo);
});
it("normalizes malformed persisted config on load", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
tempDirs.push(dir);
const paths = createStoragePaths(dir);
fs.writeFileSync(
paths.configFile,
JSON.stringify({
providerPrimary: "not-valid",
completedCleanupPolicy: "not-valid",
maxParallel: "999",
reconnectWaitSeconds: "1",
speedLimitMode: "not-valid",
updateRepo: ""
}),
"utf8"
);
const loaded = loadSettings(paths);
expect(loaded.providerPrimary).toBe("realdebrid");
expect(loaded.completedCleanupPolicy).toBe("never");
expect(loaded.maxParallel).toBe(50);
expect(loaded.reconnectWaitSeconds).toBe(10);
expect(loaded.speedLimitMode).toBe("global");
expect(loaded.updateRepo).toBe(defaultSettings().updateRepo);
});
it("keeps explicit none as fallback provider choice", () => {
const normalized = normalizeSettings({
...defaultSettings(),
providerSecondary: "none",
providerTertiary: "none"
});
expect(normalized.providerSecondary).toBe("none");
expect(normalized.providerTertiary).toBe("none");
});
it("normalizes archive password list line endings", () => {
const normalized = normalizeSettings({
...defaultSettings(),
archivePasswordList: "one\r\ntwo\r\nthree"
});
expect(normalized.archivePasswordList).toBe("one\ntwo\nthree");
});
it("resets stale active statuses to queued on session load", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
tempDirs.push(dir);
const paths = createStoragePaths(dir);
const session = emptySession();
session.packages["pkg1"] = {
id: "pkg1",
name: "Test Package",
outputDir: "/tmp/out",
extractDir: "/tmp/extract",
status: "downloading",
itemIds: ["item1", "item2", "item3", "item4"],
cancelled: false,
enabled: true,
createdAt: Date.now(),
updatedAt: Date.now()
};
session.items["item1"] = {
id: "item1",
packageId: "pkg1",
url: "https://example.com/file1.rar",
provider: null,
status: "downloading",
retries: 0,
speedBps: 1024,
downloadedBytes: 5000,
totalBytes: 10000,
progressPercent: 50,
fileName: "file1.rar",
targetPath: "/tmp/out/file1.rar",
resumable: true,
attempts: 1,
lastError: "some error",
fullStatus: "",
createdAt: Date.now(),
updatedAt: Date.now()
};
session.items["item2"] = {
id: "item2",
packageId: "pkg1",
url: "https://example.com/file2.rar",
provider: null,
status: "paused",
retries: 0,
speedBps: 0,
downloadedBytes: 0,
totalBytes: null,
progressPercent: 0,
fileName: "file2.rar",
targetPath: "/tmp/out/file2.rar",
resumable: false,
attempts: 0,
lastError: "",
fullStatus: "",
createdAt: Date.now(),
updatedAt: Date.now()
};
session.items["item3"] = {
id: "item3",
packageId: "pkg1",
url: "https://example.com/file3.rar",
provider: null,
status: "completed",
retries: 0,
speedBps: 0,
downloadedBytes: 10000,
totalBytes: 10000,
progressPercent: 100,
fileName: "file3.rar",
targetPath: "/tmp/out/file3.rar",
resumable: false,
attempts: 1,
lastError: "",
fullStatus: "",
createdAt: Date.now(),
updatedAt: Date.now()
};
session.items["item4"] = {
id: "item4",
packageId: "pkg1",
url: "https://example.com/file4.rar",
provider: null,
status: "queued",
retries: 0,
speedBps: 0,
downloadedBytes: 0,
totalBytes: null,
progressPercent: 0,
fileName: "file4.rar",
targetPath: "/tmp/out/file4.rar",
resumable: false,
attempts: 0,
lastError: "",
fullStatus: "",
createdAt: Date.now(),
updatedAt: Date.now()
};
saveSession(paths, session);
const loaded = loadSession(paths);
// Active statuses (downloading, paused) should be reset to "queued"
expect(loaded.items["item1"].status).toBe("queued");
expect(loaded.items["item2"].status).toBe("queued");
// Speed should be cleared
expect(loaded.items["item1"].speedBps).toBe(0);
// lastError should be cleared for reset items
expect(loaded.items["item1"].lastError).toBe("");
// Completed and queued statuses should be preserved
expect(loaded.items["item3"].status).toBe("completed");
expect(loaded.items["item4"].status).toBe("queued");
// Downloaded bytes should be preserved
expect(loaded.items["item1"].downloadedBytes).toBe(5000);
// Package data should be preserved
expect(loaded.packages["pkg1"].name).toBe("Test Package");
});
it("returns empty session when session file contains invalid JSON", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
tempDirs.push(dir);
const paths = createStoragePaths(dir);
fs.writeFileSync(paths.sessionFile, "{{{corrupted json!!!", "utf8");
const loaded = loadSession(paths);
const empty = emptySession();
expect(loaded.packages).toEqual(empty.packages);
expect(loaded.items).toEqual(empty.items);
expect(loaded.packageOrder).toEqual(empty.packageOrder);
});
it("returns defaults when config file contains invalid JSON", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
tempDirs.push(dir);
const paths = createStoragePaths(dir);
// Write invalid JSON to the config file
fs.writeFileSync(paths.configFile, "{{{{not valid json!!!}", "utf8");
const loaded = loadSettings(paths);
const defaults = defaultSettings();
expect(loaded.providerPrimary).toBe(defaults.providerPrimary);
expect(loaded.maxParallel).toBe(defaults.maxParallel);
expect(loaded.outputDir).toBe(defaults.outputDir);
expect(loaded.cleanupMode).toBe(defaults.cleanupMode);
});
it("applies defaults for missing fields when loading old config", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
tempDirs.push(dir);
const paths = createStoragePaths(dir);
// Write a minimal config that simulates an old version missing newer fields
fs.writeFileSync(
paths.configFile,
JSON.stringify({
token: "my-token",
rememberToken: true,
outputDir: "/custom/output"
}),
"utf8"
);
const loaded = loadSettings(paths);
const defaults = defaultSettings();
// Old fields should be preserved
expect(loaded.token).toBe("my-token");
expect(loaded.outputDir).toBe("/custom/output");
// Missing new fields should get default values
expect(loaded.autoProviderFallback).toBe(defaults.autoProviderFallback);
expect(loaded.hybridExtract).toBe(defaults.hybridExtract);
expect(loaded.completedCleanupPolicy).toBe(defaults.completedCleanupPolicy);
expect(loaded.speedLimitMode).toBe(defaults.speedLimitMode);
expect(loaded.clipboardWatch).toBe(defaults.clipboardWatch);
expect(loaded.minimizeToTray).toBe(defaults.minimizeToTray);
expect(loaded.theme).toBe(defaults.theme);
expect(loaded.bandwidthSchedules).toEqual(defaults.bandwidthSchedules);
expect(loaded.updateRepo).toBe(defaults.updateRepo);
});
});