From e0df92e05ea1677aa486c3ea0ad749876fdd448d Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 4 May 2026 13:04:00 +0800 Subject: [PATCH] feat: add sitemap image URL validation gate --- .../__tests__/validate-image-url.test.ts | 69 +++++++++++++++++++ lib/sitemap/validate-image-url.ts | 16 +++++ 2 files changed, 85 insertions(+) create mode 100644 lib/sitemap/__tests__/validate-image-url.test.ts create mode 100644 lib/sitemap/validate-image-url.ts diff --git a/lib/sitemap/__tests__/validate-image-url.test.ts b/lib/sitemap/__tests__/validate-image-url.test.ts new file mode 100644 index 0000000..be2f384 --- /dev/null +++ b/lib/sitemap/__tests__/validate-image-url.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "vitest" +import { validateImageUrl } from "@/lib/sitemap/validate-image-url" + +describe("validateImageUrl", () => { + it("returns null for null input", () => { + expect(validateImageUrl(null)).toBeNull() + }) + + it("returns null for undefined input", () => { + expect(validateImageUrl(undefined)).toBeNull() + }) + + it("returns null for empty string", () => { + expect(validateImageUrl("")).toBeNull() + }) + + it("returns null for whitespace-only string", () => { + expect(validateImageUrl(" ")).toBeNull() + }) + + it("returns null for http:// URL", () => { + expect(validateImageUrl("http://example.com/image.png")).toBeNull() + }) + + it("returns null for relative URL", () => { + expect(validateImageUrl("/images/hero.png")).toBeNull() + }) + + it("returns null for protocol-less URL", () => { + expect(validateImageUrl("example.com/image.png")).toBeNull() + }) + + it("returns null for URL exceeding 2048 characters", () => { + const longUrl = "https://example.com/" + "a".repeat(2040) + expect(longUrl.length).toBeGreaterThan(2048) + expect(validateImageUrl(longUrl)).toBeNull() + }) + + it("returns the same URL for a valid HTTPS URL", () => { + const url = "https://example.com/image.png" + expect(validateImageUrl(url)).toBe(url) + }) + + it("trims whitespace from a valid URL", () => { + const url = "https://example.com/image.png" + expect(validateImageUrl(` ${url} `)).toBe(url) + }) + + it("returns URL when exactly 2048 characters", () => { + const url = "https://example.com/" + "a".repeat(2048 - "https://example.com/".length) + expect(url.length).toBe(2048) + expect(validateImageUrl(url)).toBe(url) + }) + + it("returns null for URL at 2049 characters", () => { + const url = "https://example.com/" + "a".repeat(2049 - "https://example.com/".length) + expect(url.length).toBe(2049) + expect(validateImageUrl(url)).toBeNull() + }) + + it("returns realistic Steam capsule and SteamGridDB URLs unchanged", () => { + const steamCapsule = + "https://cdn.akamai.steamstatic.com/steam/apps/1245620/capsule_616x353.jpg" + const steamGridDb = + "https://www.steamgriddb.com/api/v2/images/grid/12345-abcdef.png" + expect(validateImageUrl(steamCapsule)).toBe(steamCapsule) + expect(validateImageUrl(steamGridDb)).toBe(steamGridDb) + }) +}) \ No newline at end of file diff --git a/lib/sitemap/validate-image-url.ts b/lib/sitemap/validate-image-url.ts new file mode 100644 index 0000000..1d7759d --- /dev/null +++ b/lib/sitemap/validate-image-url.ts @@ -0,0 +1,16 @@ +/** + * Validates an image URL for sitemap use. + * Returns `null` for invalid inputs, otherwise returns the trimmed HTTPS URL. + */ +export function validateImageUrl(url: string | null | undefined): string | null { + if (!url || typeof url !== "string") return null + + const trimmed = url.trim() + if (trimmed.length === 0) return null + + if (!trimmed.startsWith("https://")) return null + + if (trimmed.length > 2048) return null + + return trimmed +} \ No newline at end of file