import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { can } from "../../utils/permissions.ts"; import { canProxyImage, proxyImageUrl } from "../../utils/proxyImage.ts"; import { relativeTime } from "../../utils/relativeTime.ts"; import { hueFromString, initialsFor, isSafeHex, tintFor, } from "../../utils/thumbnailTint.ts"; import { dumpFileUrl, dumpThumbnailUrl, dumpUrl, normalizeUrl, playlistUrl, } from "../../utils/urls.ts"; import { API_URL } from "../../config/api.ts"; import { i18n } from "../../i18n.ts"; describe("can", () => { test("admins can do everything, moderators moderate, users nothing", () => { expect(can({ role: "admin" }, "category:manage")).toBe(true); expect(can({ role: "admin" }, "user:manage")).toBe(true); expect(can({ role: "moderator" }, "dump:moderate")).toBe(true); expect(can({ role: "moderator" }, "chat:moderate")).toBe(true); expect(can({ role: "moderator" }, "category:manage")).toBe(false); expect(can({ role: "user" }, "dump:moderate")).toBe(false); }); test("no user, no permission", () => { expect(can(null, "dump:moderate")).toBe(false); expect(can(undefined, "dump:moderate")).toBe(false); }); }); describe("proxyImageUrl", () => { test("http images go through the proxy; https ones don't", () => { expect(proxyImageUrl("http://img.test/a.png")).toBe( `${API_URL}/api/proxy-image?url=${ encodeURIComponent("http://img.test/a.png") }`, ); expect(proxyImageUrl("https://img.test/a.png")).toBe( "https://img.test/a.png", ); }); test("force proxies https too", () => { expect(proxyImageUrl("https://img.test/a.png", { force: true })).toContain( "/api/proxy-image?url=", ); }); test("relative, local and non-http URLs are left alone", () => { expect(proxyImageUrl("/api/files/x")).toBe("/api/files/x"); expect(proxyImageUrl("http://localhost:8000/a.png")).toBe( "http://localhost:8000/a.png", ); expect(proxyImageUrl("http://127.0.0.1/a.png")).toBe( "http://127.0.0.1/a.png", ); expect(proxyImageUrl("data:image/png;base64,AA")).toBe( "data:image/png;base64,AA", ); }); }); describe("canProxyImage", () => { test("third-party http(s) only", () => { expect(canProxyImage("https://elsewhere.test/a.png")).toBe(true); expect(canProxyImage(`${API_URL}/api/files/x`)).toBe(false); expect(canProxyImage("/relative.png")).toBe(false); expect(canProxyImage("data:image/png;base64,AA")).toBe(false); expect(canProxyImage("http://[bad")).toBe(false); }); }); describe("relativeTime", () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-06-15T12:00:00Z")); }); afterEach(() => vi.useRealTimers()); const ago = (s: number) => new Date(Date.now() - s * 1000); test("picks the largest sensible unit", () => { expect(relativeTime(ago(0))).toBe("now"); expect(relativeTime(ago(30))).toBe("30 seconds ago"); expect(relativeTime(ago(120))).toBe("2 minutes ago"); expect(relativeTime(ago(3 * 3600))).toBe("3 hours ago"); expect(relativeTime(ago(86400))).toBe("yesterday"); expect(relativeTime(ago(14 * 86400))).toBe("2 weeks ago"); expect(relativeTime(ago(60 * 86400))).toBe("2 months ago"); expect(relativeTime(ago(800 * 86400))).toBe("2 years ago"); }); test("follows the active locale", () => { try { i18n.load("fr", {}); i18n.activate("fr"); expect(relativeTime(ago(120))).toBe("il y a 2 minutes"); } finally { i18n.activate("en"); } }); }); describe("thumbnail tint", () => { test("isSafeHex accepts only canonical #rrggbb", () => { expect(isSafeHex("#a1b2c3")).toBe(true); expect(isSafeHex("#A1B2C3")).toBe(false); expect(isSafeHex("#abc")).toBe(false); expect(isSafeHex("red")).toBe(false); expect(isSafeHex("#000000;background:url(x)")).toBe(false); expect(isSafeHex(undefined)).toBe(false); }); test("hueFromString is stable and in range", () => { expect(hueFromString("example.com")).toBe(hueFromString("example.com")); const hue = Number(/hsl\((\d+)/.exec(hueFromString("x"))![1]); expect(hue).toBeGreaterThanOrEqual(0); expect(hue).toBeLessThan(360); }); test("tintFor prefers the page colour, then the hostname sans www", () => { expect(tintFor({ accentColor: "#112233", url: "https://a.b" })).toBe( "#112233", ); expect(tintFor({ accentColor: "evil", url: "https://www.a.b/x" })).toBe( hueFromString("a.b"), ); expect(tintFor({ seed: "file.mp3" })).toBe(hueFromString("file.mp3")); expect(tintFor({ url: "not a url" })).toBe(hueFromString("not a url")); }); test("initialsFor takes the first letter or digit", () => { expect(initialsFor(" bandcamp", undefined)).toBe("B"); expect(initialsFor(undefined, "https://www.youtube.com/x")).toBe("Y"); expect(initialsFor("«Élan»", undefined)).toBe("É"); expect(initialsFor("", "")).toBe("?"); }); }); describe("urls", () => { test("dumpFileUrl versions by size and last change, and carries the token", () => { const createdAt = new Date(1000); const updatedAt = new Date(2000); expect(dumpFileUrl({ id: "d1", fileSize: 42, createdAt })).toBe( `${API_URL}/api/files/d1?v=42-1000`, ); // An edit (a replaced file among them) moves the version on. expect(dumpFileUrl({ id: "d1", fileSize: 42, createdAt, updatedAt })) .toBe(`${API_URL}/api/files/d1?v=42-2000`); expect(dumpFileUrl({ id: "d1" }, "t o+k")).toBe( `${API_URL}/api/files/d1?v=0-0&token=t%20o%2Bk`, ); }); test("dumpThumbnailUrl", () => { expect(dumpThumbnailUrl({ id: "d1" })).toBe(`${API_URL}/api/thumbnails/d1`); expect(dumpThumbnailUrl({ id: "d1" }, "tk")).toBe( `${API_URL}/api/thumbnails/d1?token=tk`, ); expect( dumpThumbnailUrl({ id: "d1", fileSize: 7, createdAt: new Date(5) }, "tk"), ).toBe(`${API_URL}/api/thumbnails/d1?v=7-5&token=tk`); }); test("normalizeUrl adds a scheme when there's none", () => { expect(normalizeUrl(" example.com/x ")).toBe("https://example.com/x"); expect(normalizeUrl("//cdn.test/a")).toBe("https://cdn.test/a"); expect(normalizeUrl("HTTP://A.B")).toBe("HTTP://A.B"); expect(normalizeUrl(" ")).toBe(""); }); test("slug beats id in page URLs", () => { expect(dumpUrl({ id: "1", slug: "nice-title" })).toBe("/dumps/nice-title"); expect(dumpUrl({ id: "1" })).toBe("/dumps/1"); expect(playlistUrl({ id: "2", slug: "mix" })).toBe("/playlists/mix"); expect(playlistUrl({ id: "2" })).toBe("/playlists/2"); }); });