Files
Multi-Debrid-Downloader/tests/visual/mock-electron-api.ts
T
Sucukdeluxe 8a112747fc feat(accounts): improve table controls and secure credential reveal
Add persistent keyboard-accessible column resizing and increase the action-column edge spacing. Disable the active-only refresh action when no enabled checkable account exists and move the account-enabled control below the daily limit field. Add an explicit trusted IPC path that reveals only the selected stored credential on demand, keeps credentials out of renderer snapshots and audit fields, clears dialog state on close, and supports native clipboard copying. Cover UI behavior, request validation, provider-specific credential lookup, preload forwarding, renderer-state secrecy, and IPC trust boundaries.
2026-08-15 01:36:04 +02:00

421 lines
16 KiB
TypeScript

import type { ElectronApi } from "../../src/shared/preload-api";
import type { HistoryEntry, RendererSettings, RendererSettingsUpdate } from "../../src/shared/types";
import type { VisualFixture } from "./fixtures";
const stableNoopUnsubscribe = (): void => {};
function clone<T>(value: T): T {
return structuredClone(value);
}
export function createVisualElectronApi(
fixture: VisualFixture,
search = typeof window === "undefined" ? "" : window.location.search
): ElectronApi {
const historyState = new URLSearchParams(search).get("history-state");
let historyRequestCount = 0;
const updateSettings = (settings: RendererSettingsUpdate): RendererSettings => {
const { archivePasswordList: _archivePasswordList, notifyUrl: _notifyUrl, ...safe } = settings;
Object.assign(fixture.snapshot.settings, safe);
return clone(fixture.snapshot.settings);
};
return {
getSnapshot: async () => clone(fixture.snapshot),
getVersion: async () => "2.0.12",
checkUpdates: async () => clone(fixture.update),
installUpdate: async () => ({ started: true, message: "Visual update gestartet" }),
openExternal: async () => true,
updateSettings: async (settings) => updateSettings(settings),
resetProviderDailyUsage: async (provider) => {
fixture.snapshot.settings.providerDailyUsageBytes[provider] = 0;
return clone(fixture.snapshot.settings);
},
resetDebridLinkApiKeyDailyUsage: async (keyId) => {
fixture.snapshot.settings.debridLinkApiKeyDailyUsageBytes[keyId] = 0;
return clone(fixture.snapshot.settings);
},
createAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
replaceAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
updateAccountSecret: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
revealAccountSecret: async () => ({ secret: "" }),
deleteAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
addLinks: async () => ({ addedPackages: 0, addedLinks: 0, invalidCount: 0 }),
addContainers: async () => ({ addedPackages: 0, addedLinks: 0 }),
getStartConflicts: async () => [],
resolveStartConflict: async (_packageId, policy) => ({
skipped: policy === "skip",
overwritten: policy === "overwrite"
}),
clearAll: async () => {
fixture.snapshot.session.packageOrder = [];
fixture.snapshot.session.packages = {};
fixture.snapshot.session.items = {};
fixture.snapshot.session.running = false;
fixture.snapshot.session.paused = false;
fixture.snapshot.session.totalDownloadedBytes = 0;
fixture.snapshot.packageSpeedBps = {};
fixture.snapshot.canStart = false;
fixture.snapshot.canStop = false;
fixture.snapshot.canPause = false;
},
start: async () => {
fixture.snapshot.session.running = true;
fixture.snapshot.session.paused = false;
fixture.snapshot.canStop = true;
fixture.snapshot.canPause = true;
},
startPackages: async (packageIds) => {
for (const packageId of packageIds) {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "downloading";
}
}
fixture.snapshot.session.running = true;
},
stop: async () => {
fixture.snapshot.session.running = false;
fixture.snapshot.session.paused = false;
fixture.snapshot.canStop = false;
fixture.snapshot.canPause = false;
},
togglePause: async () => {
fixture.snapshot.session.paused = !fixture.snapshot.session.paused;
return fixture.snapshot.session.paused;
},
cancelPackage: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.cancelled = true;
entry.status = "cancelled";
}
},
renamePackage: async (packageId, newName) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.name = newName;
}
},
reorderPackages: async (packageIds) => {
fixture.snapshot.session.packageOrder = [...packageIds];
},
removeItem: async (itemId) => {
const item = fixture.snapshot.session.items[itemId];
if (item) {
const entry = fixture.snapshot.session.packages[item.packageId];
if (entry) {
entry.itemIds = entry.itemIds.filter((id) => id !== itemId);
}
delete fixture.snapshot.session.items[itemId];
}
},
togglePackage: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.enabled = !entry.enabled;
}
},
exportPackageSelection: async (packageIds) => ({
saved: true,
packageCount: packageIds.length,
linkCount: packageIds.reduce(
(count, packageId) => count + (fixture.snapshot.session.packages[packageId]?.itemIds.length ?? 0),
0
),
filePath: "C:\\Visual\\Exports\\packages.txt"
}),
exportItemSelection: async (itemIds) => ({
saved: true,
packageCount: new Set(
itemIds.map((itemId) => fixture.snapshot.session.items[itemId]?.packageId).filter(Boolean)
).size,
linkCount: itemIds.length,
filePath: "C:\\Visual\\Exports\\items.txt"
}),
exportQueue: async () => ({ saved: true }),
importQueue: async () => ({ addedPackages: 0, addedLinks: 0 }),
toggleClipboard: async () => {
fixture.snapshot.clipboardActive = !fixture.snapshot.clipboardActive;
fixture.snapshot.settings.clipboardWatch = fixture.snapshot.clipboardActive;
return fixture.snapshot.clipboardActive;
},
writeClipboardText: async () => true,
pickFolder: async () => "C:\\Visual\\Selected",
pickContainers: async () => ["C:\\Visual\\Containers\\visual.dlc"],
getSessionStats: async () => ({
bandwidth: {
samples: [
{ timestamp: 1786312680000, speedBps: 10485760 },
{ timestamp: 1786312740000, speedBps: 11534336 },
{ timestamp: 1786312800000, speedBps: 12582912 }
],
currentSpeedBps: 12582912,
averageSpeedBps: 11534336,
maxSpeedBps: 15728640,
totalBytesSession: 16642998272,
sessionDurationSeconds: 3600
},
totalDownloads: 4,
completedDownloads: 1,
failedDownloads: 1,
activeDownloads: 1,
queuedDownloads: 1
}),
resetSessionStats: async () => {
fixture.snapshot.stats.totalDownloaded = 0;
fixture.snapshot.stats.totalFilesSession = 0;
fixture.snapshot.session.totalDownloadedBytes = 0;
},
resetDownloadStats: async () => {
fixture.snapshot.stats.totalDownloadedAllTime = 0;
fixture.snapshot.stats.totalFilesAllTime = 0;
fixture.snapshot.settings.totalDownloadedAllTime = 0;
fixture.snapshot.settings.totalCompletedFilesAllTime = 0;
fixture.snapshot.settings.totalRuntimeAllTimeMs = 0;
},
restart: async () => {},
quit: async () => {},
exportBackup: async () => ({ saved: true }),
selectBackupImport: async () => ({ selected: true, requiresPassphrase: false }),
importBackup: async () => ({ restored: true, relaunch: false, message: "Visual backup importiert" }),
cancelBackupImport: async () => {},
exportOnlineBackup: async () => ({ key: "visual-online-backup-key" }),
importOnlineBackup: async () => ({ restored: true, relaunch: false, message: "Visual online backup importiert" }),
exportSupportBundle: async () => ({ saved: true, filePath: "C:\\Visual\\Support\\support.zip" }),
openLog: async () => {},
openLogDirectory: async () => {},
openAuditLog: async () => {},
openRenameLog: async () => {},
openSessionLog: async () => {},
openTraceLog: async () => {},
openPackageLog: async () => {},
openItemLog: async () => {},
getDebugSetupCheck: async () => ({
status: "ok",
enabled: false,
runtimeBaseDir: "C:\\Visual\\Runtime",
host: "127.0.0.1",
port: 7843,
localOnly: true,
tokenConfigured: true,
tokenPath: "C:\\Visual\\Runtime\\debug-token",
supportManifestPath: "C:\\Visual\\Runtime\\support-manifest.json",
supportManifestPresent: true,
traceConfigPath: "C:\\Visual\\Runtime\\trace-config.json",
traceLogPath: "C:\\Visual\\Runtime\\trace.log",
traceEnabled: fixture.traceConfig.enabled,
traceAutoDisableAt: fixture.traceConfig.autoDisableAt,
diskSpace: {
runtime: { path: "C:\\Visual\\Runtime", totalBytes: 1099511627776, freeBytes: 549755813888, freePercent: 50 },
output: { path: "C:\\Visual\\Downloads", totalBytes: 1099511627776, freeBytes: 549755813888, freePercent: 50 },
extract: { path: "C:\\Visual\\Extracted", totalBytes: 1099511627776, freeBytes: 549755813888, freePercent: 50 }
},
logSummary: {
totalBytes: 12288,
main: { path: "C:\\Visual\\Runtime\\main.log", exists: true, bytes: 4096 },
mainBackup: { path: null, exists: false, bytes: 0 },
audit: { path: "C:\\Visual\\Runtime\\audit.log", exists: true, bytes: 2048 },
auditBackup: { path: null, exists: false, bytes: 0 },
rename: { path: "C:\\Visual\\Runtime\\rename.log", exists: true, bytes: 1024 },
renameBackup: { path: null, exists: false, bytes: 0 },
session: { path: "C:\\Visual\\Runtime\\session.log", exists: true, bytes: 2048 },
trace: { path: "C:\\Visual\\Runtime\\trace.log", exists: true, bytes: 3072 },
traceBackup: { path: null, exists: false, bytes: 0 },
sessionLogs: { path: "C:\\Visual\\Runtime\\sessions", exists: true, fileCount: 2, bytes: 2048 },
packageLogs: { path: "C:\\Visual\\Runtime\\packages", exists: true, fileCount: 3, bytes: 3072 },
itemLogs: { path: "C:\\Visual\\Runtime\\items", exists: true, fileCount: 4, bytes: 4096 }
},
supportBundle: {
estimatedBytes: 24576,
estimatedEntries: 12,
duplicatedLiveLogBytes: 0,
note: "Visual support bundle"
},
warnings: [],
notes: ["Deterministischer Visual-Harness"],
localUrls: {
health: "http://127.0.0.1:7843/health",
meta: "http://127.0.0.1:7843/meta",
diagnostics: "http://127.0.0.1:7843/diagnostics"
},
remoteUrlTemplates: {
health: "https://visual.example.test/health",
meta: "https://visual.example.test/meta",
diagnostics: "https://visual.example.test/diagnostics"
}
}),
getRecentErrors: async () => [
{ ts: "2026-08-10T11:55:00.000Z", level: "WARN", message: "Visualer Beispielhinweis" }
],
testNotification: async () => true,
getTraceConfig: async () => clone(fixture.traceConfig),
setTraceEnabled: async (enabled) => {
fixture.traceConfig = { ...fixture.traceConfig, enabled };
return clone(fixture.traceConfig);
},
rotateDebugToken: async () => ({ path: "C:\\Visual\\Runtime\\debug-token" }),
getRemoteDiagnostics: async () => clone(fixture.remoteDiagnostics),
enableRemoteDiagnostics: async (input) => {
fixture.remoteDiagnostics = {
...fixture.remoteDiagnostics,
status: {
...fixture.remoteDiagnostics.status,
running: true,
host: input.hostMode === "local" ? "127.0.0.1" : "0.0.0.0",
port: input.port ?? 7843,
localOnly: input.hostMode === "local",
allowlistCount: input.allowlist.length
},
publicHost: input.publicHost,
name: input.name ?? fixture.remoteDiagnostics.name,
allowlist: [...input.allowlist]
};
return clone(fixture.remoteDiagnostics);
},
disableRemoteDiagnostics: async () => {
fixture.remoteDiagnostics = {
...fixture.remoteDiagnostics,
status: { ...fixture.remoteDiagnostics.status, running: false }
};
return clone(fixture.remoteDiagnostics);
},
rotateRemoteDiagnosticsToken: async () => clone(fixture.remoteDiagnostics),
openRealDebridLogin: async () => {},
openAllDebridLogin: async () => {},
importBestDebridCookies: async () => 2,
getAllDebridHostInfo: async () => ({
host: "rapidgator.net",
source: "api",
state: "up",
statusLabel: "Verfügbar",
fetchedAt: 1786312800000,
lastCheckedAt: 1786312740000,
quota: 42,
quotaMax: 100,
quotaType: "daily",
limitSimuDl: 8,
note: "Visual host status"
}),
getDebridLinkHostLimits: async () => {
const primaryKey = fixture.snapshot.accounts.find((account) => account.kind === "debridlink-api");
if (!primaryKey) return [];
return [{
keyId: primaryKey.accountId,
keyLabel: "Key 1",
host: "ddownload.com",
fetchedAt: 1786312800000,
trafficCurrentBytes: 53687091200,
trafficMaxBytes: 268435456000,
linksCurrent: 12,
linksMax: 100,
note: "Visual quota",
state: "ready",
stateLabel: "Bereit",
stateDetail: "Kontingent verfügbar",
cooldownUntil: null,
cooldownRemainingMs: 0,
lastCheckedAt: 1786312740000,
hostState: "up",
hostStateLabel: "Online",
hostNote: "Hoster verfügbar"
}];
},
checkDebridAccounts: async () => clone(Object.values(fixture.snapshot.settings.debridAccountStatuses)),
checkAccountCredentials: async (input) => clone(fixture.snapshot.accounts.find((account) => account.accountId === input.accountId)?.status || {
accountId: input.accountId || "visual-account",
provider: input.kind === "debridlink-api" ? "debridlink" : "megadebrid",
label: "Visual Account",
maskedLogin: "vi***al",
valid: true,
isPremium: true,
premiumUntilMs: null,
message: "Premium aktiv",
checkedAt: 1786312800000
}),
retryExtraction: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "extracting";
}
},
extractNow: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "extracting";
}
},
resetPackage: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "queued";
entry.cancelled = false;
}
},
getHistory: async () => {
historyRequestCount += 1;
if (historyRequestCount > 1 && historyState === "loading") {
return new Promise<HistoryEntry[]>(() => {});
}
if (historyRequestCount > 1 && historyState === "error") {
throw new Error("Visual history load failed");
}
return clone(fixture.history);
},
clearHistory: async () => {
fixture.history.splice(0, fixture.history.length);
},
removeHistoryEntry: async (entryId) => {
const index = fixture.history.findIndex((entry) => entry.id === entryId);
if (index >= 0) {
fixture.history.splice(index, 1);
}
},
revealHistoryEntry: async (entryId) => fixture.history.some((entry) => entry.id === entryId)
? { ok: true }
: { ok: false, reason: "entry-not-found" },
setPackagePriority: async (packageId, priority) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.priority = priority;
}
},
skipItems: async (itemIds) => {
for (const itemId of itemIds) {
const item = fixture.snapshot.session.items[itemId];
if (item) {
item.status = "cancelled";
item.fullStatus = "Übersprungen";
}
}
},
resetItems: async (itemIds) => {
for (const itemId of itemIds) {
const item = fixture.snapshot.session.items[itemId];
if (item) {
item.status = "queued";
item.downloadedBytes = 0;
item.progressPercent = 0;
item.speedBps = 0;
item.lastError = "";
item.fullStatus = "In Warteschlange";
}
}
},
startItems: async (itemIds) => {
for (const itemId of itemIds) {
const item = fixture.snapshot.session.items[itemId];
if (item) {
item.status = "downloading";
item.fullStatus = "Download läuft";
}
}
fixture.snapshot.session.running = true;
},
reportRendererError: () => {},
onStateUpdate: () => stableNoopUnsubscribe,
onClipboardDetected: () => stableNoopUnsubscribe,
onUpdateInstallProgress: () => stableNoopUnsubscribe
};
}