feat: add extensive test suite
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user