test(security): cover url validation and form edge cases

Add and update tests for new security utilities and tightened UI behavior to prevent regressions in validation and error handling paths.

Made-with: Cursor
This commit is contained in:
Amruth Pillai
2026-04-25 15:31:19 +02:00
parent 08e9c80037
commit d0af9f4b4f
13 changed files with 713 additions and 5 deletions
+19
View File
@@ -44,6 +44,25 @@ vi.mock("motion/react", () => ({
}));
// Mock lingui
vi.mock("@lingui/core/macro", () => ({
t: (stringsOrDescriptor: TemplateStringsArray | { message: string }, ...values: unknown[]) => {
if (
typeof stringsOrDescriptor === "object" &&
"message" in stringsOrDescriptor &&
typeof stringsOrDescriptor.message === "string"
) {
return stringsOrDescriptor.message;
}
const strings = stringsOrDescriptor as TemplateStringsArray;
let result = strings[0];
for (let i = 0; i < values.length; i++) {
result += String(values[i]) + strings[i + 1];
}
return result;
},
}));
vi.mock("@lingui/react/macro", () => ({
Trans: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
+11 -2
View File
@@ -5,7 +5,16 @@ afterEach(cleanup);
// Mock @lingui/core/macro
vi.mock("@lingui/core/macro", () => ({
t: (strings: TemplateStringsArray, ...values: unknown[]) => {
t: (stringsOrDescriptor: TemplateStringsArray | { message: string }, ...values: unknown[]) => {
if (
typeof stringsOrDescriptor === "object" &&
"message" in stringsOrDescriptor &&
typeof stringsOrDescriptor.message === "string"
) {
return stringsOrDescriptor.message;
}
const strings = stringsOrDescriptor as TemplateStringsArray;
let result = strings[0];
for (let i = 0; i < values.length; i++) {
result += String(values[i]) + strings[i + 1];
@@ -127,7 +136,7 @@ describe("LevelDisplay", () => {
describe("aria attributes", () => {
it("has aria-label showing level out of 5", () => {
render(<LevelDisplay icon="star" type="circle" level={3} />);
const el = screen.getByRole("presentation");
const el = screen.getByRole("img");
expect(el.getAttribute("aria-label")).toContain("3");
expect(el.getAttribute("aria-label")).toContain("5");
});
@@ -0,0 +1,34 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vite-plus/test";
import { PageLink } from "./page-link";
afterEach(cleanup);
describe("PageLink", () => {
it("returns null when url is empty", () => {
const { container } = render(<PageLink url="" />);
expect(container.firstChild).toBeNull();
});
it("renders link with expected attributes and label", () => {
render(<PageLink url="https://example.com" label="Visit site" className="custom-link" />);
const link = screen.getByRole("link", { name: "Visit site" });
expect(link.getAttribute("href")).toBe("https://example.com");
expect(link.getAttribute("target")).toBe("_blank");
expect(link.getAttribute("rel")).toBe("noopener");
expect(link.className).toContain("inline-block");
expect(link.className).toContain("custom-link");
});
it("falls back to url text when label is missing", () => {
render(<PageLink url="https://fallback.dev" label="" />);
const link = screen.getByRole("link", { name: "https://fallback.dev" });
expect(link.getAttribute("href")).toBe("https://fallback.dev");
});
});
+36
View File
@@ -0,0 +1,36 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vite-plus/test";
import { BrandIcon } from "./brand-icon";
afterEach(cleanup);
describe("BrandIcon", () => {
it("renders both dark and light logo images by default", () => {
render(<BrandIcon />);
const images = screen.getAllByRole("img", { name: "Reactive Resume" });
expect(images).toHaveLength(2);
expect(images[0].getAttribute("src")).toBe("/logo/dark.svg");
expect(images[0].className).toContain("hidden");
expect(images[0].className).toContain("dark:block");
expect(images[1].getAttribute("src")).toBe("/logo/light.svg");
expect(images[1].className).toContain("block");
expect(images[1].className).toContain("dark:hidden");
});
it("uses icon variant and forwards shared img props", () => {
render(<BrandIcon variant="icon" className="brand-class" loading="lazy" data-testid="brand" />);
const images = screen.getAllByTestId("brand");
expect(images).toHaveLength(2);
expect(images[0].getAttribute("src")).toBe("/icon/dark.svg");
expect(images[1].getAttribute("src")).toBe("/icon/light.svg");
expect(images[0].getAttribute("loading")).toBe("lazy");
expect(images[1].getAttribute("loading")).toBe("lazy");
expect(images[0].className).toContain("brand-class");
expect(images[1].className).toContain("brand-class");
});
});
+181
View File
@@ -0,0 +1,181 @@
import type { FieldValues, UseFormReturn } from "react-hook-form";
import { act, cleanup, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { useFormBlocker } from "./use-form-blocker";
type ConfirmOptions = {
description?: string;
confirmText?: string;
cancelText?: string;
};
type ConfirmFn = (title: string, options?: ConfirmOptions) => Promise<boolean>;
const { closeDialogMock, confirmMock } = vi.hoisted(() => ({
closeDialogMock: vi.fn<() => void>(),
confirmMock: vi.fn<ConfirmFn>(),
}));
vi.mock("@lingui/core/macro", () => ({
t: (strings: TemplateStringsArray) => strings[0],
}));
vi.mock("@/dialogs/store", () => ({
useDialogStore: (selector: (state: { closeDialog: () => void }) => unknown) =>
selector({ closeDialog: closeDialogMock }),
}));
vi.mock("@/hooks/use-confirm", () => ({
useConfirm: () => confirmMock,
}));
afterEach(cleanup);
function createFormState({
isDirty = false,
isSubmitting = false,
}: {
isDirty?: boolean;
isSubmitting?: boolean;
}): UseFormReturn<FieldValues> {
return {
formState: { isDirty, isSubmitting },
} as UseFormReturn<FieldValues>;
}
describe("useFormBlocker", () => {
beforeEach(() => {
closeDialogMock.mockReset();
confirmMock.mockReset();
confirmMock.mockResolvedValue(false);
});
describe("default shouldBlock behavior", () => {
it("does not block when form is not dirty", async () => {
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: false, isSubmitting: false })));
await act(async () => {
await result.current.requestClose();
});
expect(closeDialogMock).toHaveBeenCalledTimes(1);
expect(confirmMock).not.toHaveBeenCalled();
});
it("does not block while submitting even if dirty", async () => {
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: true, isSubmitting: true })));
await act(async () => {
await result.current.requestClose();
});
expect(closeDialogMock).toHaveBeenCalledTimes(1);
expect(confirmMock).not.toHaveBeenCalled();
});
it("blocks when form is dirty and not submitting", async () => {
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: true, isSubmitting: false })));
await act(async () => {
await result.current.requestClose();
});
expect(confirmMock).toHaveBeenCalledTimes(1);
expect(closeDialogMock).not.toHaveBeenCalled();
});
});
describe("custom shouldBlock override", () => {
it("uses custom shouldBlock instead of default form-state logic", async () => {
const shouldBlock = vi.fn<() => boolean>(() => true);
const { result } = renderHook(() =>
useFormBlocker(createFormState({ isDirty: false, isSubmitting: false }), { shouldBlock }),
);
await act(async () => {
await result.current.requestClose();
});
expect(shouldBlock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(1);
expect(closeDialogMock).not.toHaveBeenCalled();
});
});
describe("requestClose", () => {
it("closes immediately when not blocked", async () => {
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: false, isSubmitting: false })));
await act(async () => {
await result.current.requestClose();
});
expect(closeDialogMock).toHaveBeenCalledTimes(1);
expect(confirmMock).not.toHaveBeenCalled();
});
it("keeps dialog open when blocked and confirmation is declined", async () => {
confirmMock.mockResolvedValue(false);
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: true, isSubmitting: false })));
await act(async () => {
await result.current.requestClose();
});
expect(confirmMock).toHaveBeenCalledWith("Are you sure you want to close this dialog?", {
description: "You have unsaved changes that will be lost.",
confirmText: "Leave",
cancelText: "Stay",
});
expect(closeDialogMock).not.toHaveBeenCalled();
});
it("closes dialog when blocked and confirmation is accepted", async () => {
confirmMock.mockResolvedValue(true);
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: true, isSubmitting: false })));
await act(async () => {
await result.current.requestClose();
});
expect(confirmMock).toHaveBeenCalledTimes(1);
expect(closeDialogMock).toHaveBeenCalledTimes(1);
});
});
describe("blockEvents", () => {
it.each(["onEscapeKeyDown", "onPointerDownOutside", "onInteractOutside"] as const)(
"does not prevent default for %s when not blocking",
async (handlerName) => {
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: false, isSubmitting: false })));
const preventDefault = vi.fn<() => void>();
await act(async () => {
result.current.blockEvents[handlerName]({ preventDefault } as unknown as Event & KeyboardEvent);
});
expect(preventDefault).not.toHaveBeenCalled();
expect(confirmMock).not.toHaveBeenCalled();
expect(closeDialogMock).not.toHaveBeenCalled();
},
);
it.each(["onEscapeKeyDown", "onPointerDownOutside", "onInteractOutside"] as const)(
"prevents default for %s and requests close when blocking",
async (handlerName) => {
const { result } = renderHook(() => useFormBlocker(createFormState({ isDirty: true, isSubmitting: false })));
const preventDefault = vi.fn<() => void>();
await act(async () => {
result.current.blockEvents[handlerName]({ preventDefault } as unknown as Event & KeyboardEvent);
});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(1);
expect(closeDialogMock).not.toHaveBeenCalled();
},
);
});
});
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from "vite-plus/test";
const createEnvMock = vi.fn<(config: unknown) => unknown>((config: unknown) => config);
vi.mock("@t3-oss/env-core", () => ({
createEnv: createEnvMock,
}));
describe("env configuration", () => {
it("builds createEnv config with expected server schemas", async () => {
await import("./env");
expect(createEnvMock).toHaveBeenCalledOnce();
const config = createEnvMock.mock.calls[0][0] as {
clientPrefix: string;
runtimeEnv: NodeJS.ProcessEnv;
emptyStringAsUndefined: boolean;
server: Record<string, unknown>;
};
expect(config.clientPrefix).toBe("VITE_");
expect(config.runtimeEnv).toBe(process.env);
expect(config.emptyStringAsUndefined).toBe(true);
expect(config.server.APP_URL).toBeDefined();
expect(config.server.DATABASE_URL).toBeDefined();
expect(config.server.AUTH_SECRET).toBeDefined();
expect(config.server.OAUTH_SCOPES).toBeDefined();
expect(config.server.S3_FORCE_PATH_STYLE).toBeDefined();
expect(config.server.FLAG_DISABLE_SIGNUPS).toBeDefined();
const parsedScopes = (config.server.OAUTH_SCOPES as { parse: (value: string) => string[] }).parse(
"openid profile email",
);
expect(parsedScopes).toEqual(["openid", "profile", "email"]);
});
});
+64
View File
@@ -0,0 +1,64 @@
import { ORPCError } from "@orpc/client";
import { describe, expect, it } from "vite-plus/test";
import { getOrpcErrorMessage, getReadableErrorMessage, getResumeErrorMessage } from "./error-message";
describe("getReadableErrorMessage", () => {
it("returns error.message when error is an Error", () => {
const result = getReadableErrorMessage(new Error("Request failed"), "Fallback");
expect(result).toBe("Request failed");
});
it("returns string errors directly", () => {
const result = getReadableErrorMessage("String error", "Fallback");
expect(result).toBe("String error");
});
it("returns fallback for unknown values", () => {
const result = getReadableErrorMessage({ reason: "bad" }, "Fallback");
expect(result).toBe("Fallback");
});
});
describe("getOrpcErrorMessage", () => {
it("returns mapped message for matching oRPC code", () => {
const result = getOrpcErrorMessage(new ORPCError("BAD_REQUEST"), {
fallback: "Fallback",
byCode: { BAD_REQUEST: "Mapped bad request" },
});
expect(result).toBe("Mapped bad request");
});
it("returns server message when allowed and code is not mapped", () => {
const result = getOrpcErrorMessage(new ORPCError("INTERNAL_SERVER_ERROR", { message: "Server detail" }), {
fallback: "Fallback",
allowServerMessage: true,
});
expect(result).toBe("Server detail");
});
it("returns fallback when server message is not allowed and code is not mapped", () => {
const result = getOrpcErrorMessage(new ORPCError("INTERNAL_SERVER_ERROR", { message: "Server detail" }), {
fallback: "Fallback",
allowServerMessage: false,
});
expect(result).toBe("Fallback");
});
it("uses readable message handling for non-oRPC errors", () => {
const result = getOrpcErrorMessage("Custom string error", { fallback: "Fallback" });
expect(result).toBe("Custom string error");
});
});
describe("getResumeErrorMessage", () => {
it("maps known resume error codes", () => {
const result = getResumeErrorMessage(new ORPCError("RESUME_SLUG_ALREADY_EXISTS"));
expect(result).toBe("A resume with this slug already exists.");
});
it("falls back for unknown resume error codes", () => {
const result = getResumeErrorMessage(new ORPCError("FORBIDDEN"));
expect(result).toBe("Something went wrong. Please try again.");
});
});
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vite-plus/test";
import { filterFieldValues } from "./field";
describe("filterFieldValues", () => {
it("returns only fields with non-empty trimmed values", () => {
const result = filterFieldValues(
{ name: " Amruth ", title: " ", email: null, company: "Reactive Resume" },
{ key: "name", label: "Name" },
{ key: "title", label: "Title" },
{ key: "email", label: "Email" },
{ key: "company", label: "Company" },
);
expect(result.size).toBe(2);
expect(result.has("name")).toBe(true);
expect(result.has("company")).toBe(true);
expect(result.has("title")).toBe(false);
expect(result.has("email")).toBe(false);
});
});
+58 -2
View File
@@ -1,6 +1,15 @@
import { describe, expect, it } from "vite-plus/test";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { generateFilename } from "./file";
import { downloadFromUrl, downloadWithAnchor, generateFilename } from "./file";
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
describe("generateFilename", () => {
it("should slugify the prefix and append the extension", () => {
@@ -21,3 +30,50 @@ describe("generateFilename", () => {
expect(generateFilename("", "pdf")).toBe(".pdf");
});
});
describe("downloadWithAnchor", () => {
it("creates a link, clicks it, and revokes the object URL", () => {
const blob = new Blob(["test-content"], { type: "text/plain" });
const appendChildSpy = vi.spyOn(document.body, "appendChild");
const removeChildSpy = vi.spyOn(document.body, "removeChild");
const createObjectURL = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob://test");
const revokeObjectURL = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => undefined);
downloadWithAnchor(blob, "resume.pdf");
expect(createObjectURL).toHaveBeenCalledWith(blob);
expect(appendChildSpy).toHaveBeenCalledOnce();
expect(clickSpy).toHaveBeenCalledOnce();
expect(removeChildSpy).toHaveBeenCalledOnce();
vi.advanceTimersByTime(4999);
expect(revokeObjectURL).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(revokeObjectURL).toHaveBeenCalledWith("blob://test");
});
});
describe("downloadFromUrl", () => {
it("downloads from URL and triggers file download flow", async () => {
const blob = new Blob(["payload"], { type: "text/plain" });
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue({
blob: vi.fn<() => Promise<Blob>>().mockResolvedValue(blob),
} as unknown as Response);
const createObjectURL = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob://from-url");
const revokeObjectURL = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => undefined);
vi.stubGlobal("fetch", fetchMock);
await downloadFromUrl("https://example.com/resume", "resume.txt");
expect(fetchMock).toHaveBeenCalledWith("https://example.com/resume");
expect(createObjectURL).toHaveBeenCalledWith(blob);
expect(clickSpy).toHaveBeenCalledOnce();
vi.advanceTimersByTime(5000);
expect(revokeObjectURL).toHaveBeenCalledWith("blob://from-url");
});
});
+69
View File
@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
import type { ResumeData } from "@/schema/resume/data";
import { defaultResumeData } from "@/schema/resume/data";
import { buildDocx } from "./index";
type BuiltDocument = { kind: "test-doc" };
type BuildDocument = (data: ResumeData) => BuiltDocument;
type ToBlob = (doc: BuiltDocument) => Promise<Blob>;
const buildDocumentMock = vi.fn<BuildDocument>();
const toBlobMock = vi.fn<ToBlob>();
vi.mock("./builder", () => ({
buildDocument: buildDocumentMock,
}));
vi.mock("docx", () => ({
Packer: {
toBlob: toBlobMock,
},
}));
describe("buildDocx", () => {
const data: ResumeData = defaultResumeData;
const builtDocument: BuiltDocument = { kind: "test-doc" };
beforeEach(() => {
buildDocumentMock.mockReset();
toBlobMock.mockReset();
});
it("builds and packs a docx blob successfully", async () => {
const blob = new Blob(["docx-content"], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
buildDocumentMock.mockReturnValueOnce(builtDocument);
toBlobMock.mockResolvedValueOnce(blob);
await expect(buildDocx(data)).resolves.toBe(blob);
expect(buildDocumentMock).toHaveBeenCalledOnce();
expect(buildDocumentMock).toHaveBeenCalledWith(data);
expect(toBlobMock).toHaveBeenCalledOnce();
expect(toBlobMock).toHaveBeenCalledWith(builtDocument);
});
it("rejects when the builder throws", async () => {
const error = new Error("Failed to build document");
buildDocumentMock.mockImplementationOnce(() => {
throw error;
});
await expect(buildDocx(data)).rejects.toThrow("Failed to build document");
expect(toBlobMock).not.toHaveBeenCalled();
});
it("rejects when docx packing fails", async () => {
buildDocumentMock.mockReturnValueOnce(builtDocument);
toBlobMock.mockRejectedValueOnce(new Error("Packer failed"));
await expect(buildDocx(data)).rejects.toThrow("Packer failed");
expect(buildDocumentMock).toHaveBeenCalledOnce();
expect(toBlobMock).toHaveBeenCalledOnce();
});
});
+66
View File
@@ -65,6 +65,72 @@ describe("sanitizeCss", () => {
it("should strip -moz-binding", () => {
expect(sanitizeCss("-moz-binding: url(evil.xml)")).not.toContain("-moz-binding:");
});
it("should strip @import rules", () => {
const css = "@import url('https://evil.example/css'); .safe { color: red; }";
const result = sanitizeCss(css);
expect(result).not.toContain("@import");
expect(result).toContain(".safe");
});
it("should strip network url() calls and keep regular declarations", () => {
const css = ".card { background: url('https://evil.example/bg.png'); color: #111; }";
const result = sanitizeCss(css);
expect(result).not.toContain("url(");
expect(result).toMatch(/color\s*:\s*#111/);
});
it("should strip @font-face blocks", () => {
const css =
"@font-face { font-family: test; src: url('https://evil.example/font.woff2'); } .x { font-family: test; }";
const result = sanitizeCss(css);
expect(result).not.toContain("@font-face");
expect(result).toContain(".x");
});
it("should strip css comments before scanning dangerous values", () => {
const css = ".x { background: java/**/script:alert(1); color: blue; }";
const result = sanitizeCss(css);
expect(result).not.toContain("javascript:");
expect(result).toContain("color");
});
it("should decode escaped javascript protocol and strip it", () => {
const css = ".x { background: \\6a\\61\\76\\61\\73\\63\\72\\69\\70\\74:alert(1); color: green; }";
const result = sanitizeCss(css);
expect(result).not.toContain("javascript:");
expect(result).toContain("color");
});
it("should decode escaped expression() and strip it", () => {
const css = ".x { width: \\65\\78\\70\\72\\65\\73\\73\\69\\6f\\6e(alert(1)); height: 10px; }";
const result = sanitizeCss(css);
expect(result).not.toContain("expression(");
expect(result).toContain("height");
});
it("should decode non-hex escapes without dropping characters", () => {
const css = ".x { content: \\(ok\\); color: teal; }";
const result = sanitizeCss(css);
expect(result).toContain("content: (ok)");
expect(result).toContain("color: teal");
});
it("should strip data url payloads in url() functions", () => {
const css = ".x { background-image: url(data:text/html;base64,PHNjcmlwdD4=); color: black; }";
const result = sanitizeCss(css);
expect(result).not.toContain("url(");
expect(result).toContain("color");
});
it("should strip image-set and cross-fade function network payloads", () => {
const css =
".x { background-image: image-set(url('https://evil.example/a.png') 1x); mask-image: cross-fade(url('https://evil.example/a.png'), url('https://evil.example/b.png')); color: purple; }";
const result = sanitizeCss(css);
expect(result).not.toContain("image-set(");
expect(result).not.toContain("cross-fade(");
expect(result).toContain("color");
});
});
describe("isObject", () => {
+14 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vite-plus/test";
import { getInitials, slugify, stripHtml, toUsername } from "./string";
import { generateRandomName, getInitials, slugify, stripHtml, toUsername } from "./string";
describe("slugify", () => {
it("should lowercase and hyphenate spaces", () => {
@@ -70,3 +70,16 @@ describe("stripHtml", () => {
expect(stripHtml("<div><ul><li>item</li></ul></div>")).toBe("item");
});
});
describe("generateRandomName", () => {
it("should generate a 3-word capitalized name", () => {
const result = generateRandomName();
const parts = result.split(" ");
expect(parts).toHaveLength(3);
for (const part of parts) {
expect(part.length).toBeGreaterThan(0);
expect(part[0]).toBe(part[0].toUpperCase());
}
});
});
+103
View File
@@ -0,0 +1,103 @@
import { describe, expect, it } from "vite-plus/test";
import {
isAllowedExternalUrl,
isPrivateOrLoopbackHost,
parseAllowedHostList,
parseUrl,
sanitizeResumePictureUrl,
} from "./url-security";
describe("isPrivateOrLoopbackHost", () => {
it("returns true for localhost and private hosts", () => {
expect(isPrivateOrLoopbackHost("localhost")).toBe(true);
expect(isPrivateOrLoopbackHost("127.0.0.1")).toBe(true);
expect(isPrivateOrLoopbackHost("10.0.0.7")).toBe(true);
expect(isPrivateOrLoopbackHost("192.168.0.2")).toBe(true);
});
it("returns false for public hosts", () => {
expect(isPrivateOrLoopbackHost("example.com")).toBe(false);
expect(isPrivateOrLoopbackHost("8.8.8.8")).toBe(false);
});
it("returns true for private and link-local ipv6 addresses", () => {
expect(isPrivateOrLoopbackHost("::1")).toBe(true);
expect(isPrivateOrLoopbackHost("[::1]")).toBe(true);
expect(isPrivateOrLoopbackHost("fd00::1234")).toBe(true);
expect(isPrivateOrLoopbackHost("[fe80::1]")).toBe(true);
});
});
describe("isAllowedExternalUrl", () => {
const allowedHosts = new Set(["api.openai.com", "https://gateway.ai.vercel.com"]);
it("allows https URLs on allowed hosts", () => {
expect(isAllowedExternalUrl("https://api.openai.com/v1", allowedHosts)).toBe(true);
expect(isAllowedExternalUrl("https://gateway.ai.vercel.com/v1", allowedHosts)).toBe(true);
});
it("blocks local and non-https URLs", () => {
expect(isAllowedExternalUrl("http://api.openai.com/v1", allowedHosts)).toBe(false);
expect(isAllowedExternalUrl("https://localhost:11434/v1", allowedHosts)).toBe(false);
expect(isAllowedExternalUrl("https://10.0.0.1/v1", allowedHosts)).toBe(false);
});
it("blocks malformed URLs and credentialed URLs", () => {
expect(isAllowedExternalUrl("not-a-url", allowedHosts)).toBe(false);
expect(isAllowedExternalUrl("https://user:pass@api.openai.com/v1", allowedHosts)).toBe(false);
});
it("supports origin-only entries in the allow list", () => {
const originOnlyAllowedHosts = new Set(["https://example.org"]);
expect(isAllowedExternalUrl("https://example.org/v1/hello", originOnlyAllowedHosts)).toBe(true);
expect(isAllowedExternalUrl("https://sub.example.org/v1/hello", originOnlyAllowedHosts)).toBe(false);
});
});
describe("sanitizeResumePictureUrl", () => {
const appUrl = "https://rxresu.me";
it("keeps local uploads paths", () => {
expect(sanitizeResumePictureUrl("/uploads/user/pictures/a.webp", appUrl)).toBe("/uploads/user/pictures/a.webp");
});
it("converts same-origin upload URLs to path-only values", () => {
expect(sanitizeResumePictureUrl("https://rxresu.me/uploads/u/p.jpg", appUrl)).toBe("/uploads/u/p.jpg");
});
it("keeps query and hash for same-origin upload URLs", () => {
expect(sanitizeResumePictureUrl("https://rxresu.me/uploads/u/p.jpg?size=2#thumb", appUrl)).toBe(
"/uploads/u/p.jpg?size=2#thumb",
);
});
it("rejects non-upload and cross-origin URLs", () => {
expect(sanitizeResumePictureUrl("https://example.com/pic.jpg", appUrl)).toBe("");
expect(sanitizeResumePictureUrl("https://rxresu.me/other/pic.jpg", appUrl)).toBe("");
});
it("rejects invalid URLs, invalid app URLs, and credentialed URLs", () => {
expect(sanitizeResumePictureUrl("not-a-url", appUrl)).toBe("");
expect(sanitizeResumePictureUrl("ftp://rxresu.me/uploads/p.jpg", appUrl)).toBe("");
expect(sanitizeResumePictureUrl("https://user:pass@rxresu.me/uploads/p.jpg", appUrl)).toBe("");
expect(sanitizeResumePictureUrl("https://rxresu.me/uploads/p.jpg", "not-a-url")).toBe("");
});
});
describe("parse helpers", () => {
it("parseUrl returns URL object for valid URLs and null for invalid input", () => {
expect(parseUrl("https://example.com/path")?.hostname).toBe("example.com");
expect(parseUrl("invalid url")).toBeNull();
});
it("parseAllowedHostList normalizes host entries and handles empty input", () => {
const set = parseAllowedHostList(" API.OpenAI.com , https://gateway.ai.vercel.com ,,api.openai.com ");
expect(set.has("api.openai.com")).toBe(true);
expect(set.has("https://gateway.ai.vercel.com")).toBe(true);
expect(set.size).toBe(2);
expect(parseAllowedHostList(undefined).size).toBe(0);
expect(parseAllowedHostList("").size).toBe(0);
});
});