Centralize DebridAccountStatus sanitizing for direct account-check responses. The shared sanitizer collects stored and submitted credential variants, including raw, trimmed, URL-encoded, URL-decoded, full credential lines, and login:secret forms, then redacts credential-like query params, key-value echoes, authorization, cookie, API-key, token, password, secret, session, backup passphrase, and archive password text before any status DTO reaches the renderer. Apply the sanitizer to bulk checkDebridAccounts results, single checkAccountCredentials results, and account command credential checks before returning, throwing, or persisting statuses. Reuse the same sanitizer for renderer snapshots so status redaction stays on one path. Replace the Debrid-Link key popup copy action with a truthful non-secret masked-identity copy action and cover the regression so no renderer path copies key.token or reports a secret-copy success without a secret readback.
92 lines
3.8 KiB
TypeScript
92 lines
3.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { AppController } from "../src/main/app-controller";
|
|
import { defaultSettings } from "../src/main/constants";
|
|
import type { AppSettings, DebridAccountStatus } from "../src/shared/types";
|
|
|
|
vi.mock("electron", () => ({
|
|
app: { getPath: () => "C:\\MDD\\Test" },
|
|
BrowserWindow: class {},
|
|
clipboard: {},
|
|
dialog: {},
|
|
ipcMain: { handle: vi.fn(), on: vi.fn() },
|
|
Menu: { buildFromTemplate: vi.fn(), setApplicationMenu: vi.fn() },
|
|
safeStorage: { isEncryptionAvailable: () => false, encryptString: vi.fn(), decryptString: vi.fn() },
|
|
shell: {},
|
|
Tray: class {}
|
|
}));
|
|
|
|
function mockFetchOnce(status: number, body: unknown): void {
|
|
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
vi.stubGlobal("fetch", vi.fn(async () => ({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
text: async () => text
|
|
})) as unknown as typeof fetch);
|
|
}
|
|
|
|
function createController(settings: AppSettings): AppController {
|
|
const controller = Object.create(AppController.prototype) as {
|
|
settings: AppSettings;
|
|
manager: { applyDebridAccountStatuses: ReturnType<typeof vi.fn> };
|
|
audit: ReturnType<typeof vi.fn>;
|
|
};
|
|
controller.settings = settings;
|
|
controller.manager = { applyDebridAccountStatuses: vi.fn() };
|
|
controller.audit = vi.fn();
|
|
return controller as unknown as AppController;
|
|
}
|
|
|
|
function expectNoSecret(payload: unknown, secrets: readonly string[]): void {
|
|
const serialized = JSON.stringify(payload);
|
|
for (const secret of secrets) {
|
|
expect(serialized).not.toContain(secret);
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("account-check IPC result sanitizing", () => {
|
|
it("sanitizes bulk check statuses before returning or persisting them", async () => {
|
|
const login = "bulk-status@example.test";
|
|
const secret = "bulk raw secret+ä?=1";
|
|
const encodedSecret = encodeURIComponent(secret);
|
|
const credentialLine = `${login}:${secret}`;
|
|
const settings = {
|
|
...defaultSettings(),
|
|
megaCredentials: credentialLine,
|
|
megaDebridApiCredentials: credentialLine,
|
|
megaDebridApiEnabled: true
|
|
};
|
|
const providerText = `Denied https://www.mega-debrid.eu/api.php?action=connectUser&login=${encodeURIComponent(login)}&password=${encodedSecret} Authorization: Bearer ${encodedSecret} raw ${secret} line ${credentialLine}`;
|
|
mockFetchOnce(200, { response_code: "error", response_text: providerText });
|
|
const controller = createController(settings);
|
|
|
|
const statuses = await controller.checkDebridAccounts();
|
|
|
|
expectNoSecret(statuses, [secret, encodedSecret, credentialLine]);
|
|
expectNoSecret((controller as unknown as { manager: { applyDebridAccountStatuses: ReturnType<typeof vi.fn> } }).manager.applyDebridAccountStatuses.mock.calls, [secret, encodedSecret, credentialLine]);
|
|
expect(statuses[0]?.message).toContain("[geschützt]");
|
|
});
|
|
|
|
it("sanitizes single credential checks with raw, encoded, header and query echoes", async () => {
|
|
const secret = "single raw token+ö?=2";
|
|
const encodedSecret = encodeURIComponent(secret);
|
|
const echoedHeaderSecret = "provider-header-token-95K";
|
|
const echoedPassphrase = "provider-backup-passphrase-54A";
|
|
const providerText = `Rejected https://debrid-link.com/api/v2/account/infos?access_token=${encodedSecret}&apiKey=${secret} Authorization: Bearer ${encodedSecret} Cookie: sid=${secret} X-Api-Key: ${echoedHeaderSecret}
|
|
Backup-Passphrase=${echoedPassphrase}`;
|
|
mockFetchOnce(200, { success: false, error: providerText });
|
|
const controller = createController(defaultSettings());
|
|
|
|
const status = await controller.checkAccountCredentials({
|
|
kind: "debridlink-api",
|
|
secret
|
|
});
|
|
|
|
expectNoSecret(status, [secret, encodedSecret, echoedHeaderSecret, echoedPassphrase]);
|
|
expect((status as DebridAccountStatus).message).toContain("[geschützt]");
|
|
});
|
|
});
|