Compare commits
No commits in common. "8f3681b160146d70b08fd9e15f4bd0b9a9fe6349" and "c79a031be84e56194b6bf2c1e519005fe12a047d" have entirely different histories.
8f3681b160
...
c79a031be8
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.7.224",
|
||||
"version": "1.7.223",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -45,7 +45,7 @@ import { runInstallWithResume } from "./update-install-flow";
|
||||
import { rotateDebugToken, startDebugServer, stopDebugServer, restartDebugServer, getDebugServerRuntimeStatus, getActiveDebugToken, getDebugAllowlist, writeDebugServerConfig, clearDebugToken } from "./debug-server";
|
||||
import { encodeConnectionCode, loadRemoteMeta, saveRemoteMeta } from "./connection-code";
|
||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||
import { buildBackupPayload, planBackupImport, resolveMcpRemoteRestore, BackupMcpRemote } from "./backup-payload";
|
||||
import { buildBackupPayload, planBackupImport } from "./backup-payload";
|
||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
||||
import { initAccountRotationLog, shutdownAccountRotationLog } from "./account-rotation-log";
|
||||
import { initConversionLog, shutdownConversionLog } from "./conversion-trace";
|
||||
@ -372,23 +372,6 @@ export class AppController {
|
||||
return this.getRemoteDiagnostics();
|
||||
}
|
||||
|
||||
private restoreMcpRemoteFromBackup(section: unknown, restartNow: boolean): void {
|
||||
const restore = resolveMcpRemoteRestore(section);
|
||||
if (!restore) {
|
||||
return;
|
||||
}
|
||||
writeDebugServerConfig({ host: restore.host, port: restore.port, allowlist: restore.allowlist });
|
||||
if (restartNow) {
|
||||
void restartDebugServer().catch(() => {});
|
||||
}
|
||||
this.audit("INFO", "Ferndiagnose-Einstellungen aus Backup wiederhergestellt", {
|
||||
port: restore.port ?? null,
|
||||
allowlistCount: restore.allowlist?.length ?? 0,
|
||||
host: restore.host ?? "unveraendert",
|
||||
restartNow
|
||||
});
|
||||
}
|
||||
|
||||
public getDebugSetupCheck(): DebugSetupCheckResult {
|
||||
return getDebugSetupCheck(this.storagePaths.baseDir);
|
||||
}
|
||||
@ -741,22 +724,13 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
}
|
||||
|
||||
public exportBackup(): Buffer {
|
||||
let mcpRemote: BackupMcpRemote | undefined;
|
||||
if (Boolean(this.settings.backupIncludeMcp)) {
|
||||
const status = getDebugServerRuntimeStatus();
|
||||
mcpRemote = {
|
||||
allowlist: getDebugAllowlist(),
|
||||
port: status.port,
|
||||
hostMode: status.host === "0.0.0.0" ? "network" : "local"
|
||||
};
|
||||
}
|
||||
const includeDownloads = Boolean(this.settings.backupIncludeDownloads);
|
||||
const payloadObj = buildBackupPayload({
|
||||
settings: { ...this.settings },
|
||||
appVersion: APP_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
session: this.manager.getSession(),
|
||||
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()),
|
||||
mcpRemote
|
||||
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits())
|
||||
});
|
||||
this.audit("INFO", "Backup exportiert", {
|
||||
kind: payloadObj.kind,
|
||||
@ -830,7 +804,6 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
this.settings = restoredSettings;
|
||||
saveSettings(this.storagePaths, this.settings);
|
||||
this.manager.setSettings(this.settings, { suppressRetroactiveCleanup: true });
|
||||
this.restoreMcpRemoteFromBackup(parsed.mcpRemote, true);
|
||||
this.audit("INFO", "Backup importiert (nur Einstellungen)", {
|
||||
accountSummary: buildAccountSummary(this.settings)
|
||||
});
|
||||
@ -867,8 +840,6 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
|
||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||
|
||||
this.restoreMcpRemoteFromBackup(parsed.mcpRemote, false);
|
||||
|
||||
this.manager.skipShutdownPersist = true;
|
||||
this.manager.blockAllPersistence = true;
|
||||
logger.info("Backup wiederhergestellt — App startet automatisch neu");
|
||||
|
||||
@ -2,12 +2,6 @@ import type { AppSettings, SessionState, HistoryEntry } from "../shared/types";
|
||||
|
||||
export type BackupKind = "full" | "settings-only";
|
||||
|
||||
export interface BackupMcpRemote {
|
||||
allowlist: string[];
|
||||
port: number;
|
||||
hostMode: "local" | "network";
|
||||
}
|
||||
|
||||
export interface BackupPayload {
|
||||
version: 2;
|
||||
kind: BackupKind;
|
||||
@ -16,7 +10,6 @@ export interface BackupPayload {
|
||||
settings: AppSettings;
|
||||
session?: SessionState;
|
||||
history?: HistoryEntry[];
|
||||
mcpRemote?: BackupMcpRemote;
|
||||
}
|
||||
|
||||
export interface BuildBackupInput {
|
||||
@ -26,7 +19,6 @@ export interface BuildBackupInput {
|
||||
/** Only bundled when includeDownloads is true. */
|
||||
session: SessionState;
|
||||
history: HistoryEntry[];
|
||||
mcpRemote?: BackupMcpRemote;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,39 +40,9 @@ export function buildBackupPayload(input: BuildBackupInput): BackupPayload {
|
||||
base.session = input.session;
|
||||
base.history = input.history;
|
||||
}
|
||||
if (Boolean(input.settings.backupIncludeMcp) && input.mcpRemote) {
|
||||
base.mcpRemote = input.mcpRemote;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export interface McpRemoteRestore {
|
||||
host?: "127.0.0.1" | "0.0.0.0";
|
||||
port?: number;
|
||||
allowlist?: string[];
|
||||
}
|
||||
|
||||
export function resolveMcpRemoteRestore(section: unknown): McpRemoteRestore | null {
|
||||
if (!section || typeof section !== "object") {
|
||||
return null;
|
||||
}
|
||||
const s = section as { allowlist?: unknown; port?: unknown; hostMode?: unknown };
|
||||
const allowlist = Array.isArray(s.allowlist)
|
||||
? s.allowlist.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
|
||||
: undefined;
|
||||
const port = (typeof s.port === "number" && Number.isInteger(s.port) && s.port >= 1024 && s.port <= 65535) ? s.port : undefined;
|
||||
let host: "127.0.0.1" | "0.0.0.0" | undefined;
|
||||
if (s.hostMode === "network") {
|
||||
host = allowlist && allowlist.length > 0 ? "0.0.0.0" : "127.0.0.1";
|
||||
} else if (s.hostMode === "local") {
|
||||
host = "127.0.0.1";
|
||||
}
|
||||
if (host === undefined && port === undefined && allowlist === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { host, port, allowlist };
|
||||
}
|
||||
|
||||
export interface ImportPlan {
|
||||
valid: boolean;
|
||||
/** Restore the download list (session + history) and relaunch. */
|
||||
|
||||
@ -109,7 +109,6 @@ export function defaultSettings(): AppSettings {
|
||||
hideExtractedItems: true,
|
||||
confirmDeleteSelection: true,
|
||||
backupIncludeDownloads: false,
|
||||
backupIncludeMcp: false,
|
||||
notifyUrl: "",
|
||||
notifyMention: "",
|
||||
notifyOnPackageCompleted: false,
|
||||
|
||||
@ -461,7 +461,6 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
hideExtractedItems: settings.hideExtractedItems !== undefined ? Boolean(settings.hideExtractedItems) : defaults.hideExtractedItems,
|
||||
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
|
||||
backupIncludeDownloads: settings.backupIncludeDownloads !== undefined ? Boolean(settings.backupIncludeDownloads) : defaults.backupIncludeDownloads,
|
||||
backupIncludeMcp: settings.backupIncludeMcp !== undefined ? Boolean(settings.backupIncludeMcp) : defaults.backupIncludeMcp,
|
||||
notifyUrl: asText(settings.notifyUrl) || defaults.notifyUrl,
|
||||
notifyMention: asText(settings.notifyMention) || defaults.notifyMention,
|
||||
notifyOnPackageCompleted: settings.notifyOnPackageCompleted !== undefined ? Boolean(settings.notifyOnPackageCompleted) : defaults.notifyOnPackageCompleted,
|
||||
|
||||
@ -856,7 +856,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
|
||||
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
||||
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
||||
theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, backupIncludeMcp: false,
|
||||
theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false,
|
||||
notifyUrl: "", notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
|
||||
accountListShowDetailedDebridLinkKeys: false,
|
||||
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
|
||||
@ -5442,8 +5442,6 @@ export function App(): ReactElement {
|
||||
<div className="setting-hint">Sicherheitsabfrage vor dem Entfernen ausgewählter Einträge.</div>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.backupIncludeDownloads} onChange={(e) => setBool("backupIncludeDownloads", e.target.checked)} /> Download-Liste mitsichern</label>
|
||||
<div className="setting-hint">Sicherung enthält auch die Download-Liste; Standard: nur Einstellungen.</div>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.backupIncludeMcp} onChange={(e) => setBool("backupIncludeMcp", e.target.checked)} /> Ferndiagnose-Einstellungen mitsichern</label>
|
||||
<div className="setting-hint">Allowlist, Port und Freigabemodus (lokal/Netzwerk) reisen mit. Verbindungs-Token und eigene Adresse bleiben pro Server – nach dem Import einmal „Aktivieren" drücken.</div>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.theme === "light"} onChange={(e) => {
|
||||
const next = e.target.checked ? "light" : "dark";
|
||||
settingsDraftRevisionRef.current += 1;
|
||||
|
||||
@ -134,7 +134,6 @@ export interface AppSettings {
|
||||
hideExtractedItems: boolean;
|
||||
confirmDeleteSelection: boolean;
|
||||
backupIncludeDownloads: boolean;
|
||||
backupIncludeMcp: boolean;
|
||||
notifyUrl: string;
|
||||
notifyMention: string;
|
||||
notifyOnPackageCompleted: boolean;
|
||||
|
||||
@ -1,199 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { once } from "node:events";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { buildBackupPayload, resolveMcpRemoteRestore, BackupMcpRemote } from "../src/main/backup-payload";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { normalizeSettings } from "../src/main/storage";
|
||||
import {
|
||||
startDebugServer,
|
||||
stopDebugServer,
|
||||
restartDebugServer,
|
||||
writeDebugServerConfig,
|
||||
getDebugAllowlist,
|
||||
getDebugServerRuntimeStatus
|
||||
} from "../src/main/debug-server";
|
||||
import type { DownloadManager } from "../src/main/download-manager";
|
||||
import type { AppSettings, SessionState } from "../src/shared/types";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function input(settingsOverride: Partial<AppSettings>, mcpRemote?: BackupMcpRemote) {
|
||||
return {
|
||||
settings: { ...defaultSettings(), ...settingsOverride } as AppSettings,
|
||||
appVersion: "1.7.224",
|
||||
exportedAt: "2026-06-19T00:00:00.000Z",
|
||||
session: {} as unknown as SessionState,
|
||||
history: [],
|
||||
mcpRemote
|
||||
};
|
||||
}
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
const probe = http.createServer();
|
||||
probe.listen(0, "127.0.0.1");
|
||||
await once(probe, "listening");
|
||||
const address = probe.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("port probe failed");
|
||||
}
|
||||
probe.close();
|
||||
await once(probe, "close");
|
||||
return address.port;
|
||||
}
|
||||
|
||||
async function waitForReady(url: string): Promise<void> {
|
||||
const deadline = Date.now() + 5000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 40));
|
||||
}
|
||||
throw new Error(`debug server not ready: ${url}`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
stopDebugServer();
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (!dir) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("backup mcpRemote export gating", () => {
|
||||
it("includes mcpRemote when backupIncludeMcp is on", () => {
|
||||
const section: BackupMcpRemote = { allowlist: ["10.0.0.5", "192.168.1.0/24"], port: 9999, hostMode: "network" };
|
||||
const payload = buildBackupPayload(input({ backupIncludeMcp: true }, section));
|
||||
expect(payload.mcpRemote).toEqual(section);
|
||||
});
|
||||
|
||||
it("omits mcpRemote when the toggle is off even if a section is provided", () => {
|
||||
const payload = buildBackupPayload(input({ backupIncludeMcp: false }, { allowlist: ["10.0.0.5"], port: 9868, hostMode: "network" }));
|
||||
expect(payload.mcpRemote).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits mcpRemote when toggle on but no section gathered", () => {
|
||||
const payload = buildBackupPayload(input({ backupIncludeMcp: true }, undefined));
|
||||
expect(payload.mcpRemote).toBeUndefined();
|
||||
});
|
||||
|
||||
it("the mcpRemote section carries ONLY allowlist/port/hostMode (no token, publicHost, name)", () => {
|
||||
const payload = buildBackupPayload(input({ backupIncludeMcp: true }, { allowlist: ["10.0.0.5"], port: 9868, hostMode: "network" }));
|
||||
expect(payload.mcpRemote && Object.keys(payload.mcpRemote).sort()).toEqual(["allowlist", "hostMode", "port"]);
|
||||
const sectionJson = JSON.stringify(payload.mcpRemote);
|
||||
expect(sectionJson.toLowerCase()).not.toContain("token");
|
||||
expect(sectionJson).not.toContain("publicHost");
|
||||
expect(sectionJson.toLowerCase()).not.toContain("\"name\"");
|
||||
});
|
||||
});
|
||||
|
||||
describe("backupIncludeMcp settings persistence", () => {
|
||||
it("normalizeSettings preserves backupIncludeMcp (the toggle survives save/load)", () => {
|
||||
expect(normalizeSettings({ backupIncludeMcp: true } as unknown as AppSettings).backupIncludeMcp).toBe(true);
|
||||
expect(normalizeSettings({ backupIncludeMcp: false } as unknown as AppSettings).backupIncludeMcp).toBe(false);
|
||||
expect(normalizeSettings({} as unknown as AppSettings).backupIncludeMcp).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMcpRemoteRestore", () => {
|
||||
it("maps network + non-empty allowlist to 0.0.0.0", () => {
|
||||
expect(resolveMcpRemoteRestore({ allowlist: ["10.0.0.5"], port: 9868, hostMode: "network" }))
|
||||
.toEqual({ host: "0.0.0.0", port: 9868, allowlist: ["10.0.0.5"] });
|
||||
});
|
||||
|
||||
it("SAFETY: network with EMPTY allowlist binds local, never 0.0.0.0", () => {
|
||||
expect(resolveMcpRemoteRestore({ allowlist: [], port: 9868, hostMode: "network" })?.host).toBe("127.0.0.1");
|
||||
});
|
||||
|
||||
it("maps local to 127.0.0.1", () => {
|
||||
expect(resolveMcpRemoteRestore({ allowlist: ["10.0.0.5"], port: 9868, hostMode: "local" })?.host).toBe("127.0.0.1");
|
||||
});
|
||||
|
||||
it("rejects an out-of-range or non-integer port", () => {
|
||||
expect(resolveMcpRemoteRestore({ allowlist: ["10.0.0.5"], port: 80, hostMode: "network" })?.port).toBeUndefined();
|
||||
expect(resolveMcpRemoteRestore({ allowlist: ["10.0.0.5"], port: 70000, hostMode: "network" })?.port).toBeUndefined();
|
||||
expect(resolveMcpRemoteRestore({ allowlist: ["10.0.0.5"], port: 9868.5, hostMode: "network" })?.port).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters non-string and blank allowlist entries and trims", () => {
|
||||
const r = resolveMcpRemoteRestore({ allowlist: ["10.0.0.5", "", " ", 5, null, " 8.8.8.8 "], port: 9868, hostMode: "network" });
|
||||
expect(r?.allowlist).toEqual(["10.0.0.5", "8.8.8.8"]);
|
||||
});
|
||||
|
||||
it("returns null for missing or empty/invalid sections", () => {
|
||||
expect(resolveMcpRemoteRestore(undefined)).toBeNull();
|
||||
expect(resolveMcpRemoteRestore(null)).toBeNull();
|
||||
expect(resolveMcpRemoteRestore("x")).toBeNull();
|
||||
expect(resolveMcpRemoteRestore({})).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("backup mcpRemote live restore round-trip", () => {
|
||||
it("export -> resolve -> apply is reflected in the running debug-server (proves restart fired)", async () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-bkmcp-"));
|
||||
tempDirs.push(baseDir);
|
||||
const startPort = await getFreePort();
|
||||
const restorePort = await getFreePort();
|
||||
fs.writeFileSync(path.join(baseDir, "debug_token.txt"), "rt-secret", "utf8");
|
||||
fs.writeFileSync(path.join(baseDir, "debug_port.txt"), String(startPort), "utf8");
|
||||
fs.writeFileSync(path.join(baseDir, "debug_host.txt"), "127.0.0.1", "utf8");
|
||||
fs.writeFileSync(path.join(baseDir, "debug_allowlist.txt"), "", "utf8");
|
||||
startDebugServer({} as unknown as DownloadManager, baseDir);
|
||||
await waitForReady(`http://127.0.0.1:${startPort}/health?token=rt-secret`);
|
||||
expect(getDebugAllowlist()).toEqual([]);
|
||||
|
||||
const payload = buildBackupPayload(input(
|
||||
{ backupIncludeMcp: true },
|
||||
{ allowlist: ["203.0.113.4", "10.0.0.0/24"], port: restorePort, hostMode: "network" }
|
||||
));
|
||||
|
||||
const restore = resolveMcpRemoteRestore(payload.mcpRemote);
|
||||
expect(restore).not.toBeNull();
|
||||
writeDebugServerConfig({ host: restore!.host, port: restore!.port, allowlist: restore!.allowlist });
|
||||
const status = await restartDebugServer();
|
||||
|
||||
expect(getDebugAllowlist()).toEqual(["203.0.113.4", "10.0.0.0/24"]);
|
||||
expect(status.port).toBe(restorePort);
|
||||
expect(status.host).toBe("0.0.0.0");
|
||||
expect(status.allowlistCount).toBe(2);
|
||||
|
||||
expect(fs.readFileSync(path.join(baseDir, "debug_token.txt"), "utf8").trim()).toBe("rt-secret");
|
||||
expect(fs.existsSync(path.join(baseDir, "debug_remote.json"))).toBe(false);
|
||||
|
||||
await waitForReady(`http://127.0.0.1:${restorePort}/health?token=rt-secret`);
|
||||
});
|
||||
|
||||
it("full-backup path writes the debug_* files to disk without a restart (boot picks them up)", async () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-bkmcp2-"));
|
||||
tempDirs.push(baseDir);
|
||||
const startPort = await getFreePort();
|
||||
fs.writeFileSync(path.join(baseDir, "debug_token.txt"), "rt2", "utf8");
|
||||
fs.writeFileSync(path.join(baseDir, "debug_port.txt"), String(startPort), "utf8");
|
||||
fs.writeFileSync(path.join(baseDir, "debug_host.txt"), "127.0.0.1", "utf8");
|
||||
fs.writeFileSync(path.join(baseDir, "debug_allowlist.txt"), "", "utf8");
|
||||
startDebugServer({} as unknown as DownloadManager, baseDir);
|
||||
await waitForReady(`http://127.0.0.1:${startPort}/health?token=rt2`);
|
||||
|
||||
const restore = resolveMcpRemoteRestore({ allowlist: ["198.51.100.9"], port: 9100, hostMode: "network" });
|
||||
writeDebugServerConfig({ host: restore!.host, port: restore!.port, allowlist: restore!.allowlist });
|
||||
|
||||
expect(fs.readFileSync(path.join(baseDir, "debug_host.txt"), "utf8").trim()).toBe("0.0.0.0");
|
||||
expect(fs.readFileSync(path.join(baseDir, "debug_port.txt"), "utf8").trim()).toBe("9100");
|
||||
expect(fs.readFileSync(path.join(baseDir, "debug_allowlist.txt"), "utf8")).toContain("198.51.100.9");
|
||||
expect(getDebugServerRuntimeStatus().port).toBe(startPort);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user