Files
Multi-Debrid-Downloader/tests/credential-protection.test.ts
Sucukdeluxe 1b7caba2eb feat: add Deepbrid, daily scheduling, and notification center
Add encrypted Deepbrid API accounts with account validation, provider routing, fallback, usage tracking, safe error handling, and verified 1Fichier downloads. Restore persistent recurring daily starts with local-calendar deduplication and legacy schedule compatibility. Add durable Discord package, run, remaining-volume, stall, and recovery notifications with privacy-safe telemetry and disk-failure recovery.
2026-08-24 07:37:55 +02:00

203 lines
7.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { defaultSettings } from "../src/main/constants";
import { logger } from "../src/main/logger";
import { collectAccountStatusRedactionValues, sanitizeAccountStatusText } from "../src/main/account-status-sanitizer";
import {
configureCredentialProtector,
CredentialProtector,
needsPersistedSettingsRewrite,
protectPersistedSettings,
projectSettingsForRenderer,
restorePersistedSettings
} from "../src/main/credential-protection";
function createProtector(available = true): CredentialProtector {
return {
isEncryptionAvailable: () => available,
encryptString: (value) => Buffer.from(value, "utf8").reverse(),
decryptString: (value) => Buffer.from(value).reverse().toString("utf8")
};
}
describe("credential protection", () => {
beforeEach(() => {
configureCredentialProtector(createProtector());
});
afterEach(() => {
vi.restoreAllMocks();
});
it("protects remembered provider values and restores them for the main process", () => {
const input = {
...defaultSettings(),
rememberToken: true,
token: "value-to-protect",
realDebridApiTokens: "first-real-debrid-token\nsecond-real-debrid-token",
megaLogin: "account@example.invalid",
megaPassword: "password-value"
};
const persisted = protectPersistedSettings(input);
expect(persisted.token).not.toBe(input.token);
expect(persisted.realDebridApiTokens).not.toBe(input.realDebridApiTokens);
expect(persisted.megaLogin).not.toBe(input.megaLogin);
expect(JSON.stringify(persisted)).not.toContain(input.token);
expect(JSON.stringify(persisted)).not.toContain("first-real-debrid-token");
expect(restorePersistedSettings(persisted)).toMatchObject({
token: input.token,
realDebridApiTokens: input.realDebridApiTokens,
megaLogin: input.megaLogin,
megaPassword: input.megaPassword
});
});
it("protects, restores, clears and redacts the Deepbrid API key", () => {
const key = "fixture-deepbrid-protection-key-3cD5";
const input = { ...defaultSettings(), rememberToken: true, deepbridApiKey: key };
const persisted = protectPersistedSettings(input);
expect(JSON.stringify(persisted)).not.toContain(key);
expect(restorePersistedSettings(persisted).deepbridApiKey).toBe(key);
expect(projectSettingsForRenderer(input).deepbridApiKey).toBe("••••••••");
expect(protectPersistedSettings({ ...input, rememberToken: false }).deepbridApiKey).toBe("");
const redactions = collectAccountStatusRedactionValues(input);
expect(sanitizeAccountStatusText(`Deepbrid api_key=${key}`, redactions)).not.toContain(key);
});
it("requires a persisted settings rewrite only while the Deepbrid key is unprotected", () => {
const input = {
...defaultSettings(),
rememberToken: true,
deepbridApiKey: "fixture-deepbrid-rewrite-key-6rS7"
};
expect(needsPersistedSettingsRewrite(input)).toBe(true);
expect(needsPersistedSettingsRewrite(protectPersistedSettings(input))).toBe(false);
});
it("accepts plaintext values once so existing settings can be migrated", () => {
const input = {
...defaultSettings(),
rememberToken: true,
token: "legacy-value"
};
expect(restorePersistedSettings(input).token).toBe(input.token);
expect(protectPersistedSettings(restorePersistedSettings(input)).token).not.toBe(input.token);
});
it("protects plaintext credentials that begin with the legacy marker text", () => {
const input = {
...defaultSettings(),
rememberToken: true,
token: "mdd-safe-storage:v1:literal-credential"
};
const persisted = protectPersistedSettings(input);
expect(persisted.token).not.toBe(input.token);
expect(JSON.stringify(persisted)).not.toContain(input.token);
expect(restorePersistedSettings(persisted).token).toBe(input.token);
});
it("reports encryption failures without logging credential data", () => {
const value = "credential-value-not-for-logs";
const warn = vi.spyOn(logger, "warn").mockImplementation(() => {});
configureCredentialProtector({
isEncryptionAvailable: () => true,
encryptString: () => { throw new Error("encryption failed"); },
decryptString: () => ""
});
const persisted = protectPersistedSettings({ ...defaultSettings(), token: value });
expect(persisted.token).toBe("");
expect(warn).toHaveBeenCalledWith("Credential-Verschlüsselung fehlgeschlagen: token");
expect(JSON.stringify(warn.mock.calls)).not.toContain(value);
});
it("reports decryption failures without logging persisted data", () => {
const persisted = protectPersistedSettings({ ...defaultSettings(), token: "value-to-encrypt" });
const serialized = JSON.stringify(persisted.token);
const warn = vi.spyOn(logger, "warn").mockImplementation(() => {});
configureCredentialProtector({
isEncryptionAvailable: () => true,
encryptString: () => Buffer.alloc(0),
decryptString: () => { throw new Error("decryption failed"); }
});
const restored = restorePersistedSettings(persisted);
expect(restored.token).toBe("");
expect(warn).toHaveBeenCalledWith("Credential-Entschlüsselung fehlgeschlagen: token");
expect(JSON.stringify(warn.mock.calls)).not.toContain(serialized);
});
it("does not persist provider values when encryption is unavailable", () => {
configureCredentialProtector(createProtector(false));
const persisted = protectPersistedSettings({
...defaultSettings(),
rememberToken: true,
token: "ephemeral-value"
});
expect(persisted.token).toBe("");
});
it("removes persisted provider values when remembering is disabled", () => {
const persisted = protectPersistedSettings({
...defaultSettings(),
rememberToken: false,
token: "ephemeral-value",
megaLogin: "account@example.invalid",
megaPassword: "password-value"
});
expect(persisted.token).toBe("");
expect(persisted.megaLogin).toBe("");
expect(persisted.megaPassword).toBe("");
});
it("projects only masked presence metadata into renderer settings", () => {
const input = {
...defaultSettings(),
rememberToken: true,
token: "value-to-protect",
realDebridApiTokens: "pool-value-to-protect",
megaDebridApiCredentials: "account@example.invalid:password-value",
debridLinkApiKeys: "key-value"
};
const projected = projectSettingsForRenderer(input);
const serialized = JSON.stringify(projected);
expect(serialized).not.toContain(input.token);
expect(serialized).not.toContain(input.realDebridApiTokens);
expect(serialized).not.toContain("account@example.invalid");
expect(serialized).not.toContain("password-value");
expect(serialized).not.toContain("key-value");
expect(projected.token).not.toBe("");
expect(projected.realDebridApiTokens).not.toBe("");
expect(projected.megaDebridApiCredentials).not.toBe("");
expect(projected.debridLinkApiKeys).not.toBe("");
});
it("redacts every Real-Debrid pool token from account status text", () => {
const settings = {
...defaultSettings(),
realDebridApiTokens: "first-private-token\nsecond-private-token"
};
const redactions = collectAccountStatusRedactionValues(settings);
const sanitized = sanitizeAccountStatusText(
"Unrestrict first-private-token schlug fehl; Fallback second-private-token ebenfalls",
redactions
);
expect(sanitized).not.toContain("first-private-token");
expect(sanitized).not.toContain("second-private-token");
});
});