| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import { describe, expect, test, vi } from "vitest";
- import { z } from "@hono/zod-openapi";
- import {
- IdParamSchema,
- PaginationSchema,
- SortSchema,
- createActionRoute,
- createActionRoutes,
- createParamSchema,
- } from "@/lib/api/action-adapter-openapi";
- /**
- * 说明:
- * - 这些测试只覆盖 adapter 的“通用执行器”逻辑
- * - 不依赖 Next/Hono 的完整运行时
- * - 重点验证:参数映射、返回值包装、错误/异常处理、requiresAuth=false 分支
- */
- function createMockContext(options?: { body?: unknown; jsonThrows?: boolean }) {
- const body = options?.body ?? {};
- const jsonThrows = options?.jsonThrows ?? false;
- return {
- req: {
- json: async () => {
- if (jsonThrows) {
- throw new Error("invalid json");
- }
- return body;
- },
- },
- json: (payload: unknown, status = 200) =>
- new Response(JSON.stringify(payload), {
- status,
- headers: { "content-type": "application/json" },
- }),
- } as const;
- }
- describe("Action Adapter:createActionRoute(单元测试)", () => {
- test("requiresAuth=false:返回非 ActionResult 时自动包装 {ok:true,data}", async () => {
- const { handler } = createActionRoute(
- "test",
- "returnsRaw",
- async () => {
- return { hello: "world" };
- },
- { requiresAuth: false }
- );
- const response = (await handler(createMockContext({ body: {} }) as any)) as Response;
- expect(response.status).toBe(200);
- await expect(response.json()).resolves.toEqual({ ok: true, data: { hello: "world" } });
- });
- test("默认参数推断:schema 单字段时应传入该字段值", async () => {
- const action = vi.fn(async (id: number) => ({ id }));
- const { handler } = createActionRoute("test", "singleArg", action as any, {
- requiresAuth: false,
- requestSchema: z.object({ id: z.number() }),
- });
- const response = (await handler(createMockContext({ body: { id: 123 } }) as any)) as Response;
- expect(action).toHaveBeenCalledWith(123);
- expect(response.status).toBe(200);
- await expect(response.json()).resolves.toEqual({ ok: true, data: { id: 123 } });
- });
- test("默认参数推断:多字段 schema 传入整个 body(单参)", async () => {
- const action = vi.fn(async (body: { a: string; b: string }) => body);
- const { handler } = createActionRoute("test", "multiKey", action as any, {
- requiresAuth: false,
- requestSchema: z.object({ a: z.string(), b: z.string() }),
- });
- const response = (await handler(
- createMockContext({ body: { a: "x", b: "y" } }) as any
- )) as Response;
- expect(action).toHaveBeenCalledWith({ a: "x", b: "y" });
- expect(response.status).toBe(200);
- await expect(response.json()).resolves.toEqual({ ok: true, data: { a: "x", b: "y" } });
- });
- test("argsMapper:应优先使用显式映射以支持多参数 action", async () => {
- const action = vi.fn(async (userId: number, data: { name: string }) => ({ userId, data }));
- const { handler } = createActionRoute("test", "mappedArgs", action as any, {
- requiresAuth: false,
- requestSchema: z.object({
- userId: z.number(),
- data: z.object({ name: z.string() }),
- }),
- argsMapper: (body: { userId: number; data: { name: string } }) => [body.userId, body.data],
- });
- const response = (await handler(
- createMockContext({ body: { userId: 7, data: { name: "alice" } } }) as any
- )) as Response;
- expect(action).toHaveBeenCalledWith(7, { name: "alice" });
- expect(response.status).toBe(200);
- await expect(response.json()).resolves.toEqual({
- ok: true,
- data: { userId: 7, data: { name: "alice" } },
- });
- });
- test("action 返回 ok=false:应返回 400 且透传 errorCode/errorParams", async () => {
- const { handler } = createActionRoute(
- "test",
- "returnsError",
- async () => ({
- ok: false,
- error: "业务错误",
- errorCode: "BIZ_ERROR",
- errorParams: { field: "name" },
- }),
- { requiresAuth: false }
- );
- const response = (await handler(createMockContext({ body: {} }) as any)) as Response;
- expect(response.status).toBe(400);
- await expect(response.json()).resolves.toEqual({
- ok: false,
- error: "业务错误",
- errorCode: "BIZ_ERROR",
- errorParams: { field: "name" },
- });
- });
- test("action 抛出 Error:应返回 500 且返回 error.message", async () => {
- const { handler } = createActionRoute(
- "test",
- "throwsError",
- async () => {
- throw new Error("boom");
- },
- { requiresAuth: false }
- );
- const response = (await handler(createMockContext({ body: {} }) as any)) as Response;
- expect(response.status).toBe(500);
- await expect(response.json()).resolves.toEqual({ ok: false, error: "boom" });
- });
- test("action 抛出非 Error:应返回 500 且返回通用错误消息", async () => {
- const { handler } = createActionRoute(
- "test",
- "throwsUnknown",
- async () => {
- // eslint-disable-next-line no-throw-literal
- throw "boom";
- },
- { requiresAuth: false }
- );
- const response = (await handler(createMockContext({ body: {} }) as any)) as Response;
- expect(response.status).toBe(500);
- await expect(response.json()).resolves.toEqual({ ok: false, error: "服务器内部错误" });
- });
- test("请求体不是 JSON:应降级为 {} 并继续执行", async () => {
- const action = vi.fn(async () => "ok");
- const { handler } = createActionRoute("test", "badJson", action as any, {
- requiresAuth: false,
- });
- const response = (await handler(createMockContext({ jsonThrows: true }) as any)) as Response;
- expect(action).toHaveBeenCalledTimes(1);
- expect(response.status).toBe(200);
- await expect(response.json()).resolves.toEqual({ ok: true, data: "ok" });
- });
- });
- describe("Action Adapter:辅助导出函数(单元测试)", () => {
- test("createActionRoutes:应批量生成 route/handler", () => {
- const routes = createActionRoutes(
- "demo",
- {
- a: async () => ({ ok: true, data: 1 }),
- b: async () => 2,
- },
- {
- b: { requiresAuth: false },
- }
- );
- expect(routes).toHaveLength(2);
- });
- test("通用 schemas:应支持解析与默认值", () => {
- const schema = createParamSchema({ name: z.string() });
- expect(schema.parse({ name: "x" })).toEqual({ name: "x" });
- expect(IdParamSchema.parse({ id: 1 })).toEqual({ id: 1 });
- expect(PaginationSchema.parse({})).toEqual({ page: 1, pageSize: 20 });
- expect(SortSchema.parse({})).toEqual({ sortBy: undefined, sortOrder: "desc" });
- });
- });
|