import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { PLUGIN_SETTINGS_FRAME_ALLOW, PLUGIN_SETTINGS_FRAME_CHANNEL_META_NAME, PLUGIN_SETTINGS_FRAME_CSP, PLUGIN_SETTINGS_FRAME_DEFAULT_ERROR_MESSAGES, PLUGIN_SETTINGS_FRAME_LOAD_COALESCE_MS, PLUGIN_SETTINGS_FRAME_MAX_ARRAY_LENGTH, PLUGIN_SETTINGS_FRAME_MAX_IN_FLIGHT_REQUESTS, PLUGIN_SETTINGS_FRAME_MAX_KEY_LENGTH, PLUGIN_SETTINGS_FRAME_MAX_OBJECT_KEYS, PLUGIN_SETTINGS_FRAME_MAX_PLUGIN_ID_LENGTH, PLUGIN_SETTINGS_FRAME_MAX_REQUEST_ID_LENGTH, PLUGIN_SETTINGS_FRAME_MAX_VALUE_BYTES, PLUGIN_SETTINGS_FRAME_PROTOCOL_VERSION, PLUGIN_SETTINGS_FRAME_SANDBOX, PluginSettingsFrame, addPluginSettingsFrameScriptNonce, buildPluginSettingsFrameCsp, buildPluginSettingsFrameSrcDoc, parsePluginSettingsFrameMessage, } from "./PluginSettingsFrame"; import { readRepoFile } from "~/utils/testing/repoPaths"; const TEST_CHANNEL_ID = "test-channel"; const TEST_SCRIPT_NONCE = "test-script-nonce"; function dispatchFrameMessage(iframe: HTMLIFrameElement, data: unknown, origin = "null") { const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); act(() => { window.dispatchEvent(new MessageEvent("message", { data, origin, source })); }); return source; } function getFrameChannelId(iframe: Element) { const srcDoc = iframe.getAttribute("srcdoc") ?? ""; const match = srcDoc.match(new RegExp(`name="${PLUGIN_SETTINGS_FRAME_CHANNEL_META_NAME}" content="([^"]+)"`, "u")); if (!match?.[1]) throw new Error("plugin settings frame channel missing"); return match[1]; } describe("PluginSettingsFrame", () => { it("keeps default frame error copy aligned with the English locale", () => { const en = JSON.parse(readRepoFile("apps/web/public/locales/en/translations.json")) as { settings: { plugins: { settings: { frameErrors: Record } } }; }; expect(PLUGIN_SETTINGS_FRAME_DEFAULT_ERROR_MESSAGES).toEqual(en.settings.plugins.settings.frameErrors); }); it("renders a sandboxed opaque-origin iframe with an embedded CSP", () => { render(); const iframe = screen.getByTitle("Acme settings"); expect(iframe).toHaveAttribute("data-ui-id", "ui.plugins.settings.frame"); expect(iframe).toHaveAttribute("data-plugin-id", "com.acme.settings"); expect(iframe).toHaveAttribute("referrerpolicy", "no-referrer"); expect(iframe).toHaveAttribute("allow", PLUGIN_SETTINGS_FRAME_ALLOW); expect(iframe).toHaveAttribute("sandbox", PLUGIN_SETTINGS_FRAME_SANDBOX); expect(iframe).not.toHaveAttribute("sandbox", expect.stringContaining("allow-forms")); expect(iframe).not.toHaveAttribute("sandbox", expect.stringContaining("allow-same-origin")); expect(iframe).not.toHaveAttribute("src"); expect(iframe).toHaveAttribute("srcdoc", expect.stringContaining('http-equiv="Content-Security-Policy"')); expect(iframe).toHaveAttribute("srcdoc", expect.stringContaining("plugin-settings-protocol")); expect(iframe).toHaveAttribute("srcdoc", expect.stringContaining(`name="${PLUGIN_SETTINGS_FRAME_CHANNEL_META_NAME}"`)); }); it("allows routes to provide a stable UI selector", () => { render(); expect(screen.getByTitle("Acme settings")).toHaveAttribute("data-ui-id", "ui.custom.plugin.frame"); }); it("keeps one message listener while reading fresh settings after rerenders", async () => { const addEventListener = vi.spyOn(window, "addEventListener"); const removeEventListener = vi.spyOn(window, "removeEventListener"); const { rerender, unmount } = render( ); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); rerender(); expect(addEventListener.mock.calls.filter(([type]) => type === "message")).toHaveLength(1); expect(removeEventListener.mock.calls.filter(([type]) => type === "message")).toHaveLength(0); dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-theme", key: "theme", }); await waitFor(() => expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-theme", ok: true, key: "theme", value: "dark", }, "*" ) ); unmount(); expect(removeEventListener.mock.calls.filter(([type]) => type === "message")).toHaveLength(1); }); it("builds srcDoc with plugin identity and escaped CSP attributes", () => { const srcDoc = buildPluginSettingsFrameSrcDoc({ pluginId: 'com.acme."quoted"', trustedHtml: "
", channelId: 'channel-"quoted"', scriptNonce: TEST_SCRIPT_NONCE, }); expect(srcDoc).toContain(`content="${buildPluginSettingsFrameCsp(TEST_SCRIPT_NONCE).replace(/"/gu, """)}"`); expect(srcDoc).toContain('content="com.acme."quoted""'); expect(srcDoc).toContain(`name="${PLUGIN_SETTINGS_FRAME_CHANNEL_META_NAME}" content="channel-"quoted""`); expect(srcDoc).toContain(``); }); it("nonces bundled inline scripts without authorizing external scripts", () => { const trustedHtml = [ "", '', '', '', ].join(""); const srcDoc = buildPluginSettingsFrameSrcDoc({ pluginId: "com.acme.settings", trustedHtml, channelId: TEST_CHANNEL_ID, scriptNonce: TEST_SCRIPT_NONCE, }); expect(srcDoc).toContain(`script-src 'nonce-${TEST_SCRIPT_NONCE}'`); expect(srcDoc).toContain("script-src-attr 'none'"); expect(srcDoc).not.toContain("script-src 'unsafe-inline'"); expect(srcDoc).toContain(``); expect(srcDoc).toContain(``); expect(srcDoc).toContain(''); expect(srcDoc).toContain('onclick="window.clicked = true"'); }); it("exposes script nonce rewriting as a focused helper", () => { expect(addPluginSettingsFrameScriptNonce("", TEST_SCRIPT_NONCE)).toBe( `` ); expect(addPluginSettingsFrameScriptNonce('', TEST_SCRIPT_NONCE)).toBe( `` ); expect(addPluginSettingsFrameScriptNonce('', TEST_SCRIPT_NONCE)).toBe(''); }); it("does not allow remote image beacons in the iframe CSP", () => { expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("img-src data:"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("script-src 'nonce-"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("script-src-attr 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).not.toContain("script-src 'unsafe-inline'"); expect(PLUGIN_SETTINGS_FRAME_CSP).not.toContain("img-src data: https:"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("connect-src 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("frame-src 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("child-src 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("worker-src 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("media-src 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("object-src 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).toContain("navigate-to 'none'"); expect(PLUGIN_SETTINGS_FRAME_CSP).not.toContain("frame-ancestors"); }); it("rejects script nonces that could mutate the iframe CSP", () => { expect(buildPluginSettingsFrameCsp("safe-nonce_123+/=")).toContain("script-src 'nonce-safe-nonce_123+/='"); expect(() => buildPluginSettingsFrameCsp("nonce'; script-src 'unsafe-inline'")).toThrow("unsafe CSP characters"); expect(() => buildPluginSettingsFrameCsp("nonce with spaces")).toThrow("unsafe CSP characters"); }); it("denies sensitive browser capabilities through the iframe allow policy", () => { const deniedCapabilities = [ "accelerometer", "ambient-light-sensor", "attribution-reporting", "autoplay", "bluetooth", "browsing-topics", "camera", "clipboard-read", "clipboard-write", "display-capture", "encrypted-media", "fullscreen", "gamepad", "geolocation", "gyroscope", "hid", "identity-credentials-get", "idle-detection", "local-fonts", "magnetometer", "microphone", "midi", "otp-credentials", "payment", "picture-in-picture", "publickey-credentials-get", "screen-wake-lock", "serial", "speaker-selection", "storage-access", "sync-xhr", "usb", "web-share", "xr-spatial-tracking", ]; for (const capability of deniedCapabilities) { expect(PLUGIN_SETTINGS_FRAME_ALLOW).toContain(`${capability} 'none'`); } expect(PLUGIN_SETTINGS_FRAME_ALLOW).not.toContain("'self'"); expect(PLUGIN_SETTINGS_FRAME_ALLOW).not.toContain("*"); }); it("remounts the iframe if plugin content attempts to navigate itself", async () => { vi.useFakeTimers(); try { const onInvalidMessage = vi.fn(); render( Leave'} onInvalidMessage={onInvalidMessage} /> ); const initialFrame = screen.getByTitle("Acme settings"); // Initial (legitimate) load of the srcdoc document. fireEvent.load(initialFrame); // A genuine self-navigation happens later than the brief duplicate-load // coalescing window, after user/script interaction. act(() => { vi.advanceTimersByTime(PLUGIN_SETTINGS_FRAME_LOAD_COALESCE_MS + 1); }); fireEvent.load(initialFrame); await vi.waitFor(() => expect(onInvalidMessage).toHaveBeenCalledWith("unexpected_navigation")); await vi.waitFor(() => expect(screen.getByTitle("Acme settings")).not.toBe(initialFrame)); const remountedFrame = screen.getByTitle("Acme settings"); expect(remountedFrame).toHaveAttribute("srcdoc", expect.stringContaining("navigate-to 'none'")); expect(remountedFrame).toHaveAttribute("sandbox", PLUGIN_SETTINGS_FRAME_SANDBOX); } finally { vi.useRealTimers(); } }); it("ignores a spurious duplicate load of the same srcdoc without rotating the channel", () => { const onInvalidMessage = vi.fn(); render( ); const initialFrame = screen.getByTitle("Acme settings"); const channelBefore = getFrameChannelId(initialFrame); // The browser re-fires `load` for the same document back-to-back; this must // not be treated as a navigation, otherwise the live channelId is silently // rotated out from under an in-flight settings write. fireEvent.load(initialFrame); fireEvent.load(initialFrame); expect(onInvalidMessage).not.toHaveBeenCalled(); const frameAfter = screen.getByTitle("Acme settings"); expect(frameAfter).toBe(initialFrame); expect(getFrameChannelId(frameAfter)).toBe(channelBefore); }); it("accepts versioned ready/get/set messages from the iframe only", async () => { const onReady = vi.fn(); const onSetSetting = vi.fn(); render( ); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.ready", version: 1, pluginId: "com.acme.settings", channelId }); expect(onReady).toHaveBeenCalledWith({ type: "plugin.settings.ready", version: 1, pluginId: "com.acme.settings", channelId }); dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-1", key: "theme" }); await waitFor(() => expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-1", ok: true, key: "theme", value: "dark" }, "*" ) ); dispatchFrameMessage(iframe, { type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId, requestId: "set-1", key: "theme", value: "light", }); await waitFor(() => expect(onSetSetting).toHaveBeenCalledWith("theme", "light")); expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "set-1", ok: true, key: "theme", value: "light" }, "*" ); }); it("rejects same-frame non-opaque origins, protocol messages from unrelated windows, and invalid payloads", () => { const onInvalidMessage = vi.fn(); render(); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.ready", version: 1, pluginId: "com.acme.settings", channelId }, "https://evil.example"); act(() => { window.dispatchEvent( new MessageEvent("message", { data: { type: "plugin.settings.ready", version: 1, pluginId: "com.acme.settings", channelId }, origin: "null", source: window, }) ); }); expect(postMessage).not.toHaveBeenCalled(); dispatchFrameMessage(iframe, { type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId, requestId: "bad", key: "theme", value: Number.NaN, }); expect(onInvalidMessage).toHaveBeenNthCalledWith(1, "wrong_origin"); expect(onInvalidMessage).toHaveBeenNthCalledWith(2, "wrong_source"); expect(onInvalidMessage).toHaveBeenNthCalledWith(3, "invalid_payload"); expect(postMessage).toHaveBeenCalledTimes(1); expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "bad", ok: false, error: "invalid_payload", message: "The plugin settings request is invalid.", }, "*" ); }); it("rejects same-frame messages that do not carry the current channel id", () => { const onInvalidMessage = vi.fn(); render(); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId: "stale-channel", requestId: "stale-1", key: "theme", }); expect(onInvalidMessage).toHaveBeenCalledWith("invalid_payload"); expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "stale-1", ok: false, error: "invalid_payload", message: "The plugin settings request is invalid.", }, "*" ); }); it("rejects same-frame messages that do not carry the current plugin id", () => { const onInvalidMessage = vi.fn(); render(); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.set", version: 1, pluginId: "com.evil.settings", channelId, requestId: "wrong-plugin", key: "theme", value: "light", }); expect(onInvalidMessage).toHaveBeenCalledWith("invalid_payload"); expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "wrong-plugin", ok: false, error: "invalid_payload", message: "The plugin settings request is invalid.", }, "*" ); }); it("rejects same-frame messages with unknown schema fields before invoking settings handlers", () => { const onInvalidMessage = vi.fn(); const onSetSetting = vi.fn(); render( ); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId, requestId: "extra-field", key: "theme", value: "light", settings: { theme: "light" }, }); expect(onSetSetting).not.toHaveBeenCalled(); expect(onInvalidMessage).toHaveBeenCalledWith("invalid_payload"); expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "extra-field", ok: false, error: "invalid_payload", message: "The plugin settings request is invalid.", }, "*" ); }); it("fails settings updates when persistence is not wired", async () => { render(); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId, requestId: "set-1", key: "theme", value: "light", }); await waitFor(() => expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "set-1", ok: false, error: "settings_update_failed", message: "Plugin settings could not be saved.", }, "*" ) ); }); it("reports settings get handler failures back to the iframe", async () => { const onGetSetting = vi.fn(async () => { throw new Error("settings store unavailable"); }); render(); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-1", key: "theme" }); await waitFor(() => expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-1", ok: false, error: "settings_unavailable", message: "This setting is unavailable.", }, "*" ) ); }); it("rejects unsafe values returned by custom get handlers before posting to the iframe", async () => { const onGetSetting = vi.fn(async () => ({ headers: { authorization: "Bearer must-not-cross-frame" } })); render(); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-secret", key: "theme" }); await waitFor(() => expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "get-secret", ok: false, error: "settings_unavailable", message: "This setting is unavailable.", }, "*" ) ); }); it("rejects duplicate in-flight request ids before invoking settings handlers again", async () => { const onInvalidMessage = vi.fn(); let resolveGet: (value: string) => void = () => undefined; const onGetSetting = vi.fn( () => new Promise((resolve) => { resolveGet = resolve; }) ); render( ); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId, requestId: "dup-1", key: "theme" }); await waitFor(() => expect(onGetSetting).toHaveBeenCalledTimes(1)); dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId, requestId: "dup-1", key: "theme" }); expect(onGetSetting).toHaveBeenCalledTimes(1); expect(onInvalidMessage).toHaveBeenCalledWith("invalid_payload"); expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "dup-1", ok: false, error: "invalid_payload", message: "The plugin settings request is invalid.", }, "*" ); act(() => resolveGet("dark")); await waitFor(() => expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "dup-1", ok: true, key: "theme", value: "dark" }, "*" ) ); }); it("caps the number of simultaneous settings requests from the frame", async () => { const onInvalidMessage = vi.fn(); const onGetSetting = vi.fn(() => new Promise(() => undefined)); render( ); const iframe = screen.getByTitle("Acme settings") as HTMLIFrameElement; const channelId = getFrameChannelId(iframe); const source = iframe.contentWindow; if (!source) throw new Error("iframe contentWindow missing"); const postMessage = vi.spyOn(source, "postMessage").mockImplementation(() => undefined); for (let index = 0; index < PLUGIN_SETTINGS_FRAME_MAX_IN_FLIGHT_REQUESTS; index += 1) { dispatchFrameMessage(iframe, { type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId, requestId: `req-${index}`, key: "theme", }); } await waitFor(() => expect(onGetSetting).toHaveBeenCalledTimes(PLUGIN_SETTINGS_FRAME_MAX_IN_FLIGHT_REQUESTS)); dispatchFrameMessage(iframe, { type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId, requestId: "overflow", key: "theme", value: "light", }); expect(onGetSetting).toHaveBeenCalledTimes(PLUGIN_SETTINGS_FRAME_MAX_IN_FLIGHT_REQUESTS); expect(onInvalidMessage).toHaveBeenCalledWith("invalid_payload"); expect(postMessage).toHaveBeenCalledWith( { type: "plugin.settings.result", version: 1, pluginId: "com.acme.settings", channelId, requestId: "overflow", ok: false, error: "invalid_payload", message: "The plugin settings request is invalid.", }, "*" ); }); }); describe("parsePluginSettingsFrameMessage", () => { it("requires protocol version and known payload shapes", () => { expect(parsePluginSettingsFrameMessage({ type: "plugin.settings.ready", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID })).toEqual({ type: "plugin.settings.ready", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, }); expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "r1", key: "theme", }) ).toEqual({ type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "r1", key: "theme", }); expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "r2", key: "theme", value: { accent: "blue" }, }) ).toEqual({ type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "r2", key: "theme", value: { accent: "blue" }, }); expect(parsePluginSettingsFrameMessage({ type: "plugin.settings.ready", version: 2, channelId: TEST_CHANNEL_ID })).toBeNull(); expect(parsePluginSettingsFrameMessage({ type: "plugin.settings.ready", version: 1 })).toBeNull(); expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "r3", key: "theme", value: () => "nope", }) ).toBeNull(); }); it("bounds request identifiers and setting keys", () => { expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.ready", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "" }) ).toBeNull(); expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.ready", version: 1, pluginId: "x".repeat(PLUGIN_SETTINGS_FRAME_MAX_PLUGIN_ID_LENGTH + 1), channelId: TEST_CHANNEL_ID, }) ).toBeNull(); expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "x".repeat(PLUGIN_SETTINGS_FRAME_MAX_REQUEST_ID_LENGTH + 1), key: "theme", }) ).toBeNull(); expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.set", version: 1, pluginId: "com.acme.settings", channelId: TEST_CHANNEL_ID, requestId: "r1", key: "k".repeat(PLUGIN_SETTINGS_FRAME_MAX_KEY_LENGTH + 1), value: true, }) ).toBeNull(); }); it("rejects protocol identifiers with unsafe characters", () => { expect( parsePluginSettingsFrameMessage({ type: "plugin.settings.get", version: 1, pluginId: "com.acme.settings