Files
Multi-Debrid-Downloader/tests/credential-protection.test.ts

177 lines
6.4 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,
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("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");
});
});