|
5 | 5 | import express from "express"; |
6 | 6 | import { expressAdapter } from "./adapters"; |
7 | 7 | import { httpReq as request } from "./http-req"; |
8 | | -import { afterAll, describe, expect, it } from "vitest"; |
| 8 | +import { afterAll, describe, expect, it, vi } from "vitest"; |
| 9 | +import { z } from "zod"; |
| 10 | +import { wrapWithEnvelope } from "../lib/adapters/utils.js"; |
| 11 | +import type { Context } from "../lib/adapters/types.js"; |
9 | 12 | import lib from "./lib"; |
10 | 13 |
|
| 14 | +/** 构造最小 Context mock(state 可读写,reply 记录调用) */ |
| 15 | +function mockCtx(): Context & { |
| 16 | + __returned?: boolean; |
| 17 | + __returnValue?: unknown; |
| 18 | + reply: { |
| 19 | + status: ReturnType<typeof vi.fn>; |
| 20 | + json: ReturnType<typeof vi.fn>; |
| 21 | + send: ReturnType<typeof vi.fn>; |
| 22 | + raw: unknown; |
| 23 | + }; |
| 24 | +} { |
| 25 | + const reply = { |
| 26 | + status: vi.fn(() => reply), |
| 27 | + json: vi.fn(), |
| 28 | + send: vi.fn(), |
| 29 | + raw: {}, |
| 30 | + }; |
| 31 | + return { |
| 32 | + method: "GET", |
| 33 | + path: "/", |
| 34 | + headers: {}, |
| 35 | + params: {}, |
| 36 | + query: {}, |
| 37 | + body: {}, |
| 38 | + state: {}, |
| 39 | + reply, |
| 40 | + } as Context & { __returned?: boolean; __returnValue?: unknown; reply: typeof reply }; |
| 41 | +} |
| 42 | + |
11 | 43 | describe("全局 response envelope(registerTyped return 模式)", () => { |
12 | 44 | const envelopers = { |
13 | 45 | success: (data: unknown) => ({ success: true, data }), |
@@ -80,6 +112,114 @@ describe("全局 response envelope(registerTyped return 模式)", () => { |
80 | 112 | expect(res.status).toBe(200); |
81 | 113 | expect(res.body).toEqual({ success: true, data: undefined }); |
82 | 114 | }); |
| 115 | + |
| 116 | + it("enveloper 模式 + response schema → 校验 return 值后再包装", async () => { |
| 117 | + const app = express(); |
| 118 | + app.use(express.json()); |
| 119 | + const apiService = lib({ basePath: "" }); |
| 120 | + apiService.setResponseEnvelopers(envelopers); |
| 121 | + |
| 122 | + apiService.api |
| 123 | + .get("/env-schema") |
| 124 | + .group("Index") |
| 125 | + .title("env-schema") |
| 126 | + .registerTyped( |
| 127 | + { response: z.object({ id: z.number() }) }, |
| 128 | + async () => ({ id: 7 }) // return 值会先经 response schema 校验,再经 enveloper 包装 |
| 129 | + ); |
| 130 | + |
| 131 | + apiService.bind({ adapter: expressAdapter, router: app }); |
| 132 | + const res = await request(app).get("/env-schema"); |
| 133 | + expect(res.status).toBe(200); |
| 134 | + expect(res.body).toEqual({ success: true, data: { id: 7 } }); |
| 135 | + }); |
| 136 | + |
| 137 | + it("enveloper 模式 + response schema → return 值不合 schema 时抛错(走 error enveloper)", async () => { |
| 138 | + const app = express(); |
| 139 | + app.use(express.json()); |
| 140 | + const apiService = lib({ basePath: "" }); |
| 141 | + apiService.setResponseEnvelopers(envelopers); |
| 142 | + |
| 143 | + apiService.api |
| 144 | + .get("/env-schema-bad") |
| 145 | + .group("Index") |
| 146 | + .title("env-schema-bad") |
| 147 | + .registerTyped( |
| 148 | + { response: z.object({ id: z.number() }) }, |
| 149 | + async () => ({ id: "not-a-number" }) as never // 类型撒谎:实际返回 string |
| 150 | + ); |
| 151 | + |
| 152 | + apiService.bind({ adapter: expressAdapter, router: app }); |
| 153 | + const res = await request(app).get("/env-schema-bad"); |
| 154 | + // response schema 校验失败 → 抛 ZodError → error enveloper 包装 |
| 155 | + expect(res.status).toBe(500); |
| 156 | + expect(res.body.success).toBe(false); |
| 157 | + }); |
| 158 | +}); |
| 159 | + |
| 160 | +// wrapWithEnvelope 直接单测(不经子包 dist,确保 coverage 统计到 src/lib/adapters/utils) |
| 161 | +describe("wrapWithEnvelope 单元测试", () => { |
| 162 | + it("未注册任何 enveloper 时零开销退化(返回原 dispatch)", () => { |
| 163 | + const dispatch = vi.fn(); |
| 164 | + const wrapped = wrapWithEnvelope(dispatch as never, {}); |
| 165 | + expect(wrapped).toBe(dispatch); // 直接返回原 dispatch,不包装 |
| 166 | + }); |
| 167 | + |
| 168 | + it("handler return data → successEnveloper 包装写入 reply.json", async () => { |
| 169 | + const ctx = mockCtx(); |
| 170 | + ctx.__returned = true; |
| 171 | + ctx.__returnValue = { id: 1 }; |
| 172 | + const success = vi.fn((data: unknown) => ({ success: true, data })); |
| 173 | + |
| 174 | + await wrapWithEnvelope(async () => Promise.resolve(), { success })(ctx); |
| 175 | + |
| 176 | + expect(success).toHaveBeenCalledWith({ id: 1 }, ctx); |
| 177 | + expect(ctx.reply.json).toHaveBeenCalledWith({ success: true, data: { id: 1 } }); |
| 178 | + }); |
| 179 | + |
| 180 | + it("handler 未置 __returned(自行调 ctx.reply)→ 不二次包装", async () => { |
| 181 | + const ctx = mockCtx(); |
| 182 | + const success = vi.fn(); |
| 183 | + |
| 184 | + await wrapWithEnvelope(async () => Promise.resolve(), { success })(ctx); |
| 185 | + |
| 186 | + expect(success).not.toHaveBeenCalled(); |
| 187 | + expect(ctx.reply.json).not.toHaveBeenCalled(); |
| 188 | + }); |
| 189 | + |
| 190 | + it("handler 抛错 → errorEnveloper 包装 status + body", async () => { |
| 191 | + const ctx = mockCtx(); |
| 192 | + const boom = Object.assign(new Error("not found"), { statusCode: 404 }); |
| 193 | + const error = vi.fn(() => ({ body: { fail: true }, status: 404 })); |
| 194 | + |
| 195 | + await wrapWithEnvelope(async () => Promise.reject(boom), { error })(ctx); |
| 196 | + |
| 197 | + expect(error).toHaveBeenCalledWith(boom, ctx); |
| 198 | + expect(ctx.reply.status).toHaveBeenCalledWith(404); |
| 199 | + expect(ctx.reply.json).toHaveBeenCalledWith({ fail: true }); |
| 200 | + }); |
| 201 | + |
| 202 | + it("仅注册 success enveloper 时,抛错 re-throw(不吞错)", async () => { |
| 203 | + const ctx = mockCtx(); |
| 204 | + const success = vi.fn(); |
| 205 | + |
| 206 | + await expect(wrapWithEnvelope(async () => Promise.reject(new Error("escape")), { success })(ctx)).rejects.toThrow( |
| 207 | + "escape" |
| 208 | + ); |
| 209 | + expect(success).not.toHaveBeenCalled(); |
| 210 | + }); |
| 211 | + |
| 212 | + it("__returned=true 但 returnValue=undefined → successEnveloper 仍被调用", async () => { |
| 213 | + const ctx = mockCtx(); |
| 214 | + ctx.__returned = true; |
| 215 | + ctx.__returnValue = undefined; |
| 216 | + const success = vi.fn((data: unknown) => ({ ok: true, data })); |
| 217 | + |
| 218 | + await wrapWithEnvelope(async () => Promise.resolve(), { success })(ctx); |
| 219 | + |
| 220 | + expect(success).toHaveBeenCalledWith(undefined, ctx); |
| 221 | + expect(ctx.reply.json).toHaveBeenCalled(); |
| 222 | + }); |
83 | 223 | }); |
84 | 224 |
|
85 | 225 | afterAll(() => {}); |
0 commit comments