feat: add extensive test suite

This commit is contained in:
2026-08-30 11:33:53 +07:00 Unverified
parent 061cce00dc
commit 8359c7272e
131 changed files with 8717 additions and 2200 deletions
+95
View File
@@ -0,0 +1,95 @@
import { describe, expect, mock, test } from "bun:test";
import {
type CardCharacter,
type CardSubmission,
createCardHandler,
} from "./handler";
const context = (sub: string) => ({ params: Promise.resolve({ sub }) });
const request = new Request("https://buzz.test/api/card/sub-1");
function dependencies() {
return {
findSubmission: mock(
async (_id: string): Promise<CardSubmission | undefined> => ({
uid: "800000001",
char: "Furina",
}),
),
findImage: mock(
async (_id: string): Promise<Uint8Array<ArrayBuffer> | undefined> =>
undefined,
),
findCharacter: mock(
async (_name: string): Promise<CardCharacter | undefined> => ({
amber: "10000089-Furina",
}),
),
fetch: mock(
async (_input: RequestInfo | URL, _init?: RequestInit) =>
new Response(new Uint8Array([4, 5, 6])),
),
persistImage: mock(async (_id: string, _image: ArrayBuffer) => {}),
after: mock((task: () => Promise<void>) => void task()),
};
}
describe("card handler", () => {
test("returns a cached card without querying character services", async () => {
const deps = dependencies();
deps.findImage.mockResolvedValue(new Uint8Array([1, 2, 3]));
const response = await createCardHandler(deps)(request, context("cached"));
expect(new Uint8Array(await response.arrayBuffer())).toEqual(
new Uint8Array([1, 2, 3]),
);
expect(deps.findCharacter).not.toHaveBeenCalled();
expect(deps.fetch).not.toHaveBeenCalled();
});
test("identifies an unknown submission by the requested id", async () => {
const deps = dependencies();
deps.findSubmission.mockResolvedValue(undefined);
const response = await createCardHandler(deps)(request, context("missing"));
expect(response.status).toBe(404);
expect(await response.text()).toBe("Unknown submission: missing");
});
test("rejects submissions without a character", async () => {
const deps = dependencies();
deps.findSubmission.mockResolvedValue({ uid: "1", char: null });
const response = await createCardHandler(deps)(request, context("generic"));
expect(response.status).toBe(422);
expect(deps.findCharacter).not.toHaveBeenCalled();
});
test("reports a missing character record", async () => {
const deps = dependencies();
deps.findCharacter.mockResolvedValue(undefined);
const response = await createCardHandler(deps)(request, context("sub-1"));
expect(response.status).toBe(500);
expect(await response.text()).toBe("Unknown character: Furina");
});
test("returns upstream errors unchanged", async () => {
const deps = dependencies();
deps.fetch.mockResolvedValue(new Response("busy", { status: 503 }));
const response = await createCardHandler(deps)(request, context("sub-1"));
expect(response.status).toBe(503);
expect(await response.text()).toBe("busy");
expect(deps.persistImage).not.toHaveBeenCalled();
});
test("fetches and schedules persistence of a fresh card", async () => {
const deps = dependencies();
const response = await createCardHandler(deps)(request, context("sub-1"));
expect(deps.fetch.mock.calls[0]?.[0]).toBe(
"https://api.astrxl.dev/v1/card/genshin/800000001/10000089?lang=th&substat=true&quality=true",
);
expect(new Uint8Array(await response.arrayBuffer())).toEqual(
new Uint8Array([4, 5, 6]),
);
expect(deps.after).toHaveBeenCalledTimes(1);
expect(deps.persistImage).toHaveBeenCalledTimes(1);
expect(deps.persistImage.mock.calls[0]?.[0]).toBe("sub-1");
});
});
+46
View File
@@ -0,0 +1,46 @@
export type CardSubmission = { uid: string; char: string | null };
export type CardCharacter = { amber: string };
type CardDependencies = {
findSubmission: (id: string) => Promise<CardSubmission | undefined>;
findImage: (id: string) => Promise<Uint8Array<ArrayBuffer> | undefined>;
findCharacter: (name: string) => Promise<CardCharacter | undefined>;
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
persistImage: (id: string, image: ArrayBuffer) => Promise<unknown>;
after: (task: () => Promise<void>) => void;
};
export function createCardHandler(dependencies: CardDependencies) {
return async function GET(
_request: Request,
{ params }: { params: Promise<{ sub: string }> },
) {
const { sub: subId } = await params;
const [sub, image] = await Promise.all([
dependencies.findSubmission(subId),
dependencies.findImage(subId),
]);
if (image) return new Response(image);
if (!sub)
return new Response(`Unknown submission: ${subId}`, { status: 404 });
if (!sub.char)
return new Response("This submission isn't character-specific.", {
status: 422,
});
const character = await dependencies.findCharacter(sub.char);
if (!character)
return new Response(`Unknown character: ${sub.char}`, { status: 500 });
const card = await dependencies.fetch(
`https://api.astrxl.dev/v1/card/genshin/${encodeURIComponent(sub.uid)}/${encodeURIComponent(character.amber.split("-")[0])}?lang=th&substat=true&quality=true`,
);
if (!card.ok) return card;
const fresh = await card.arrayBuffer();
dependencies.after(async () => {
await dependencies.persistImage(subId, fresh);
});
return new Response(fresh);
};
}
+33 -29
View File
@@ -2,34 +2,32 @@ import { eq } from "drizzle-orm";
import { after, type NextRequest } from "next/server";
import { db } from "@/lib/db";
import { cards, characters, submissions } from "@/lib/db/schema";
import { createCardHandler } from "./handler";
export async function GET(
_req: NextRequest,
{ params }: RouteContext<"/api/card/[sub]">,
) {
const { sub: subId } = await params;
const [[sub], [image]] = await Promise.all([
db.select().from(submissions).where(eq(submissions.id, subId)),
db.select().from(cards).where(eq(cards.submission, subId)),
]);
if (image?.image) return new Response(new Uint8Array(image.image));
if (!sub) return new Response(`Unknown submission: ${sub}`, { status: 404 });
if (!sub.char)
return new Response(`This submission isn't character-specific.`, {
status: 422,
});
const [char] = await db
.select()
.from(characters)
.where(eq(characters.name, sub.char));
if (!char)
return new Response(`Unknown character: ${sub.char}`, { status: 500 });
const card = await fetch(
`https://api.astrxl.dev/v1/card/genshin/${sub.uid}/${char.amber.split("-")[0]}?lang=th&substat=true&quality=true`,
);
if (!card.ok) return card;
const fresh = await card.arrayBuffer();
after(async () => {
const handler = createCardHandler({
findSubmission: async (id) =>
db
.select({ uid: submissions.uid, char: submissions.char })
.from(submissions)
.where(eq(submissions.id, id))
.then(([value]) => value),
findImage: async (id) =>
db
.select({ image: cards.image })
.from(cards)
.where(eq(cards.submission, id))
.then(([value]) =>
value?.image ? new Uint8Array(value.image) : undefined,
),
findCharacter: async (name) =>
db
.select({ amber: characters.amber })
.from(characters)
.where(eq(characters.name, name))
.then(([value]) => value),
fetch,
after,
persistImage: async (subId, fresh) => {
await db
.insert(cards)
.values({
@@ -42,6 +40,12 @@ export async function GET(
image: Buffer.from(fresh),
},
});
});
return new Response(fresh);
},
});
export function GET(
req: NextRequest,
context: RouteContext<"/api/card/[sub]">,
) {
return handler(req, context);
}