import { describe, expect, it } from "@rstest/core";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import {
createMarkdownLinkComponent,
isSafeHref,
MessageArtifactPathsContext,
} from "@/components/workspace/messages/markdown-link";
describe("isSafeHref", () => {
it("allows web URLs and same-origin paths", () => {
expect(isSafeHref("https://example.com/path")).toBe(true);
expect(isSafeHref("http://example.com/path")).toBe(true);
expect(isSafeHref("/workspace/chats/1")).toBe(true);
expect(isSafeHref("#section")).toBe(true);
});
it("allows scheme-less relative references", () => {
expect(isSafeHref("report.md")).toBe(true);
expect(isSafeHref("./report.md")).toBe(true);
expect(isSafeHref("../assets/chart.png")).toBe(true);
});
it("allows non-executing contact schemes", () => {
expect(isSafeHref("mailto:someone@example.com")).toBe(true);
expect(isSafeHref("tel:+15551234567")).toBe(true);
});
it("rejects executable, local, and protocol-relative URLs", () => {
expect(isSafeHref("javascript:alert(1)")).toBe(false);
expect(isSafeHref("java\tscript:alert(1)")).toBe(false);
expect(isSafeHref("data:text/html,")).toBe(false);
expect(isSafeHref("file:///etc/passwd")).toBe(false);
expect(isSafeHref("//example.com/path")).toBe(false);
expect(isSafeHref("\\\\evil.com")).toBe(false);
expect(isSafeHref(undefined)).toBe(false);
});
});
// Render-level coverage: these tests exercise the component itself, so
// removing (or inverting) the isSafeHref guard inside MarkdownLink fails
// them even though isSafeHref stays untouched.
describe("MarkdownLink rendering", () => {
const MarkdownLink = createMarkdownLinkComponent();
it("renders an unsafe href as a disabled span, never an anchor", () => {
const html = renderToStaticMarkup(
createElement(MarkdownLink, { href: "javascript:alert(1)" }, "click me"),
);
expect(html).not.toContain(" {
const html = renderToStaticMarkup(
createElement(
MarkdownLink,
{ href: "javascript:alert(1)" },
"citation:evil",
),
);
expect(html).not.toContain(" {
const html = renderToStaticMarkup(
createElement(
MarkdownLink,
{ href: "https://example.com/report" },
"report",
),
);
expect(html).toContain(" {
const html = renderToStaticMarkup(
createElement(
MarkdownLink,
{ href: "https://example.com/report" },
createElement("span", { key: "prefix" }, "citation:"),
createElement("span", { key: "title" }, "Test Source"),
),
);
expect(html).not.toContain("citation:");
expect(html).toContain("Test Source");
});
it("renders a scheme-less relative href as a navigable anchor", () => {
const tests = ["report.md", "./report.md", "../assets/chart.png"];
for (const href of tests) {
const html = renderToStaticMarkup(
createElement(MarkdownLink, { href }, "doc"),
);
expect(html).toContain("), which then loads the file name as a thread.
describe("MarkdownLink relative artifact links", () => {
const artifacts = [
"/mnt/user-data/outputs/final report.md",
"/mnt/user-data/outputs/first/shared.md",
"/mnt/user-data/outputs/second/shared.md",
"/mnt/user-data/outputs/notes.md",
];
const MessageLink = createMarkdownLinkComponent("thread-1");
const reportURL =
"/api/threads/thread-1/artifacts/mnt/user-data/outputs/final%20report.md";
const notesURL =
"/api/threads/thread-1/artifacts/mnt/user-data/outputs/notes.md";
function render(href: string, paths: readonly string[] | null = artifacts) {
const link = createElement(MessageLink, { href }, "doc");
return renderToStaticMarkup(
paths === null
? link
: createElement(
MessageArtifactPathsContext.Provider,
{ value: paths },
link,
),
);
}
it("rewrites a uniquely matching relative link to the artifact URL", () => {
// Markdown destinations reach the renderer percent-encoded.
const html = render("final%20report.md");
expect(html).toContain(`href="${reportURL}"`);
expect(html).toContain('target="_blank"');
expect(html).toContain('rel="noopener noreferrer"');
expect(html).toContain("text-primary");
expect(render("./final report.md")).toContain(`href="${reportURL}"`);
expect(render("././outputs/notes.md")).toContain(`href="${notesURL}"`);
});
it("keeps ?query and #fragment suffixes on the rewritten URL", () => {
expect(render("notes.md?v=2#part-1")).toContain(
`href="${notesURL}?v=2#part-1"`,
);
});
it("renders ambiguous or unmatched links as text, never a relative href", () => {
for (const href of ["shared.md", "missing.md", "./missing.md"]) {
const html = render(href);
expect(html).not.toContain(" {
for (const href of ["../notes.md", "..%2fnotes.md"]) {
const html = render(href);
expect(html).not.toContain(" {
const html = render("/mnt/user-data/outputs/chart.png?v=2#detail");
expect(html).toContain(
'href="/api/threads/thread-1/artifacts/mnt/user-data/outputs/chart.png?v=2#detail"',
);
expect(html).toContain('target="_blank"');
expect(html).toContain("text-primary");
});
it("leaves relative links alone outside the message list", () => {
expect(render("missing.md", null)).toContain('href="missing.md"');
const GenericLink = createMarkdownLinkComponent();
expect(
renderToStaticMarkup(
createElement(
MessageArtifactPathsContext.Provider,
{ value: artifacts },
createElement(GenericLink, { href: "notes.md" }, "doc"),
),
),
).toContain('href="notes.md"');
});
});