All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
6.5 KiB
TypeScript
179 lines
6.5 KiB
TypeScript
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");
|
|
});
|
|
});
|