Files
openclaw/ui/src/ui/open-external-url.test.ts
2026-02-24 22:28:58 +00:00

109 lines
3.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { openExternalUrlSafe, resolveSafeExternalUrl } from "./open-external-url.ts";
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("resolveSafeExternalUrl", () => {
const baseHref = "https://openclaw.ai/chat";
it("allows absolute https URLs", () => {
expect(resolveSafeExternalUrl("https://example.com/a.png?x=1#y", baseHref)).toBe(
"https://example.com/a.png?x=1#y",
);
});
it("allows relative URLs resolved against the current origin", () => {
expect(resolveSafeExternalUrl("/assets/pic.png", baseHref)).toBe(
"https://openclaw.ai/assets/pic.png",
);
});
it("allows blob URLs", () => {
expect(resolveSafeExternalUrl("blob:https://openclaw.ai/abc-123", baseHref)).toBe(
"blob:https://openclaw.ai/abc-123",
);
});
it("allows data image URLs when enabled", () => {
expect(
resolveSafeExternalUrl("data:image/png;base64,iVBORw0KGgo=", baseHref, {
allowDataImage: true,
}),
).toBe("data:image/png;base64,iVBORw0KGgo=");
});
it("rejects non-image data URLs", () => {
expect(
resolveSafeExternalUrl("data:text/html,<script>alert(1)</script>", baseHref, {
allowDataImage: true,
}),
).toBeNull();
});
it("rejects SVG data image URLs", () => {
expect(
resolveSafeExternalUrl(
"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' />",
baseHref,
{
allowDataImage: true,
},
),
).toBeNull();
});
it("rejects base64-encoded SVG data image URLs", () => {
expect(
resolveSafeExternalUrl(
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIC8+",
baseHref,
{
allowDataImage: true,
},
),
).toBeNull();
});
it("rejects data image URLs unless explicitly enabled", () => {
expect(resolveSafeExternalUrl("data:image/png;base64,iVBORw0KGgo=", baseHref)).toBeNull();
});
it("rejects javascript URLs", () => {
expect(resolveSafeExternalUrl("javascript:alert(1)", baseHref)).toBeNull();
});
it("rejects file URLs", () => {
expect(resolveSafeExternalUrl("file:///tmp/x.png", baseHref)).toBeNull();
});
it("rejects empty values", () => {
expect(resolveSafeExternalUrl(" ", baseHref)).toBeNull();
});
});
describe("openExternalUrlSafe", () => {
it("nulls opener when window.open returns a proxy-like object", () => {
const openedLikeProxy = {
opener: { postMessage: () => void 0 },
} as unknown as WindowProxy;
const openMock = vi.fn(() => openedLikeProxy);
vi.stubGlobal("window", {
location: { href: "https://openclaw.ai/chat" },
open: openMock,
} as unknown as Window & typeof globalThis);
const opened = openExternalUrlSafe("https://example.com/safe.png");
expect(openMock).toHaveBeenCalledWith(
"https://example.com/safe.png",
"_blank",
"noopener,noreferrer",
);
expect(opened).toBe(openedLikeProxy);
expect(openedLikeProxy.opener).toBeNull();
});
});