The hotel moved from .nitro to .hab bundles, so the imager must treat .hab like .nitro and the public avatar endpoints must accept the figure strings the client sends (including extra trailing segments such as `.0`, which the old FIGURE_RE rejected with a 400). - imager-upstream: relax FIGURE_RE, extend parseAvatarQuery test coverage - api/imaging/avatar + /imaging: shared validation, placeholder and ETag/304 - api/imaging/badge: single-flight upstream fetch and conditional responses - imager-http/imager-png: strong ETag helpers and APNG passthrough guard - csp + next/image: unoptimized external images, tightened CSP - deps: override postcss-selector-parser to ^7.1.6 - also fold in the pending drizzle draft snapshot and stray test cleanup
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
// @ts-nocheck
|
|
import { rm } from "node:fs/promises";
|
|
import { NextRequest } from "next/server";
|
|
import { afterAll, afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
import { GET } from "../app/api/imaging/badge/route";
|
|
|
|
const SCRATCH_ROOT = `${process.cwd()}/storage/imaging/test-badge-route-${process.pid}`;
|
|
|
|
beforeEach(async () => {
|
|
// Cold disk cache for every test so the candidate walk actually runs.
|
|
await rm(SCRATCH_ROOT, { recursive: true, force: true });
|
|
vi.stubEnv("IMAGING_CACHE_ROOT", SCRATCH_ROOT);
|
|
// A configured CDN short-circuits the database lookup for badge_base_url.
|
|
vi.stubEnv("BADGE_URL", "https://badges.test/album1584");
|
|
globalThis.imagingBadgeFetches?.clear?.();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await rm(SCRATCH_ROOT, { recursive: true, force: true });
|
|
});
|
|
|
|
function gifResponse() {
|
|
return new Response(new Uint8Array([1, 2, 3]), {
|
|
headers: { "content-type": "image/gif" },
|
|
});
|
|
}
|
|
|
|
it("shares a single upstream fetch across concurrent cold-cache requests", async () => {
|
|
let calls = 0;
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => {
|
|
calls += 1;
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
return gifResponse();
|
|
}),
|
|
);
|
|
const request = () =>
|
|
new NextRequest("https://hotel.test/api/imaging/badge?code=ADM");
|
|
const [a, b] = await Promise.all([GET(request()), GET(request())]);
|
|
expect(a.status).toBe(200);
|
|
expect(b.status).toBe(200);
|
|
expect(a.headers.get("content-type")).toBe("image/gif");
|
|
expect(calls).toBe(1);
|
|
});
|
|
|
|
it("serves a previously fetched badge from disk without re-fetching", async () => {
|
|
const fetcher = vi.fn(async () => gifResponse());
|
|
vi.stubGlobal("fetch", fetcher);
|
|
const request = new NextRequest(
|
|
"https://hotel.test/api/imaging/badge?code=HABBO",
|
|
);
|
|
const first = await GET(request);
|
|
expect(first.status).toBe(200);
|
|
const callsAfterFirst = fetcher.mock.calls.length;
|
|
expect(callsAfterFirst).toBeGreaterThan(0);
|
|
|
|
const second = await GET(request);
|
|
expect(second.status).toBe(200);
|
|
expect(second.headers.get("x-imager-source")).toBe("cache");
|
|
expect(fetcher.mock.calls.length).toBe(callsAfterFirst);
|
|
});
|
|
|
|
it("revalidates a cached badge with a 304 when the client already has it", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => gifResponse()),
|
|
);
|
|
const url = "https://hotel.test/api/imaging/badge?code=ETAG";
|
|
const first = await GET(new NextRequest(url));
|
|
const etag = first.headers.get("etag");
|
|
expect(etag).toBeTruthy();
|
|
|
|
const second = await GET(
|
|
new NextRequest(url, { headers: { "if-none-match": etag } }),
|
|
);
|
|
expect(second.status).toBe(304);
|
|
expect(second.headers.get("etag")).toBe(etag);
|
|
});
|
|
|
|
it("serves a placeholder when every candidate is unavailable", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => new Response("nope", { status: 404 })),
|
|
);
|
|
const result = await GET(
|
|
new NextRequest("https://hotel.test/api/imaging/badge?code=GONE"),
|
|
);
|
|
expect(result.status).toBe(200);
|
|
expect(result.headers.get("content-type")).toBe("image/svg+xml");
|
|
expect(result.headers.get("x-imager-source")).toBe("unavailable");
|
|
});
|
|
|
|
it("rejects malformed badge codes without contacting any candidate", async () => {
|
|
const fetcher = vi.fn();
|
|
vi.stubGlobal("fetch", fetcher);
|
|
const result = await GET(
|
|
new NextRequest("https://hotel.test/api/imaging/badge?code=../bad"),
|
|
);
|
|
expect(result.status).toBe(400);
|
|
expect(fetcher).not.toHaveBeenCalled();
|
|
});
|