feat: add extensive test suite
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { createSseHandler, tierListTopic } from "./handler";
|
||||
|
||||
const context = (topic: string) => ({ params: Promise.resolve({ topic }) });
|
||||
|
||||
function dependencies() {
|
||||
return {
|
||||
activeStream: mock(
|
||||
async (_options: { signal: AbortSignal }) => new Response("active"),
|
||||
),
|
||||
logStream: mock(
|
||||
async (_options: { signal: AbortSignal }) => new Response("log"),
|
||||
),
|
||||
tierStream: mock(
|
||||
async (_options: { signal: AbortSignal }) => new Response("tier"),
|
||||
),
|
||||
adminCheck: mock(async () => true),
|
||||
};
|
||||
}
|
||||
|
||||
describe("SSE handler", () => {
|
||||
test("the tier-list topic regex permits only intended identifier characters", () => {
|
||||
expect(tierListTopic.test("tl.genshin_5.0-beta")).toBeTrue();
|
||||
expect(tierListTopic.test("tl.bad/topic")).toBeFalse();
|
||||
expect(tierListTopic.test("tl.bad@topic")).toBeFalse();
|
||||
expect(tierListTopic.test("tl.")).toBeFalse();
|
||||
});
|
||||
|
||||
test("streams a named endpoint with the request abort signal", async () => {
|
||||
const deps = dependencies();
|
||||
const request = new Request("https://buzz.test/sse/active");
|
||||
const handler = createSseHandler({
|
||||
endpoints: { active: { stream: deps.activeStream } },
|
||||
adminTopics: [],
|
||||
adminCheck: deps.adminCheck,
|
||||
tierListEndpoint: () => ({ stream: deps.tierStream }),
|
||||
});
|
||||
expect(await (await handler(request, context("active"))).text()).toBe(
|
||||
"active",
|
||||
);
|
||||
expect(deps.activeStream).toHaveBeenCalledWith({ signal: request.signal });
|
||||
});
|
||||
|
||||
test("rejects an unknown endpoint", async () => {
|
||||
const deps = dependencies();
|
||||
const response = await createSseHandler({
|
||||
endpoints: {},
|
||||
adminTopics: [],
|
||||
adminCheck: deps.adminCheck,
|
||||
tierListEndpoint: () => ({ stream: deps.tierStream }),
|
||||
})(new Request("https://buzz.test/sse/nope"), context("nope"));
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test("protects admin endpoints", async () => {
|
||||
const deps = dependencies();
|
||||
deps.adminCheck.mockResolvedValue(false);
|
||||
const response = await createSseHandler({
|
||||
endpoints: { log: { stream: deps.logStream } },
|
||||
adminTopics: ["log"],
|
||||
adminCheck: deps.adminCheck,
|
||||
tierListEndpoint: () => ({ stream: deps.tierStream }),
|
||||
})(new Request("https://buzz.test/sse/log"), context("log"));
|
||||
expect(response.status).toBe(401);
|
||||
expect(deps.logStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("extracts a tier-list id and forwards the abort signal", async () => {
|
||||
const deps = dependencies();
|
||||
const getTier = mock((_id: string) => ({ stream: deps.tierStream }));
|
||||
const request = new Request("https://buzz.test/sse/tl.list-1");
|
||||
await createSseHandler({
|
||||
endpoints: {},
|
||||
adminTopics: [],
|
||||
adminCheck: deps.adminCheck,
|
||||
tierListEndpoint: getTier,
|
||||
})(request, context("tl.list-1"));
|
||||
expect(getTier).toHaveBeenCalledWith("list-1");
|
||||
expect(deps.tierStream).toHaveBeenCalledWith({ signal: request.signal });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
type Endpoint = {
|
||||
stream: (options: {
|
||||
signal: AbortSignal;
|
||||
}) => Response | Promise<Response> | undefined;
|
||||
};
|
||||
|
||||
const stream = async (endpoint: Endpoint, signal: AbortSignal) => {
|
||||
const response = await endpoint.stream({ signal });
|
||||
if (!response) return new Response("SSE unavailable", { status: 503 });
|
||||
return response;
|
||||
};
|
||||
|
||||
export const tierListTopic = /^tl\.([a-z0-9._-]+)$/;
|
||||
|
||||
export function createSseHandler(dependencies: {
|
||||
endpoints: Record<string, Endpoint>;
|
||||
adminTopics: readonly string[];
|
||||
adminCheck: () => Promise<unknown>;
|
||||
tierListEndpoint: (id: string) => Endpoint;
|
||||
}) {
|
||||
return async function GET(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ topic: string }> },
|
||||
) {
|
||||
const { topic } = await params;
|
||||
const tierList = tierListTopic.exec(topic);
|
||||
if (tierList)
|
||||
return stream(dependencies.tierListEndpoint(tierList[1]), req.signal);
|
||||
|
||||
const endpoint = dependencies.endpoints[topic];
|
||||
if (!endpoint) return new Response("Invalid SSE Endpoint", { status: 404 });
|
||||
if (
|
||||
dependencies.adminTopics.includes(topic) &&
|
||||
!(await dependencies.adminCheck())
|
||||
)
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
return stream(endpoint, req.signal);
|
||||
};
|
||||
}
|
||||
@@ -1,24 +1,12 @@
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { adminSseList, sse, tlSse } from "@/lib/db/sse-endpoints";
|
||||
import { createSseHandler } from "./handler";
|
||||
|
||||
function isValidKey<T extends { [x: string]: unknown }>(
|
||||
map: T,
|
||||
key: string | number | symbol,
|
||||
): key is keyof T {
|
||||
return Object.hasOwn(map, key);
|
||||
}
|
||||
|
||||
const tlRegex = /^tl\.([a-z0-9.-_]+)$/;
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ topic: string }> },
|
||||
) {
|
||||
const { topic } = await params;
|
||||
if (tlRegex.test(topic)) return tlSse(topic.match(tlRegex)![1]).stream();
|
||||
if (!isValidKey(sse, topic))
|
||||
return new Response("Invalid SSE Endpoint", { status: 404 });
|
||||
if (adminSseList.includes(topic) && !(await adminCheck()))
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
return sse[topic].stream();
|
||||
}
|
||||
export const GET = createSseHandler({
|
||||
endpoints: sse as unknown as Parameters<
|
||||
typeof createSseHandler
|
||||
>[0]["endpoints"],
|
||||
adminTopics: adminSseList,
|
||||
adminCheck,
|
||||
tierListEndpoint: tlSse,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user