From aaf9064a75baaefc15fc2df36354d2119e82c941 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 18:46:16 +0100 Subject: [PATCH] test: move chat image safety to direct render --- .../ui/views/chat-image-open.browser.test.ts | 61 ------------------- ui/src/ui/views/chat.test.ts | 49 +++++++++++++++ 2 files changed, 49 insertions(+), 61 deletions(-) delete mode 100644 ui/src/ui/views/chat-image-open.browser.test.ts diff --git a/ui/src/ui/views/chat-image-open.browser.test.ts b/ui/src/ui/views/chat-image-open.browser.test.ts deleted file mode 100644 index 500e1c01d69..00000000000 --- a/ui/src/ui/views/chat-image-open.browser.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { mountApp, registerAppMountHooks } from "../test-helpers/app-mount.ts"; - -registerAppMountHooks(); - -afterEach(() => { - vi.restoreAllMocks(); -}); - -function renderAssistantImage(url: string) { - return { - role: "assistant", - content: [{ type: "image_url", image_url: { url } }], - timestamp: Date.now(), - }; -} - -describe("chat image open safety", () => { - it("opens only safe image URLs in a hardened new tab", async () => { - const app = mountApp("/chat"); - await app.updateComplete; - - const openSpy = vi.spyOn(window, "open").mockReturnValue(null); - - app.chatMessages = [renderAssistantImage("https://example.com/cat.png")]; - await app.updateComplete; - - let image = app.querySelector(".chat-message-image"); - expect(image).not.toBeNull(); - image?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - - expect(openSpy).toHaveBeenCalledTimes(1); - expect(openSpy).toHaveBeenCalledWith( - "https://example.com/cat.png", - "_blank", - "noopener,noreferrer", - ); - - openSpy.mockClear(); - app.chatMessages = [renderAssistantImage("javascript:alert(1)")]; - await app.updateComplete; - - image = app.querySelector(".chat-message-image"); - expect(image).not.toBeNull(); - image?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - - expect(openSpy).not.toHaveBeenCalled(); - - openSpy.mockClear(); - app.chatMessages = [ - renderAssistantImage("data:image/svg+xml,"), - ]; - await app.updateComplete; - - image = app.querySelector(".chat-message-image"); - expect(image).not.toBeNull(); - image?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - - expect(openSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index acedaf72c35..31a0d58d9ed 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -1057,6 +1057,55 @@ describe("chat view", () => { expect(container.textContent).not.toContain("MEDIA:https://example.com/photo.png"); }); + it("opens only safe assistant image URLs in a hardened new tab", () => { + const container = document.createElement("div"); + const openSpy = vi.spyOn(window, "open").mockReturnValue(null); + const renderAssistantImage = (url: string) => + render( + renderChat( + createProps({ + messages: [ + { + role: "assistant", + content: [{ type: "image_url", image_url: { url } }], + timestamp: Date.now(), + }, + ], + }), + ), + container, + ); + + try { + renderAssistantImage("https://example.com/cat.png"); + let image = container.querySelector(".chat-message-image"); + expect(image).not.toBeNull(); + image?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + + expect(openSpy).toHaveBeenCalledTimes(1); + expect(openSpy).toHaveBeenCalledWith( + "https://example.com/cat.png", + "_blank", + "noopener,noreferrer", + ); + + openSpy.mockClear(); + renderAssistantImage("javascript:alert(1)"); + image = container.querySelector(".chat-message-image"); + expect(image).not.toBeNull(); + image?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(openSpy).not.toHaveBeenCalled(); + + renderAssistantImage("data:image/svg+xml,"); + image = container.querySelector(".chat-message-image"); + expect(image).not.toBeNull(); + image?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(openSpy).not.toHaveBeenCalled(); + } finally { + openSpy.mockRestore(); + } + }); + it("renders verified local assistant attachments through the Control UI media route", async () => { resetAssistantAttachmentAvailabilityCacheForTest(); const fetchMock = vi.fn(async (url: string) => {