Files
buzz/lib/server-handlers.test.ts

244 lines
7.1 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
createAmberProxyHandler,
createAmberSyncHandler,
createCdnHandler,
createCountHandler,
createProxyHandler,
createSlipHandler,
createStatesHandler,
} from "./server-handlers";
const request = new Request("https://buzz.example/test");
const notFound = () => {
throw new Error("not found");
};
describe("artifact count handler", () => {
test("returns the submission count as JSON", async () => {
const response = await createCountHandler(async () => 12)();
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("application/json");
expect(await response.json()).toBe(12);
});
});
describe("rubgram count handler", () => {
test("returns the non-deleted submission count as JSON", async () => {
const response = await createCountHandler(async () => 7)();
expect(await response.json()).toBe(7);
});
});
describe("tier-list states handler", () => {
test("passes the route version to the query and returns its states", async () => {
let queriedVersion: string | undefined;
const states = [{ id: "state-1" }, { id: "state-2" }];
const handler = createStatesHandler(async (version) => {
queriedVersion = version;
return states;
});
const response = await handler(request, {
params: Promise.resolve({ ver: "3.4" }),
});
expect(queriedVersion).toBe("3.4");
expect(await response.json()).toEqual(states);
});
});
describe("slip handler", () => {
test("returns JPEG bytes for the requested slip", async () => {
let queriedId: string | undefined;
const handler = createSlipHandler(async (id) => {
queriedId = id;
return new Uint8Array([0xff, 0xd8, 0xff]);
}, notFound);
const response = await handler(request, {
params: Promise.resolve({ id: "slip-1" }),
});
expect(queriedId).toBe("slip-1");
expect(response.headers.get("content-type")).toBe("image/jpeg");
expect(new Uint8Array(await response.arrayBuffer())).toEqual(
new Uint8Array([0xff, 0xd8, 0xff]),
);
});
test("uses the not-found path when the slip is absent", async () => {
const handler = createSlipHandler(async () => null, notFound);
expect(
handler(request, { params: Promise.resolve({ id: "missing" }) }),
).rejects.toThrow("not found");
});
});
describe("CDN handler", () => {
test("returns immutable content with stored metadata", async () => {
let queriedId: string | undefined;
const handler = createCdnHandler(async (id) => {
queriedId = id;
return {
data: new Uint8Array([1, 2, 3, 4]),
type: "image/png",
size: "4",
};
}, notFound);
const response = await handler(request, {
params: Promise.resolve({ id: "asset-1" }),
});
expect(queriedId).toBe("asset-1");
expect(response.headers.get("content-type")).toBe("image/png");
expect(response.headers.get("content-length")).toBe("4");
expect(response.headers.get("cache-control")).toBe(
"public, max-age=31536000, immutable",
);
expect(new Uint8Array(await response.arrayBuffer())).toEqual(
new Uint8Array([1, 2, 3, 4]),
);
});
test("uses the not-found path when the asset is absent", async () => {
const handler = createCdnHandler(async () => null, notFound);
expect(
handler(request, { params: Promise.resolve({ id: "missing" }) }),
).rejects.toThrow("not found");
});
});
describe("Amber log handler", () => {
test("fetches the changelog with the current version hash", async () => {
let requestedUrl: string | undefined;
const handler = createAmberProxyHandler("static/changelog", {
getVersionHash: async () => "hash-log",
fetchText: async (url) => {
requestedUrl = url;
return '{"changes":[]}';
},
});
const response = await handler();
expect(requestedUrl).toBe(
"https://gi.yatta.moe/api/v2/static/changelog?vh=hash-log",
);
expect(response.headers.get("content-type")).toBe("application/json");
expect(await response.text()).toBe('{"changes":[]}');
});
});
describe("Amber character handler", () => {
test("fetches avatars with the current version hash", async () => {
let requestedUrl: string | undefined;
const handler = createAmberProxyHandler("en/avatar", {
getVersionHash: async () => "hash-char",
fetchText: async (url) => {
requestedUrl = url;
return '{"items":[]}';
},
});
const response = await handler();
expect(requestedUrl).toBe(
"https://gi.yatta.moe/api/v2/en/avatar?vh=hash-char",
);
expect(await response.text()).toBe('{"items":[]}');
});
});
describe("Amber sync handler", () => {
test("rejects non-admin requests without syncing or logging", async () => {
let synced = false;
let logged = false;
const handler = createAmberSyncHandler({
adminCheck: async () => null,
sync: async () => {
synced = true;
},
log: async () => {
logged = true;
},
});
const response = await handler();
expect(response.status).toBe(401);
expect(await response.text()).toBe("Unauthorized");
expect(synced).toBeFalse();
expect(logged).toBeFalse();
});
test("syncs before recording the admin action", async () => {
const calls: string[] = [];
const handler = createAmberSyncHandler({
adminCheck: async () => ({ role: "admin" }),
sync: async () => {
calls.push("sync");
},
log: async (message) => {
calls.push(message);
},
});
const response = await handler();
expect(calls).toEqual(["sync", "Triggered an Amber sync from API"]);
expect(response.status).toBe(200);
expect(await response.text()).toContain("OK(");
});
});
describe("application proxy", () => {
const proxyRequest = {
url: "https://buzz.example/admin/users?tab=banned",
nextUrl: { pathname: "/admin/users", search: "?tab=banned" },
};
test("redirects anonymous requests to login with the full next path", async () => {
let redirectUrl: URL | undefined;
const handler = createProxyHandler({
getSessionCookie: () => undefined,
redirect: (url) => {
redirectUrl = url;
return new Response(null, { status: 307 });
},
next: () => new Response("next"),
});
const response = await handler(proxyRequest);
expect(response.status).toBe(307);
expect(redirectUrl?.pathname).toBe("/login");
expect(redirectUrl?.searchParams.get("next")).toBe(
"/admin/users?tab=banned",
);
});
test("continues requests with a session cookie", async () => {
let redirected = false;
const handler = createProxyHandler({
getSessionCookie: () => "session-cookie",
redirect: () => {
redirected = true;
return new Response(null, { status: 307 });
},
next: () => new Response("next", { status: 200 }),
});
const response = await handler(proxyRequest);
expect(response.status).toBe(200);
expect(await response.text()).toBe("next");
expect(redirected).toBeFalse();
});
});