226 lines
8.0 KiB
TypeScript
226 lines
8.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const saveMajoorSettings = vi.fn();
|
|
const applySettingsToConfig = vi.fn();
|
|
const syncBackendSecuritySettings = vi.fn();
|
|
const setSecuritySettings = vi.fn();
|
|
const setRuntimeSecurityToken = vi.fn();
|
|
const comfyToast = vi.fn();
|
|
const t = vi.fn((key, fallback, vars) => {
|
|
if (!fallback) return key;
|
|
if (!vars || typeof vars !== "object") return fallback;
|
|
return String(fallback).replace(/\{(\w+)\}/g, (_m, name) => String(vars[name] ?? `{${name}}`));
|
|
});
|
|
|
|
vi.mock("../app/i18n.js", () => ({
|
|
t,
|
|
}));
|
|
|
|
vi.mock("../app/toast.js", () => ({
|
|
comfyToast,
|
|
}));
|
|
|
|
vi.mock("../api/client.js", () => ({
|
|
setSecuritySettings,
|
|
setRuntimeSecurityToken,
|
|
}));
|
|
|
|
vi.mock("../app/settings/settingsCore.js", () => ({
|
|
saveMajoorSettings,
|
|
applySettingsToConfig,
|
|
syncBackendSecuritySettings,
|
|
}));
|
|
|
|
describe("settingsSecurity recommended remote LAN preset", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.unstubAllGlobals();
|
|
vi.clearAllMocks();
|
|
globalThis.location = {
|
|
protocol: "http:",
|
|
hostname: "192.168.1.10",
|
|
};
|
|
vi.stubGlobal("crypto", {
|
|
getRandomValues: vi.fn((buffer) => {
|
|
for (let index = 0; index < buffer.length; index += 1) {
|
|
buffer[index] = (index * 17 + 23) & 0xff;
|
|
}
|
|
return buffer;
|
|
}),
|
|
});
|
|
setSecuritySettings.mockResolvedValue({ ok: true, data: { prefs: {} } });
|
|
});
|
|
|
|
it("builds the recommended preset with a generated token on HTTP LAN sessions", async () => {
|
|
const mod = await import("../app/settings/settingsSecurity.js");
|
|
const patch = mod.buildRecommendedRemoteLanSecuritySettings({});
|
|
|
|
expect(patch.allowWrite).toBe(true);
|
|
expect(patch.requireAuth).toBe(true);
|
|
expect(patch.allowRemoteWrite).toBe(false);
|
|
expect(patch.allowInsecureTokenTransport).toBe(true);
|
|
expect(String(patch.apiToken)).toMatch(/^mjr_[0-9a-f]+$/);
|
|
expect(String(patch.apiToken)).toHaveLength(40);
|
|
});
|
|
|
|
it("rejects token generation when secure randomness is unavailable", async () => {
|
|
vi.stubGlobal("crypto", undefined);
|
|
|
|
const mod = await import("../app/settings/settingsSecurity.js");
|
|
|
|
expect(() => mod.generateRecommendedApiToken()).toThrow(
|
|
"Secure token generation requires crypto.getRandomValues().",
|
|
);
|
|
});
|
|
|
|
it("applies the preset through the settings toggle and syncs backend security", async () => {
|
|
const mod = await import("../app/settings/settingsSecurity.js");
|
|
const defs = [];
|
|
const settings = {
|
|
safety: {},
|
|
security: {
|
|
allowWrite: false,
|
|
requireAuth: false,
|
|
allowRemoteWrite: false,
|
|
allowInsecureTokenTransport: false,
|
|
},
|
|
};
|
|
const notifyApplied = vi.fn();
|
|
|
|
mod.registerSecuritySettings((def) => defs.push(def), settings, notifyApplied);
|
|
|
|
const presetDef = defs.find((def) => def.id === "Majoor.Security.RemoteLanPreset");
|
|
expect(presetDef).toBeTruthy();
|
|
|
|
presetDef.onChange(true);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
expect(saveMajoorSettings).toHaveBeenCalled();
|
|
expect(setRuntimeSecurityToken).toHaveBeenCalledTimes(1);
|
|
expect(setSecuritySettings).toHaveBeenCalledTimes(1);
|
|
expect(setSecuritySettings).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
allow_write: true,
|
|
require_auth: true,
|
|
allow_remote_write: false,
|
|
allow_insecure_token_transport: true,
|
|
}),
|
|
);
|
|
expect(String(settings.security.apiToken)).toMatch(/^mjr_[0-9a-f]+$/);
|
|
expect(notifyApplied).toHaveBeenCalledWith("security.remoteLanPreset");
|
|
expect(notifyApplied).toHaveBeenCalledWith("security.apiToken");
|
|
expect(syncBackendSecuritySettings).toHaveBeenCalledTimes(1);
|
|
expect(comfyToast).toHaveBeenCalledWith(
|
|
"Recommended remote LAN setup applied. This browser session is now authorized for Majoor write operations.",
|
|
"success",
|
|
);
|
|
});
|
|
|
|
it("persists a manually entered API token into the runtime auth session", async () => {
|
|
const mod = await import("../app/settings/settingsSecurity.js");
|
|
const defs = [];
|
|
const settings = {
|
|
safety: {},
|
|
security: {},
|
|
};
|
|
|
|
mod.registerSecuritySettings((def) => defs.push(def), settings, vi.fn());
|
|
|
|
const tokenDef = defs.find((def) => def.id === "Majoor.Security.ApiToken");
|
|
expect(tokenDef).toBeTruthy();
|
|
|
|
tokenDef.onChange("mjr_manual_token_1234567890");
|
|
await Promise.resolve();
|
|
|
|
expect(setRuntimeSecurityToken).toHaveBeenCalledWith("mjr_manual_token_1234567890");
|
|
expect(setSecuritySettings).toHaveBeenCalledWith({
|
|
api_token: "mjr_manual_token_1234567890",
|
|
});
|
|
});
|
|
|
|
it("does not push unchanged security toggle values during initial settings hydration", async () => {
|
|
const mod = await import("../app/settings/settingsSecurity.js");
|
|
const defs = [];
|
|
const settings = {
|
|
safety: {
|
|
confirmDeletion: true,
|
|
},
|
|
security: {
|
|
allowWrite: true,
|
|
requireAuth: true,
|
|
allowRemoteWrite: false,
|
|
allowInsecureTokenTransport: false,
|
|
},
|
|
};
|
|
|
|
mod.registerSecuritySettings((def) => defs.push(def), settings, vi.fn());
|
|
|
|
const allowWriteDef = defs.find((def) => def.id === "Majoor.Security.allowWrite");
|
|
expect(allowWriteDef).toBeTruthy();
|
|
|
|
allowWriteDef.onChange(true);
|
|
await Promise.resolve();
|
|
|
|
expect(saveMajoorSettings).not.toHaveBeenCalled();
|
|
expect(setSecuritySettings).not.toHaveBeenCalled();
|
|
expect(syncBackendSecuritySettings).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses permissive local defaults except safe mode when security settings are missing", async () => {
|
|
const mod = await import("../app/settings/settingsSecurity.js");
|
|
const defs = [];
|
|
const settings = {
|
|
safety: {},
|
|
security: {},
|
|
};
|
|
|
|
mod.registerSecuritySettings((def) => defs.push(def), settings, vi.fn());
|
|
|
|
expect(defs.find((def) => def.id === "Majoor.Safety.ConfirmDeletion")?.defaultValue).toBe(
|
|
true,
|
|
);
|
|
expect(defs.find((def) => def.id === "Majoor.Security.safeMode")?.defaultValue).toBe(
|
|
false,
|
|
);
|
|
expect(defs.find((def) => def.id === "Majoor.Security.allowWrite")?.defaultValue).toBe(
|
|
true,
|
|
);
|
|
expect(defs.find((def) => def.id === "Majoor.Security.allowDelete")?.defaultValue).toBe(
|
|
true,
|
|
);
|
|
expect(defs.find((def) => def.id === "Majoor.Security.allowRename")?.defaultValue).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
defs.find((def) => def.id === "Majoor.Security.allowOpenInFolder")?.defaultValue,
|
|
).toBe(true);
|
|
expect(defs.find((def) => def.id === "Majoor.Security.allowResetIndex")?.defaultValue).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("does not push an unchanged API token during initial settings hydration", async () => {
|
|
const mod = await import("../app/settings/settingsSecurity.js");
|
|
const defs = [];
|
|
const settings = {
|
|
safety: {},
|
|
security: {
|
|
apiToken: "mjr_same_token_1234567890",
|
|
},
|
|
};
|
|
|
|
mod.registerSecuritySettings((def) => defs.push(def), settings, vi.fn());
|
|
|
|
const tokenDef = defs.find((def) => def.id === "Majoor.Security.ApiToken");
|
|
expect(tokenDef).toBeTruthy();
|
|
|
|
tokenDef.onChange("mjr_same_token_1234567890");
|
|
await Promise.resolve();
|
|
|
|
expect(saveMajoorSettings).not.toHaveBeenCalled();
|
|
expect(setRuntimeSecurityToken).not.toHaveBeenCalled();
|
|
expect(setSecuritySettings).not.toHaveBeenCalled();
|
|
});
|
|
});
|