- Move web app into apps/web/ - Create packages/shared/ with shared types - Create plugins/decky-vault/ scaffold - Root package.json manages workspaces only
211 lines
6.5 KiB
TypeScript
211 lines
6.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
|
import {
|
|
PRODUCTION_URL,
|
|
getBaseUrl,
|
|
imageEntry,
|
|
toDate,
|
|
querySafe,
|
|
STATIC_PAGES,
|
|
} from "@/lib/sitemap-utils"
|
|
|
|
describe("PRODUCTION_URL", () => {
|
|
it("is the canonical production URL", () => {
|
|
expect(PRODUCTION_URL).toBe("https://deckyvault.xyz")
|
|
})
|
|
})
|
|
|
|
describe("getBaseUrl", () => {
|
|
afterEach(() => {
|
|
delete process.env.NEXT_PUBLIC_SITE_URL
|
|
})
|
|
|
|
it("returns PRODUCTION_URL when no env var is set", () => {
|
|
expect(getBaseUrl()).toBe("https://deckyvault.xyz")
|
|
})
|
|
|
|
it("returns PRODUCTION_URL when env var is localhost", () => {
|
|
process.env.NEXT_PUBLIC_SITE_URL = "http://localhost:3000"
|
|
expect(getBaseUrl()).toBe("https://deckyvault.xyz")
|
|
})
|
|
|
|
it("returns PRODUCTION_URL when env var is 127.0.0.1", () => {
|
|
process.env.NEXT_PUBLIC_SITE_URL = "http://127.0.0.1:8080"
|
|
expect(getBaseUrl()).toBe("https://deckyvault.xyz")
|
|
})
|
|
|
|
it("returns the custom env var when set to a real domain", () => {
|
|
process.env.NEXT_PUBLIC_SITE_URL = "https://staging.deckyvault.xyz"
|
|
expect(getBaseUrl()).toBe("https://staging.deckyvault.xyz")
|
|
})
|
|
|
|
it("strips trailing slash from env var", () => {
|
|
process.env.NEXT_PUBLIC_SITE_URL = "https://staging.deckyvault.xyz/"
|
|
expect(getBaseUrl()).toBe("https://staging.deckyvault.xyz")
|
|
})
|
|
})
|
|
|
|
describe("imageEntry", () => {
|
|
it("returns an images array for valid HTTPS capsule URLs", () => {
|
|
const result = imageEntry("https://cdn.example.com/capsule.jpg")
|
|
expect(result).toEqual({ images: ["https://cdn.example.com/capsule.jpg"] })
|
|
})
|
|
|
|
it("trims whitespace from the URL", () => {
|
|
const result = imageEntry(" https://cdn.example.com/capsule.jpg ")
|
|
expect(result).toEqual({ images: ["https://cdn.example.com/capsule.jpg"] })
|
|
})
|
|
|
|
it("returns empty object for non-HTTPS URLs", () => {
|
|
const result = imageEntry("http://cdn.example.com/capsule.jpg")
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it("returns empty object for non-string values", () => {
|
|
expect(imageEntry(null)).toEqual({})
|
|
expect(imageEntry(undefined)).toEqual({})
|
|
expect(imageEntry(123)).toEqual({})
|
|
expect(imageEntry({})).toEqual({})
|
|
})
|
|
|
|
it("returns empty object for empty strings", () => {
|
|
expect(imageEntry("")).toEqual({})
|
|
expect(imageEntry(" ")).toEqual({})
|
|
})
|
|
|
|
it("returns empty object for URLs exceeding 2048 characters", () => {
|
|
const longUrl = "https://cdn.example.com/" + "a".repeat(2048)
|
|
expect(imageEntry(longUrl)).toEqual({})
|
|
})
|
|
|
|
it("accepts URLs exactly at the 2048 character limit", () => {
|
|
const maxUrl = "https://cdn.example.com/" + "a".repeat(2048 - 26)
|
|
expect(imageEntry(maxUrl)).toEqual({ images: [maxUrl] })
|
|
})
|
|
})
|
|
|
|
describe("toDate", () => {
|
|
it("returns the same Date object if passed a valid Date", () => {
|
|
const d = new Date("2025-01-15T10:00:00Z")
|
|
expect(toDate(d)).toBe(d)
|
|
})
|
|
|
|
it("returns undefined for invalid Date objects", () => {
|
|
const d = new Date("not-a-date")
|
|
expect(toDate(d)).toBeUndefined()
|
|
})
|
|
|
|
it("parses a valid ISO date string", () => {
|
|
const result = toDate("2025-01-15T10:00:00Z")
|
|
expect(result).toBeInstanceOf(Date)
|
|
expect(result!.toISOString()).toBe("2025-01-15T10:00:00.000Z")
|
|
})
|
|
|
|
it("returns undefined for invalid date strings", () => {
|
|
expect(toDate("not-a-date")).toBeUndefined()
|
|
})
|
|
|
|
it("parses a numeric timestamp", () => {
|
|
const result = toDate(1700000000000)
|
|
expect(result).toBeInstanceOf(Date)
|
|
expect(result!.getTime()).toBe(1700000000000)
|
|
})
|
|
|
|
it("returns undefined for null", () => {
|
|
expect(toDate(null)).toBeUndefined()
|
|
})
|
|
|
|
it("returns undefined for undefined", () => {
|
|
expect(toDate(undefined)).toBeUndefined()
|
|
})
|
|
|
|
it("returns undefined for non-date objects", () => {
|
|
expect(toDate({ foo: "bar" })).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("querySafe", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it("returns the query result on success", async () => {
|
|
const query = vi.fn().mockResolvedValue([{ id: 1 }])
|
|
const result = await querySafe("test", query)
|
|
expect(result).toEqual([{ id: 1 }])
|
|
expect(query).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it("returns undefined when the query throws", async () => {
|
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
const query = vi.fn().mockRejectedValue(new Error("DB down"))
|
|
const result = await querySafe("test", query)
|
|
expect(result).toBeUndefined()
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
"[Sitemap] test query failed:",
|
|
expect.any(Error),
|
|
)
|
|
consoleErrorSpy.mockRestore()
|
|
})
|
|
|
|
it("returns undefined when the query times out", async () => {
|
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
const query = vi.fn().mockImplementation(
|
|
() => new Promise((resolve) => setTimeout(() => resolve([{ id: 1 }]), 20_000)),
|
|
)
|
|
const resultPromise = querySafe("test", query, 5_000)
|
|
vi.advanceTimersByTime(5_001)
|
|
const result = await resultPromise
|
|
expect(result).toBeUndefined()
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
"[Sitemap] test query failed:",
|
|
expect.any(Error),
|
|
)
|
|
consoleErrorSpy.mockRestore()
|
|
})
|
|
|
|
it("uses default timeout of 15s when not specified", async () => {
|
|
const query = vi.fn().mockResolvedValue([{ id: 1 }])
|
|
const result = await querySafe("test", query)
|
|
expect(result).toEqual([{ id: 1 }])
|
|
})
|
|
})
|
|
|
|
describe("STATIC_PAGES", () => {
|
|
it("contains exactly 7 entries", () => {
|
|
expect(STATIC_PAGES).toHaveLength(7)
|
|
})
|
|
|
|
it("first entry is the homepage with empty urlPath and priority 1", () => {
|
|
expect(STATIC_PAGES[0].urlPath).toBe("")
|
|
expect(STATIC_PAGES[0].priority).toBe(1.0)
|
|
})
|
|
|
|
it("includes /games with priority 0.9", () => {
|
|
const entry = STATIC_PAGES.find((p) => p.urlPath === "/games")
|
|
expect(entry).toBeDefined()
|
|
expect(entry!.priority).toBe(0.9)
|
|
})
|
|
|
|
it("includes /compare with priority 0.5", () => {
|
|
const entry = STATIC_PAGES.find((p) => p.urlPath === "/compare")
|
|
expect(entry).toBeDefined()
|
|
expect(entry!.priority).toBe(0.5)
|
|
})
|
|
|
|
it("includes /search with priority 0.3", () => {
|
|
const entry = STATIC_PAGES.find((p) => p.urlPath === "/search")
|
|
expect(entry).toBeDefined()
|
|
expect(entry!.priority).toBe(0.3)
|
|
})
|
|
|
|
it("every entry has a valid changeFrequency", () => {
|
|
const validFreqs = ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]
|
|
for (const page of STATIC_PAGES) {
|
|
expect(validFreqs).toContain(page.changeFrequency)
|
|
}
|
|
})
|
|
}) |