test: move chat image safety to direct render

This commit is contained in:
Peter Steinberger
2026-04-17 18:46:16 +01:00
parent 36b98f78b2
commit aaf9064a75
2 changed files with 49 additions and 61 deletions

View File

@@ -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<HTMLImageElement>(".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<HTMLImageElement>(".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,<svg xmlns='http://www.w3.org/2000/svg' />"),
];
await app.updateComplete;
image = app.querySelector<HTMLImageElement>(".chat-message-image");
expect(image).not.toBeNull();
image?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(openSpy).not.toHaveBeenCalled();
});
});

View File

@@ -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<HTMLImageElement>(".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<HTMLImageElement>(".chat-message-image");
expect(image).not.toBeNull();
image?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(openSpy).not.toHaveBeenCalled();
renderAssistantImage("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' />");
image = container.querySelector<HTMLImageElement>(".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) => {