Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/plugins/services/.server/PluginSettingsService.test.ts
T

518 lines
24 KiB
TypeScript

import { promises as nodeFs } from "node:fs";
import type { FileHandle } from "node:fs/promises";
import { mkdtemp, mkdir, symlink, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, test, vi } from "vitest";
import type { PluginManifest } from "@toolctrl/plugin-runtime";
const getPluginForTenantMock = vi.fn();
const publishAuditEventViaOutboxMock = vi.fn();
const dbMock = {
$transaction: vi.fn(),
pluginUserConfig: {
findUnique: vi.fn(),
upsert: vi.fn(),
},
};
vi.mock("./PluginLifecycleService", () => ({
getPluginForTenant: (...args: unknown[]) => getPluginForTenantMock(...args),
}));
vi.mock("@toolctrl/db/audit-events.server", () => ({
publishAuditEventViaOutbox: (...args: unknown[]) => publishAuditEventViaOutboxMock(...args),
}));
vi.mock("~/utils/db.server", () => ({
db: dbMock,
}));
const baseManifest = {
schemaVersion: 1,
id: "com.acme.settings",
version: "1.0.0",
name: "Settings",
entry: "plugin.wasm",
wasmSha256: "0".repeat(64),
signature: "0".repeat(128),
signedBy: "0".repeat(64),
allowedHosts: [],
capabilities: [],
hooks: [{ name: "entity.created", export: "handleEntityCreated" }],
} satisfies PluginManifest;
const settingsHtml = "<main>Settings</main>";
const settingsHtmlSha256 = createHash("sha256").update(settingsHtml).digest("hex");
const settingsHtmlIntegrity = `sha256-${createHash("sha256").update(settingsHtml).digest("base64")}`;
function pluginFixture(manifest: PluginManifest = { ...baseManifest, ui: { settings: { entry: "ui/settings.html", sha256: settingsHtmlSha256 } } }) {
return {
id: "plugin-row-1",
pluginId: "com.acme.settings",
version: "1.0.0",
enabled: true,
manifest,
installedAt: new Date("2026-05-01T00:00:00.000Z"),
updatedAt: new Date("2026-05-01T00:00:00.000Z"),
};
}
beforeEach(() => {
getPluginForTenantMock.mockReset();
publishAuditEventViaOutboxMock.mockReset();
publishAuditEventViaOutboxMock.mockResolvedValue(undefined);
dbMock.$transaction.mockReset();
dbMock.$transaction.mockImplementation((callback: (tx: typeof dbMock) => unknown) => callback(dbMock));
dbMock.pluginUserConfig.findUnique.mockReset();
dbMock.pluginUserConfig.upsert.mockReset();
delete process.env.PLUGIN_BINARY_BASE_PATH;
});
describe("PluginSettingsService", () => {
test("loads settings HTML through tenant-scoped plugin lookup", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "plugin-settings-"));
const bundleRoot = path.join(root, "com.acme.settings", "1.0.0", "ui");
await mkdir(bundleRoot, { recursive: true });
await writeFile(path.join(bundleRoot, "settings.html"), settingsHtml);
process.env.PLUGIN_BINARY_BASE_PATH = root;
getPluginForTenantMock.mockResolvedValue(pluginFixture());
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
const result = await loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" });
expect(getPluginForTenantMock).toHaveBeenCalledWith({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" });
expect(result.entry).toBe("ui/settings.html");
expect(result.trustedHtml).toBe(settingsHtml);
});
test("loads settings HTML declared with SRI integrity metadata", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "plugin-settings-"));
const bundleRoot = path.join(root, "com.acme.settings", "1.0.0", "ui");
await mkdir(bundleRoot, { recursive: true });
await writeFile(path.join(bundleRoot, "settings.html"), settingsHtml);
process.env.PLUGIN_BINARY_BASE_PATH = root;
getPluginForTenantMock.mockResolvedValue(
pluginFixture({ ...baseManifest, ui: { settings: { entry: "ui/settings.html", integrity: settingsHtmlIntegrity } } })
);
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
const result = await loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" });
expect(result.trustedHtml).toBe(settingsHtml);
});
test("rejects manifests without settings UI", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture(baseManifest));
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
await expect(loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" })).rejects.toMatchObject({
code: "settings_ui_unavailable",
});
});
test("rejects settings UI manifests without HTML integrity metadata before touching the filesystem", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture({ ...baseManifest, ui: { settings: { entry: "ui/settings.html" } } as never }));
const openSpy = vi.spyOn(nodeFs, "open");
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
try {
await expect(loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" })).rejects.toMatchObject({
code: "settings_ui_integrity_missing",
});
expect(openSpy).not.toHaveBeenCalled();
} finally {
openSpy.mockRestore();
}
});
test("rejects unsafe entries before touching the filesystem", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture({ ...baseManifest, ui: { settings: { entry: "../settings.html" } } }));
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
await expect(loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" })).rejects.toMatchObject({
code: "settings_ui_entry_unsafe",
});
});
test("rejects settings HTML whose bytes differ from the manifest sha256", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "plugin-settings-"));
const bundleRoot = path.join(root, "com.acme.settings", "1.0.0", "ui");
await mkdir(bundleRoot, { recursive: true });
await writeFile(path.join(bundleRoot, "settings.html"), "<main>Tampered</main>");
process.env.PLUGIN_BINARY_BASE_PATH = root;
getPluginForTenantMock.mockResolvedValue(pluginFixture());
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
await expect(loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" })).rejects.toMatchObject({
code: "settings_ui_integrity_mismatch",
});
});
test("rejects symlink traversal out of the plugin bundle", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "plugin-settings-"));
const outside = path.join(root, "outside.html");
const bundleRoot = path.join(root, "com.acme.settings", "1.0.0", "ui");
await mkdir(bundleRoot, { recursive: true });
await writeFile(outside, "<main>Outside</main>");
await symlink(outside, path.join(bundleRoot, "settings.html"));
process.env.PLUGIN_BINARY_BASE_PATH = root;
getPluginForTenantMock.mockResolvedValue(pluginFixture());
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
await expect(loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" })).rejects.toMatchObject({
code: "settings_ui_entry_unsafe",
});
});
test("rejects oversized settings HTML after opening the file handle and before reading it into the route payload", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "plugin-settings-"));
const bundleRoot = path.join(root, "com.acme.settings", "1.0.0", "ui");
const settingsPath = path.join(bundleRoot, "settings.html");
await mkdir(bundleRoot, { recursive: true });
await writeFile(settingsPath, settingsHtml);
process.env.PLUGIN_BINARY_BASE_PATH = root;
getPluginForTenantMock.mockResolvedValue(pluginFixture());
const originalOpen = nodeFs.open.bind(nodeFs);
const openSpy = vi.spyOn(nodeFs, "open").mockImplementation(async (...args: Parameters<typeof nodeFs.open>) => {
const fileHandle = (await originalOpen(...args)) as FileHandle;
const originalStat = fileHandle.stat.bind(fileHandle);
vi.spyOn(fileHandle, "stat").mockImplementation(async (...statArgs: Parameters<FileHandle["stat"]>) => {
await writeFile(settingsPath, `<main>${"x".repeat(257 * 1024)}</main>`);
return originalStat(...statArgs);
});
return fileHandle;
});
const { loadPluginSettingsHtml } = await import("./PluginSettingsService");
try {
await expect(loadPluginSettingsHtml({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" })).rejects.toMatchObject({
code: "settings_ui_file_too_large",
});
expect(openSpy).toHaveBeenCalledWith(expect.stringContaining("settings.html"), "r");
} finally {
openSpy.mockRestore();
}
});
test("loads per-user settings through tenant-scoped plugin lookup", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture());
dbMock.pluginUserConfig.findUnique.mockResolvedValue({
configJson: {
theme: "dark",
count: 2,
apiKey: "must-not-reach-frame",
password: "must-not-reach-frame",
"../escape": "unsafe",
__proto__: "polluted",
constructor: "unsafe",
prototype: "unsafe",
oversized: "x".repeat(33 * 1024),
tooManyItems: Array.from({ length: 513 }, (_, index) => index),
tooManyKeys: Object.fromEntries(Array.from({ length: 257 }, (_, index) => [`k${index}`, index])),
nestedReserved: JSON.parse('{"safe":true,"__proto__":{"polluted":true}}'),
nestedSecret: { appearance: { apiKey: "must-not-reach-frame" } },
ignored: () => "nope",
},
});
const { loadPluginUserSettings } = await import("./PluginSettingsService");
const result = await loadPluginUserSettings({ tenantId: "tenant-1", pluginRowId: "plugin-row-1", userId: "user-1" });
expect(getPluginForTenantMock).toHaveBeenCalledWith({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" });
expect(dbMock.pluginUserConfig.findUnique).toHaveBeenCalledWith({
where: { pluginId_userId: { pluginId: "plugin-row-1", userId: "user-1" } },
select: { configJson: true },
});
expect(result).toEqual({ theme: "dark", count: 2 });
expect(JSON.stringify(result)).not.toContain("must-not-reach-frame");
expect(result).not.toHaveProperty("apiKey");
expect(result).not.toHaveProperty("password");
expect(result).not.toHaveProperty("../escape");
expect(result).not.toHaveProperty("__proto__");
expect(result).not.toHaveProperty("constructor");
expect(result).not.toHaveProperty("prototype");
expect(result).not.toHaveProperty("oversized");
expect(result).not.toHaveProperty("tooManyItems");
expect(result).not.toHaveProperty("tooManyKeys");
expect(result).not.toHaveProperty("nestedReserved");
expect(result).not.toHaveProperty("nestedSecret");
});
test("loads settings page data with a single tenant plugin lookup", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "plugin-settings-"));
const bundleRoot = path.join(root, "com.acme.settings", "1.0.0", "ui");
await mkdir(bundleRoot, { recursive: true });
await writeFile(path.join(bundleRoot, "settings.html"), settingsHtml);
process.env.PLUGIN_BINARY_BASE_PATH = root;
getPluginForTenantMock.mockResolvedValue(pluginFixture());
dbMock.pluginUserConfig.findUnique.mockResolvedValue({ configJson: { theme: "dark" } });
const { loadPluginSettingsPageData } = await import("./PluginSettingsService");
const result = await loadPluginSettingsPageData({ tenantId: "tenant-1", pluginRowId: "plugin-row-1", userId: "user-1" });
expect(getPluginForTenantMock).toHaveBeenCalledOnce();
expect(getPluginForTenantMock).toHaveBeenCalledWith({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" });
expect(dbMock.pluginUserConfig.findUnique).toHaveBeenCalledWith({
where: { pluginId_userId: { pluginId: "plugin-row-1", userId: "user-1" } },
select: { configJson: true },
});
expect(result).toMatchObject({
entry: "ui/settings.html",
trustedHtml: settingsHtml,
settings: { theme: "dark" },
plugin: { id: "plugin-row-1", pluginId: "com.acme.settings", version: "1.0.0" },
});
});
test("rejects per-user settings reads for disabled plugins before reading config", async () => {
getPluginForTenantMock.mockResolvedValue({ ...pluginFixture(), enabled: false });
const { loadPluginUserSettings } = await import("./PluginSettingsService");
await expect(loadPluginUserSettings({ tenantId: "tenant-1", pluginRowId: "plugin-row-1", userId: "user-1" })).rejects.toMatchObject({
code: "plugin_disabled",
});
expect(dbMock.pluginUserConfig.findUnique).not.toHaveBeenCalled();
});
test("rejects per-user settings reads when no safe settings UI is declared before reading config", async () => {
const { loadPluginUserSettings } = await import("./PluginSettingsService");
const args = { tenantId: "tenant-1", pluginRowId: "plugin-row-1", userId: "user-1" };
getPluginForTenantMock.mockResolvedValueOnce(pluginFixture(baseManifest));
await expect(loadPluginUserSettings(args)).rejects.toMatchObject({ code: "settings_ui_unavailable" });
getPluginForTenantMock.mockResolvedValueOnce(pluginFixture({ ...baseManifest, ui: { settings: { entry: "../settings.html" } } }));
await expect(loadPluginUserSettings(args)).rejects.toMatchObject({ code: "settings_ui_entry_unsafe" });
expect(dbMock.pluginUserConfig.findUnique).not.toHaveBeenCalled();
});
test("upserts a single per-user setting without storing secret-like keys", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture());
dbMock.pluginUserConfig.findUnique.mockResolvedValue({ configJson: { theme: "dark" } });
dbMock.pluginUserConfig.upsert.mockResolvedValue({});
const { setPluginUserSetting } = await import("./PluginSettingsService");
const result = await setPluginUserSetting({
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
userId: "user-1",
key: "layout.mode",
value: "compact",
actor: { userId: "user-1", email: "user@example.com", ip: "127.0.0.1" },
});
expect(dbMock.pluginUserConfig.upsert).toHaveBeenCalledWith({
where: { pluginId_userId: { pluginId: "plugin-row-1", userId: "user-1" } },
create: { pluginId: "plugin-row-1", userId: "user-1", configJson: { theme: "dark", "layout.mode": "compact" } },
update: { configJson: { theme: "dark", "layout.mode": "compact" } },
});
expect(dbMock.$transaction).toHaveBeenCalledOnce();
expect(publishAuditEventViaOutboxMock).toHaveBeenCalledWith(
dbMock,
expect.objectContaining({
tenantId: "tenant-1",
actorId: "user-1",
action: "plugin.configuration.updated",
targetId: "plugin-row-1",
metadata: { pluginId: "com.acme.settings", key: "layout.mode" },
})
);
expect(JSON.stringify(publishAuditEventViaOutboxMock.mock.calls[0]?.[1]?.metadata)).not.toContain("compact");
expect(result.settings).toEqual({ theme: "dark", "layout.mode": "compact" });
});
test("merges per-user settings from the write transaction snapshot", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture());
dbMock.pluginUserConfig.findUnique.mockResolvedValue({ configJson: { theme: "stale" } });
const tx = {
pluginUserConfig: {
findUnique: vi.fn().mockResolvedValue({ configJson: { theme: "dark", density: "comfortable" } }),
upsert: vi.fn().mockResolvedValue({}),
},
};
dbMock.$transaction.mockImplementationOnce((callback: (tx: typeof tx) => unknown) => callback(tx));
const { setPluginUserSetting } = await import("./PluginSettingsService");
const result = await setPluginUserSetting({
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
userId: "user-1",
key: "layout.mode",
value: "compact",
actor: { userId: "user-1", email: "user@example.com", ip: "127.0.0.1" },
});
expect(dbMock.pluginUserConfig.findUnique).not.toHaveBeenCalled();
expect(tx.pluginUserConfig.findUnique).toHaveBeenCalledWith({
where: { pluginId_userId: { pluginId: "plugin-row-1", userId: "user-1" } },
select: { configJson: true },
});
expect(tx.pluginUserConfig.upsert).toHaveBeenCalledWith({
where: { pluginId_userId: { pluginId: "plugin-row-1", userId: "user-1" } },
create: { pluginId: "plugin-row-1", userId: "user-1", configJson: { theme: "dark", density: "comfortable", "layout.mode": "compact" } },
update: { configJson: { theme: "dark", density: "comfortable", "layout.mode": "compact" } },
});
expect(publishAuditEventViaOutboxMock).toHaveBeenCalledWith(tx, expect.objectContaining({ action: "plugin.configuration.updated" }));
expect(result.settings).toEqual({ theme: "dark", density: "comfortable", "layout.mode": "compact" });
});
test("rolls back per-user setting writes when audit enqueue fails", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture());
dbMock.pluginUserConfig.findUnique.mockResolvedValue({ configJson: {} });
dbMock.pluginUserConfig.upsert.mockResolvedValue({});
publishAuditEventViaOutboxMock.mockRejectedValueOnce(new Error("outbox unavailable"));
const { setPluginUserSetting } = await import("./PluginSettingsService");
await expect(
setPluginUserSetting({
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
userId: "user-1",
key: "theme",
value: "dark",
actor: { userId: "user-1", email: "user@example.com", ip: "127.0.0.1" },
})
).rejects.toThrow("outbox unavailable");
expect(dbMock.$transaction).toHaveBeenCalledOnce();
expect(dbMock.pluginUserConfig.upsert).toHaveBeenCalledOnce();
expect(publishAuditEventViaOutboxMock).toHaveBeenCalledOnce();
});
test("reports settings iframe violations as redacted tenant-scoped audit events", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture());
const { reportPluginSettingsFrameViolation } = await import("./PluginSettingsService");
await reportPluginSettingsFrameViolation({
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
reason: "wrong_origin",
actor: { userId: "user-1", email: "user@example.com", ip: "203.0.113.7" },
});
expect(getPluginForTenantMock).toHaveBeenCalledWith({ tenantId: "tenant-1", pluginRowId: "plugin-row-1" });
expect(publishAuditEventViaOutboxMock).toHaveBeenCalledWith(dbMock, {
tenantId: "tenant-1",
actorId: "user-1",
actorType: "user",
actorEmail: "user@example.com",
actorIp: "203.0.113.7",
action: "plugin.settings_frame.violation",
targetType: "plugin",
targetId: "plugin-row-1",
sourceModule: "plugin-settings",
outcome: "blocked",
dataSensitivity: "internal",
metadata: { pluginId: "com.acme.settings", pluginRowId: "plugin-row-1", reason: "wrong_origin", reportedByClient: true },
});
expect(JSON.stringify(publishAuditEventViaOutboxMock.mock.calls[0]?.[1])).not.toContain("payload");
});
test("rejects settings iframe violation reports for disabled plugins or missing settings UI", async () => {
const { reportPluginSettingsFrameViolation } = await import("./PluginSettingsService");
const base = {
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
reason: "invalid_payload" as const,
actor: { userId: "user-1", email: "user@example.com", ip: null },
};
getPluginForTenantMock.mockResolvedValueOnce({ ...pluginFixture(), enabled: false });
await expect(reportPluginSettingsFrameViolation(base)).rejects.toMatchObject({ code: "plugin_disabled" });
getPluginForTenantMock.mockResolvedValueOnce(pluginFixture(baseManifest));
await expect(reportPluginSettingsFrameViolation(base)).rejects.toMatchObject({ code: "settings_ui_unavailable" });
expect(publishAuditEventViaOutboxMock).not.toHaveBeenCalled();
});
test("rejects direct per-user setting writes for disabled plugins", async () => {
getPluginForTenantMock.mockResolvedValue({ ...pluginFixture(), enabled: false });
const { setPluginUserSetting } = await import("./PluginSettingsService");
await expect(
setPluginUserSetting({
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
userId: "user-1",
key: "theme",
value: "dark",
actor: { userId: "user-1", email: "user@example.com", ip: "127.0.0.1" },
})
).rejects.toMatchObject({ code: "plugin_disabled" });
expect(dbMock.pluginUserConfig.findUnique).not.toHaveBeenCalled();
expect(dbMock.pluginUserConfig.upsert).not.toHaveBeenCalled();
expect(publishAuditEventViaOutboxMock).not.toHaveBeenCalled();
});
test("rejects direct per-user setting writes when no safe settings UI is declared", async () => {
const { setPluginUserSetting } = await import("./PluginSettingsService");
const base = {
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
userId: "user-1",
key: "theme",
value: "dark",
actor: { userId: "user-1", email: "user@example.com", ip: "127.0.0.1" },
};
getPluginForTenantMock.mockResolvedValueOnce(pluginFixture(baseManifest));
await expect(setPluginUserSetting(base)).rejects.toMatchObject({ code: "settings_ui_unavailable" });
getPluginForTenantMock.mockResolvedValueOnce(pluginFixture({ ...baseManifest, ui: { settings: { entry: "../settings.html" } } }));
await expect(setPluginUserSetting(base)).rejects.toMatchObject({ code: "settings_ui_entry_unsafe" });
expect(dbMock.pluginUserConfig.findUnique).not.toHaveBeenCalled();
expect(dbMock.pluginUserConfig.upsert).not.toHaveBeenCalled();
expect(publishAuditEventViaOutboxMock).not.toHaveBeenCalled();
});
test("rejects unsafe, secret-like, invalid, and oversized setting writes", async () => {
getPluginForTenantMock.mockResolvedValue(pluginFixture());
dbMock.pluginUserConfig.findUnique.mockResolvedValue({ configJson: {} });
const { setPluginUserSetting } = await import("./PluginSettingsService");
const base = {
tenantId: "tenant-1",
pluginRowId: "plugin-row-1",
userId: "user-1",
actor: { userId: "user-1", email: null, ip: null },
};
await expect(setPluginUserSetting({ ...base, key: "../theme", value: "dark" })).rejects.toMatchObject({ code: "settings_key_invalid" });
await expect(setPluginUserSetting({ ...base, key: "k".repeat(129), value: "dark" })).rejects.toMatchObject({ code: "settings_key_invalid" });
await expect(setPluginUserSetting({ ...base, key: "__proto__", value: "polluted" })).rejects.toMatchObject({ code: "settings_key_invalid" });
await expect(setPluginUserSetting({ ...base, key: "constructor", value: "unsafe" })).rejects.toMatchObject({ code: "settings_key_invalid" });
await expect(setPluginUserSetting({ ...base, key: "prototype", value: "unsafe" })).rejects.toMatchObject({ code: "settings_key_invalid" });
await expect(setPluginUserSetting({ ...base, key: "apiKey", value: "abc" })).rejects.toMatchObject({ code: "settings_key_secret_like" });
await expect(setPluginUserSetting({ ...base, key: "theme", value: Number.NaN as never })).rejects.toMatchObject({ code: "settings_value_invalid" });
await expect(
setPluginUserSetting({ ...base, key: "deep", value: { a: { b: { c: { d: { e: { f: { g: { h: { i: true } } } } } } } } } })
).rejects.toMatchObject({
code: "settings_value_invalid",
});
await expect(setPluginUserSetting({ ...base, key: "array", value: Array.from({ length: 513 }, (_, index) => index) })).rejects.toMatchObject({
code: "settings_value_invalid",
});
await expect(
setPluginUserSetting({ ...base, key: "object", value: Object.fromEntries(Array.from({ length: 257 }, (_, index) => [`k${index}`, index])) })
).rejects.toMatchObject({
code: "settings_value_invalid",
});
await expect(
setPluginUserSetting({ ...base, key: "nestedReserved", value: JSON.parse('{"theme":"dark","__proto__":{"polluted":true}}') })
).rejects.toMatchObject({
code: "settings_value_invalid",
});
await expect(setPluginUserSetting({ ...base, key: "appearance", value: { theme: { accessToken: "must-not-persist" } } })).rejects.toMatchObject({
code: "settings_value_invalid",
});
await expect(setPluginUserSetting({ ...base, key: "blob", value: "x".repeat(70 * 1024) })).rejects.toMatchObject({
code: "settings_value_too_large",
});
expect(dbMock.pluginUserConfig.upsert).not.toHaveBeenCalled();
});
});