@mpen/routekit 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.mts +4 -0
- package/dist/client/react.d.mts +178 -0
- package/dist/client/react.mjs +142 -0
- package/dist/client.d.mts +433 -0
- package/dist/client.mjs +264 -0
- package/dist/content-BuDOmhH_.mjs +102 -0
- package/dist/core-CzUCxvGk.d.mts +140 -0
- package/dist/core-DbmQauwS.mjs +81 -0
- package/dist/handlers.d.mts +72 -0
- package/dist/handlers.mjs +153 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1152 -0
- package/dist/middleware.d.mts +388 -0
- package/dist/middleware.mjs +1222 -0
- package/dist/request-Dn0zc-xm.mjs +1025 -0
- package/dist/response/content.d.mts +79 -0
- package/dist/response/content.mjs +2 -0
- package/dist/response/json-rpc.d.mts +1 -0
- package/dist/response/json-rpc.mjs +1 -0
- package/dist/response/problem/valibot.d.mts +230 -0
- package/dist/response/problem/valibot.mjs +258 -0
- package/dist/response/problem.d.mts +415 -0
- package/dist/response/problem.mjs +183 -0
- package/dist/response/status.d.mts +45 -0
- package/dist/response/status.mjs +2 -0
- package/dist/responses-B379Ep9Y.d.mts +296 -0
- package/dist/responses-BpVrgeYi.mjs +101 -0
- package/dist/router-Cwb7ak0J.d.mts +1819 -0
- package/dist/routes.d.mts +282 -0
- package/dist/routes.mjs +311 -0
- package/dist/status-C-8mw-FB.mjs +59 -0
- package/dist/valibot-D7liFYyB.d.mts +290 -0
- package/dist/valibot-Du97X-TS.mjs +326 -0
- package/package.json +8 -2
- package/src/bin/gen-api-client.test.ts +0 -70
- package/src/bin/gen-api-client.ts +0 -986
- package/src/client/headers.ts +0 -31
- package/src/client/index.ts +0 -8
- package/src/client/promise.ts +0 -11
- package/src/client/react/index.test.tsx +0 -266
- package/src/client/react/index.ts +0 -431
- package/src/client/responses.test.ts +0 -151
- package/src/client/responses.ts +0 -278
- package/src/client/transport.ts +0 -74
- package/src/client/transports/body-codec.ts +0 -61
- package/src/client/transports/fetch.ts +0 -113
- package/src/client/tsconfig.json +0 -9
- package/src/client/types.ts +0 -15
- package/src/client/url.ts +0 -31
- package/src/index.ts +0 -63
- package/src/router/fetch-types.ts +0 -13
- package/src/router/handlers/index.ts +0 -2
- package/src/router/handlers/openapi/index.ts +0 -2
- package/src/router/handlers/openapi/openapi.ts +0 -293
- package/src/router/integration/zod-openapi.test.ts +0 -74
- package/src/router/lib/charset.test.ts +0 -22
- package/src/router/lib/charset.ts +0 -133
- package/src/router/lib/collections.ts +0 -3
- package/src/router/lib/format.test.ts +0 -67
- package/src/router/lib/format.ts +0 -35
- package/src/router/lib/host.ts +0 -4
- package/src/router/lib/json-schema.ts +0 -6
- package/src/router/lib/media-type.test.ts +0 -122
- package/src/router/lib/media-type.ts +0 -289
- package/src/router/lib/pathname.test.ts +0 -18
- package/src/router/lib/pathname.ts +0 -19
- package/src/router/lib/route-names.ts +0 -70
- package/src/router/lib/route-normalize.test.ts +0 -36
- package/src/router/lib/route-normalize.ts +0 -67
- package/src/router/lib/schema-merge.ts +0 -56
- package/src/router/middleware/accept-ctx.test.ts +0 -33
- package/src/router/middleware/accept-ctx.ts +0 -12
- package/src/router/middleware/body-limit.test.ts +0 -112
- package/src/router/middleware/body-limit.ts +0 -121
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +0 -269
- package/src/router/middleware/cors.ts +0 -490
- package/src/router/middleware/csrf.test.ts +0 -106
- package/src/router/middleware/csrf.ts +0 -192
- package/src/router/middleware/define.ts +0 -249
- package/src/router/middleware/index.ts +0 -34
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +0 -886
- package/src/router/middleware/rate-limit.ts +0 -920
- package/src/router/middleware/request-id-ctx.test.ts +0 -183
- package/src/router/middleware/request-id-ctx.ts +0 -135
- package/src/router/middleware/request-logger-format.test.ts +0 -16
- package/src/router/middleware/request-logger-format.ts +0 -269
- package/src/router/middleware/request-logger.test.ts +0 -267
- package/src/router/middleware/request-logger.ts +0 -131
- package/src/router/middleware/start-time-ctx.ts +0 -5
- package/src/router/request.ts +0 -611
- package/src/router/response/core.ts +0 -181
- package/src/router/response/directives.ts +0 -233
- package/src/router/response/formats/content/bodyless.ts +0 -54
- package/src/router/response/formats/content/content.ts +0 -79
- package/src/router/response/formats/content/index.ts +0 -2
- package/src/router/response/formats/json-rpc/index.ts +0 -2
- package/src/router/response/formats/problem/badRequest.ts +0 -90
- package/src/router/response/formats/problem/conflict.ts +0 -90
- package/src/router/response/formats/problem/created.ts +0 -40
- package/src/router/response/formats/problem/index.ts +0 -27
- package/src/router/response/formats/problem/notFound.ts +0 -90
- package/src/router/response/formats/problem/permissionDenied.ts +0 -90
- package/src/router/response/formats/problem/problem.test.ts +0 -888
- package/src/router/response/formats/problem/rateLimited.ts +0 -90
- package/src/router/response/formats/problem/responses.ts +0 -219
- package/src/router/response/formats/problem/root-errors.ts +0 -48
- package/src/router/response/formats/problem/sessionExpired.ts +0 -90
- package/src/router/response/formats/problem/types.ts +0 -170
- package/src/router/response/formats/problem/unauthenticated.ts +0 -90
- package/src/router/response/formats/problem/valibot.ts +0 -410
- package/src/router/response/formats/status/index.ts +0 -1
- package/src/router/response/formats/status/responses.ts +0 -59
- package/src/router/response/formats/status/status.test.ts +0 -21
- package/src/router/response/framers.ts +0 -85
- package/src/router/response/index.ts +0 -28
- package/src/router/response/openapi.test.ts +0 -96
- package/src/router/response/openapi.ts +0 -1
- package/src/router/response/serializers.ts +0 -66
- package/src/router/response/stream.ts +0 -35
- package/src/router/router.test.ts +0 -1571
- package/src/router/router.ts +0 -1965
- package/src/router/routes/index.ts +0 -46
- package/src/router/routes/valibot/index.ts +0 -18
- package/src/router/routes/valibot/valibot.ts +0 -1393
- package/src/router/routes/valibot.test.ts +0 -286
- package/src/router/routes/zod/index.ts +0 -18
- package/src/router/routes/zod/zod.ts +0 -1318
- package/src/router/routes/zod.test.ts +0 -280
- package/src/router/server-interface.ts +0 -31
- package/src/router/types.ts +0 -657
|
@@ -1,888 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { HttpMethod, HttpStatus } from '@mpen/http'
|
|
4
|
-
import type { LogActivity, Logger } from '@mpen/logger'
|
|
5
|
-
import { expectType, type TypeEqual } from '@mpen/ts-types'
|
|
6
|
-
import * as v from 'valibot'
|
|
7
|
-
import { Router } from '../../../router'
|
|
8
|
-
import {
|
|
9
|
-
badRequest,
|
|
10
|
-
conflict,
|
|
11
|
-
created,
|
|
12
|
-
notFound,
|
|
13
|
-
ok,
|
|
14
|
-
permissionDenied,
|
|
15
|
-
problem,
|
|
16
|
-
problemRootErrors,
|
|
17
|
-
rateLimited,
|
|
18
|
-
sessionExpired,
|
|
19
|
-
unauthenticated,
|
|
20
|
-
validationProblem,
|
|
21
|
-
} from './index'
|
|
22
|
-
import {
|
|
23
|
-
createValibotRouteBuilder,
|
|
24
|
-
issueFromValibotIssue,
|
|
25
|
-
issuesFromValibotIssues,
|
|
26
|
-
okSchema,
|
|
27
|
-
problemIssueSchema,
|
|
28
|
-
problemValidationErrorHandler,
|
|
29
|
-
problemSchema,
|
|
30
|
-
standardResponseSchema,
|
|
31
|
-
validationProblemFromValibotIssues,
|
|
32
|
-
} from './valibot'
|
|
33
|
-
import type { SuccessResponse, ProblemResponse } from './index'
|
|
34
|
-
|
|
35
|
-
type TestLogger = Logger & {
|
|
36
|
-
errorCalls: unknown[][]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function createTestLogger(): TestLogger {
|
|
40
|
-
const errorCalls: unknown[][] = []
|
|
41
|
-
const create = (): Logger => {
|
|
42
|
-
const logger: Logger = {
|
|
43
|
-
log() {},
|
|
44
|
-
info() {},
|
|
45
|
-
warn() {},
|
|
46
|
-
error(...data: unknown[]) {
|
|
47
|
-
errorCalls.push(data)
|
|
48
|
-
},
|
|
49
|
-
table() {},
|
|
50
|
-
withContext() {
|
|
51
|
-
return logger
|
|
52
|
-
},
|
|
53
|
-
startActivity(): LogActivity {
|
|
54
|
-
return {
|
|
55
|
-
logger,
|
|
56
|
-
end() {},
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
return logger
|
|
61
|
-
}
|
|
62
|
-
return Object.assign(create(), { errorCalls })
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
describe(ok.name, () => {
|
|
66
|
-
it('creates a successful standard response envelope', () => {
|
|
67
|
-
const result = ok(
|
|
68
|
-
{ id: 'user_123' },
|
|
69
|
-
{ headers: { 'x-request-id': 'req_123' }, meta: { requestId: 'req_123' } },
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
expect(result.status).toBe(HttpStatus.OK)
|
|
73
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
74
|
-
expect(result.body).toEqual({
|
|
75
|
-
success: true,
|
|
76
|
-
data: { id: 'user_123' },
|
|
77
|
-
meta: { requestId: 'req_123' },
|
|
78
|
-
})
|
|
79
|
-
expectType<
|
|
80
|
-
TypeEqual<
|
|
81
|
-
typeof result.body,
|
|
82
|
-
SuccessResponse<{ readonly id: 'user_123' }, { readonly requestId: 'req_123' }>
|
|
83
|
-
>
|
|
84
|
-
>(true)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe(created.name, () => {
|
|
89
|
-
it('creates a created standard response envelope', () => {
|
|
90
|
-
const result = created(
|
|
91
|
-
{ id: 'user_123' },
|
|
92
|
-
{ headers: { 'x-request-id': 'req_123' }, meta: { requestId: 'req_123' } },
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
expect(result.status).toBe(HttpStatus.CREATED)
|
|
96
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
97
|
-
expect(result.body).toEqual({
|
|
98
|
-
success: true,
|
|
99
|
-
data: { id: 'user_123' },
|
|
100
|
-
meta: { requestId: 'req_123' },
|
|
101
|
-
})
|
|
102
|
-
expectType<
|
|
103
|
-
TypeEqual<
|
|
104
|
-
typeof result.body,
|
|
105
|
-
SuccessResponse<{ readonly id: 'user_123' }, { readonly requestId: 'req_123' }>
|
|
106
|
-
>
|
|
107
|
-
>(true)
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe(problem.name, () => {
|
|
112
|
-
it('creates a problem response envelope', () => {
|
|
113
|
-
const result = problem({
|
|
114
|
-
code: 'not_found',
|
|
115
|
-
message: 'No user exists for the provided id.',
|
|
116
|
-
status: HttpStatus.NOT_FOUND,
|
|
117
|
-
title: 'User not found',
|
|
118
|
-
headers: { 'x-request-id': 'req_123' },
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
expect(result.status).toBe(HttpStatus.NOT_FOUND)
|
|
122
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
123
|
-
expect(result.body).toEqual({
|
|
124
|
-
success: false,
|
|
125
|
-
error: {
|
|
126
|
-
code: 'not_found',
|
|
127
|
-
message: 'No user exists for the provided id.',
|
|
128
|
-
title: 'User not found',
|
|
129
|
-
},
|
|
130
|
-
})
|
|
131
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'not_found'>>>(true)
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
describe(problemRootErrors.name, () => {
|
|
136
|
-
it('installs problem response handlers for router root errors', async () => {
|
|
137
|
-
const error = new Error('boom')
|
|
138
|
-
const logger = createTestLogger()
|
|
139
|
-
const router = new Router()
|
|
140
|
-
.setLogger(logger)
|
|
141
|
-
.install(problemRootErrors())
|
|
142
|
-
.post('/json', {
|
|
143
|
-
accept: ['application/json'],
|
|
144
|
-
handler: () => ok({ accepted: true }),
|
|
145
|
-
})
|
|
146
|
-
.get('/boom', () => {
|
|
147
|
-
throw error
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const cases = [
|
|
151
|
-
{
|
|
152
|
-
request: new Request('https://example.com/missing'),
|
|
153
|
-
status: HttpStatus.NOT_FOUND,
|
|
154
|
-
code: 'not_found',
|
|
155
|
-
message: 'Not found',
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
request: new Request('https://example.com/json', { method: HttpMethod.GET }),
|
|
159
|
-
status: HttpStatus.METHOD_NOT_ALLOWED,
|
|
160
|
-
code: 'method_not_allowed',
|
|
161
|
-
message: 'Method not allowed',
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
request: new Request('https://example.com/json', { method: HttpMethod.POST }),
|
|
165
|
-
status: HttpStatus.UNSUPPORTED_MEDIA_TYPE,
|
|
166
|
-
code: 'unsupported_media_type',
|
|
167
|
-
message: 'Unsupported media type',
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
request: new Request('https://example.com/boom'),
|
|
171
|
-
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
172
|
-
code: 'internal_server_error',
|
|
173
|
-
message: 'Internal server error',
|
|
174
|
-
},
|
|
175
|
-
] as const
|
|
176
|
-
|
|
177
|
-
for (const testCase of cases) {
|
|
178
|
-
const response = await router.fetch(testCase.request)
|
|
179
|
-
|
|
180
|
-
expect(response.status).toBe(testCase.status)
|
|
181
|
-
expect(response.headers.get('content-type')).toBe('application/json')
|
|
182
|
-
expect(await response.json()).toEqual({
|
|
183
|
-
success: false,
|
|
184
|
-
error: {
|
|
185
|
-
code: testCase.code,
|
|
186
|
-
message: testCase.message,
|
|
187
|
-
},
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
expect(logger.errorCalls).toHaveLength(1)
|
|
191
|
-
expect(logger.errorCalls[0]?.[1]).toBe(error)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('lets later root handlers override installed defaults', async () => {
|
|
195
|
-
const router = new Router()
|
|
196
|
-
.install(problemRootErrors())
|
|
197
|
-
.onNotFound(() => new Response('custom missing', { status: HttpStatus.NOT_FOUND }))
|
|
198
|
-
|
|
199
|
-
const response = await router.fetch(new Request('https://example.com/missing'))
|
|
200
|
-
|
|
201
|
-
expect(response.status).toBe(HttpStatus.NOT_FOUND)
|
|
202
|
-
expect(await response.text()).toBe('custom missing')
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
describe(notFound.name, () => {
|
|
207
|
-
it('creates a not found problem response envelope with string message', () => {
|
|
208
|
-
const result = notFound('Todo not found.', {
|
|
209
|
-
headers: { 'x-request-id': 'req_123' },
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
expect(result.status).toBe(HttpStatus.NOT_FOUND)
|
|
213
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
214
|
-
expect(result.body).toEqual({
|
|
215
|
-
success: false,
|
|
216
|
-
error: {
|
|
217
|
-
code: 'not_found',
|
|
218
|
-
message: 'Todo not found.',
|
|
219
|
-
},
|
|
220
|
-
})
|
|
221
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'not_found'>>>(true)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('creates a not found problem response envelope with options object', () => {
|
|
225
|
-
const result = notFound({
|
|
226
|
-
message: 'User not found.',
|
|
227
|
-
headers: { 'x-request-id': 'req_123' },
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
expect(result.status).toBe(HttpStatus.NOT_FOUND)
|
|
231
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
232
|
-
expect(result.body).toEqual({
|
|
233
|
-
success: false,
|
|
234
|
-
error: {
|
|
235
|
-
code: 'not_found',
|
|
236
|
-
message: 'User not found.',
|
|
237
|
-
},
|
|
238
|
-
})
|
|
239
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'not_found'>>>(true)
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('uses default message when none provided', () => {
|
|
243
|
-
const result = notFound()
|
|
244
|
-
|
|
245
|
-
expect(result.status).toBe(HttpStatus.NOT_FOUND)
|
|
246
|
-
expect(result.body).toEqual({
|
|
247
|
-
success: false,
|
|
248
|
-
error: {
|
|
249
|
-
code: 'not_found',
|
|
250
|
-
message: 'Not found',
|
|
251
|
-
},
|
|
252
|
-
})
|
|
253
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'not_found'>>>(true)
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('allows overriding code and status', () => {
|
|
257
|
-
const result = notFound({
|
|
258
|
-
code: 'todo_not_found',
|
|
259
|
-
status: HttpStatus.GONE,
|
|
260
|
-
message: 'This todo has been deleted.',
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
expect(result.status).toBe(HttpStatus.GONE)
|
|
264
|
-
expect(result.body).toEqual({
|
|
265
|
-
success: false,
|
|
266
|
-
error: {
|
|
267
|
-
code: 'todo_not_found',
|
|
268
|
-
message: 'This todo has been deleted.',
|
|
269
|
-
},
|
|
270
|
-
})
|
|
271
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'todo_not_found'>>>(true)
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
describe(permissionDenied.name, () => {
|
|
276
|
-
it('creates a permission denied problem response envelope with string message', () => {
|
|
277
|
-
const result = permissionDenied('You cannot access this.', {
|
|
278
|
-
headers: { 'x-request-id': 'req_123' },
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
expect(result.status).toBe(HttpStatus.FORBIDDEN)
|
|
282
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
283
|
-
expect(result.body).toEqual({
|
|
284
|
-
success: false,
|
|
285
|
-
error: {
|
|
286
|
-
code: 'permission_denied',
|
|
287
|
-
message: 'You cannot access this.',
|
|
288
|
-
},
|
|
289
|
-
})
|
|
290
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'permission_denied'>>>(true)
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('creates a permission denied problem response envelope with options object', () => {
|
|
294
|
-
const result = permissionDenied({
|
|
295
|
-
message: 'Insufficient privileges.',
|
|
296
|
-
headers: { 'x-request-id': 'req_123' },
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
expect(result.status).toBe(HttpStatus.FORBIDDEN)
|
|
300
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
301
|
-
expect(result.body).toEqual({
|
|
302
|
-
success: false,
|
|
303
|
-
error: {
|
|
304
|
-
code: 'permission_denied',
|
|
305
|
-
message: 'Insufficient privileges.',
|
|
306
|
-
},
|
|
307
|
-
})
|
|
308
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'permission_denied'>>>(true)
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it('uses default message when none provided', () => {
|
|
312
|
-
const result = permissionDenied()
|
|
313
|
-
|
|
314
|
-
expect(result.status).toBe(HttpStatus.FORBIDDEN)
|
|
315
|
-
expect(result.body).toEqual({
|
|
316
|
-
success: false,
|
|
317
|
-
error: {
|
|
318
|
-
code: 'permission_denied',
|
|
319
|
-
message: 'Permission denied',
|
|
320
|
-
},
|
|
321
|
-
})
|
|
322
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'permission_denied'>>>(true)
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
it('allows overriding code and status', () => {
|
|
326
|
-
const result = permissionDenied({
|
|
327
|
-
code: 'custom_denied',
|
|
328
|
-
status: HttpStatus.FORBIDDEN,
|
|
329
|
-
message: 'Custom permission error.',
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
expect(result.status).toBe(HttpStatus.FORBIDDEN)
|
|
333
|
-
expect(result.body).toEqual({
|
|
334
|
-
success: false,
|
|
335
|
-
error: {
|
|
336
|
-
code: 'custom_denied',
|
|
337
|
-
message: 'Custom permission error.',
|
|
338
|
-
},
|
|
339
|
-
})
|
|
340
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'custom_denied'>>>(true)
|
|
341
|
-
})
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
describe(unauthenticated.name, () => {
|
|
345
|
-
it('creates an unauthenticated problem response envelope with string message', () => {
|
|
346
|
-
const result = unauthenticated('Missing token.', {
|
|
347
|
-
headers: { 'x-request-id': 'req_123' },
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
expect(result.status).toBe(HttpStatus.UNAUTHORIZED)
|
|
351
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
352
|
-
expect(result.body).toEqual({
|
|
353
|
-
success: false,
|
|
354
|
-
error: {
|
|
355
|
-
code: 'unauthenticated',
|
|
356
|
-
message: 'Missing token.',
|
|
357
|
-
},
|
|
358
|
-
})
|
|
359
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'unauthenticated'>>>(true)
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('creates an unauthenticated problem response envelope with options object', () => {
|
|
363
|
-
const result = unauthenticated({
|
|
364
|
-
message: 'Invalid credentials.',
|
|
365
|
-
headers: { 'x-request-id': 'req_123' },
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
expect(result.status).toBe(HttpStatus.UNAUTHORIZED)
|
|
369
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
370
|
-
expect(result.body).toEqual({
|
|
371
|
-
success: false,
|
|
372
|
-
error: {
|
|
373
|
-
code: 'unauthenticated',
|
|
374
|
-
message: 'Invalid credentials.',
|
|
375
|
-
},
|
|
376
|
-
})
|
|
377
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'unauthenticated'>>>(true)
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
it('uses default message when none provided', () => {
|
|
381
|
-
const result = unauthenticated()
|
|
382
|
-
|
|
383
|
-
expect(result.status).toBe(HttpStatus.UNAUTHORIZED)
|
|
384
|
-
expect(result.body).toEqual({
|
|
385
|
-
success: false,
|
|
386
|
-
error: {
|
|
387
|
-
code: 'unauthenticated',
|
|
388
|
-
message: 'Unauthenticated',
|
|
389
|
-
},
|
|
390
|
-
})
|
|
391
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'unauthenticated'>>>(true)
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
it('allows overriding code and status', () => {
|
|
395
|
-
const result = unauthenticated({
|
|
396
|
-
code: 'invalid_auth',
|
|
397
|
-
status: HttpStatus.UNAUTHORIZED,
|
|
398
|
-
message: 'Auth failed.',
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
expect(result.status).toBe(HttpStatus.UNAUTHORIZED)
|
|
402
|
-
expect(result.body).toEqual({
|
|
403
|
-
success: false,
|
|
404
|
-
error: {
|
|
405
|
-
code: 'invalid_auth',
|
|
406
|
-
message: 'Auth failed.',
|
|
407
|
-
},
|
|
408
|
-
})
|
|
409
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'invalid_auth'>>>(true)
|
|
410
|
-
})
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
describe(sessionExpired.name, () => {
|
|
414
|
-
it('creates a session expired problem response envelope with string message', () => {
|
|
415
|
-
const result = sessionExpired('Session has timed out.', {
|
|
416
|
-
headers: { 'x-request-id': 'req_123' },
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
expect(result.status).toBe(HttpStatus.UNAUTHORIZED)
|
|
420
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
421
|
-
expect(result.body).toEqual({
|
|
422
|
-
success: false,
|
|
423
|
-
error: {
|
|
424
|
-
code: 'session_expired',
|
|
425
|
-
message: 'Session has timed out.',
|
|
426
|
-
},
|
|
427
|
-
})
|
|
428
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'session_expired'>>>(true)
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
it('creates a session expired problem response envelope with options object', () => {
|
|
432
|
-
const result = sessionExpired({
|
|
433
|
-
message: 'Session expired.',
|
|
434
|
-
headers: { 'x-request-id': 'req_123' },
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
expect(result.status).toBe(HttpStatus.UNAUTHORIZED)
|
|
438
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
439
|
-
expect(result.body).toEqual({
|
|
440
|
-
success: false,
|
|
441
|
-
error: {
|
|
442
|
-
code: 'session_expired',
|
|
443
|
-
message: 'Session expired.',
|
|
444
|
-
},
|
|
445
|
-
})
|
|
446
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'session_expired'>>>(true)
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
it('uses default message when none provided', () => {
|
|
450
|
-
const result = sessionExpired()
|
|
451
|
-
|
|
452
|
-
expect(result.status).toBe(HttpStatus.UNAUTHORIZED)
|
|
453
|
-
expect(result.body).toEqual({
|
|
454
|
-
success: false,
|
|
455
|
-
error: {
|
|
456
|
-
code: 'session_expired',
|
|
457
|
-
message: 'Session expired',
|
|
458
|
-
},
|
|
459
|
-
})
|
|
460
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'session_expired'>>>(true)
|
|
461
|
-
})
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
describe(badRequest.name, () => {
|
|
465
|
-
it('creates a bad request problem response envelope with string message', () => {
|
|
466
|
-
const result = badRequest('Invalid format.', {
|
|
467
|
-
headers: { 'x-request-id': 'req_123' },
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
expect(result.status).toBe(HttpStatus.BAD_REQUEST)
|
|
471
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
472
|
-
expect(result.body).toEqual({
|
|
473
|
-
success: false,
|
|
474
|
-
error: {
|
|
475
|
-
code: 'bad_request',
|
|
476
|
-
message: 'Invalid format.',
|
|
477
|
-
},
|
|
478
|
-
})
|
|
479
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'bad_request'>>>(true)
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
it('creates a bad request problem response envelope with options object', () => {
|
|
483
|
-
const result = badRequest({
|
|
484
|
-
message: 'Malformed payload.',
|
|
485
|
-
headers: { 'x-request-id': 'req_123' },
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
expect(result.status).toBe(HttpStatus.BAD_REQUEST)
|
|
489
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
490
|
-
expect(result.body).toEqual({
|
|
491
|
-
success: false,
|
|
492
|
-
error: {
|
|
493
|
-
code: 'bad_request',
|
|
494
|
-
message: 'Malformed payload.',
|
|
495
|
-
},
|
|
496
|
-
})
|
|
497
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'bad_request'>>>(true)
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
it('uses default message when none provided', () => {
|
|
501
|
-
const result = badRequest()
|
|
502
|
-
|
|
503
|
-
expect(result.status).toBe(HttpStatus.BAD_REQUEST)
|
|
504
|
-
expect(result.body).toEqual({
|
|
505
|
-
success: false,
|
|
506
|
-
error: {
|
|
507
|
-
code: 'bad_request',
|
|
508
|
-
message: 'Bad request',
|
|
509
|
-
},
|
|
510
|
-
})
|
|
511
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'bad_request'>>>(true)
|
|
512
|
-
})
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
describe(conflict.name, () => {
|
|
516
|
-
it('creates a conflict problem response envelope with string message', () => {
|
|
517
|
-
const result = conflict('Already exists.', {
|
|
518
|
-
headers: { 'x-request-id': 'req_123' },
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
expect(result.status).toBe(HttpStatus.CONFLICT)
|
|
522
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
523
|
-
expect(result.body).toEqual({
|
|
524
|
-
success: false,
|
|
525
|
-
error: {
|
|
526
|
-
code: 'conflict',
|
|
527
|
-
message: 'Already exists.',
|
|
528
|
-
},
|
|
529
|
-
})
|
|
530
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'conflict'>>>(true)
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
it('creates a conflict problem response envelope with options object', () => {
|
|
534
|
-
const result = conflict({
|
|
535
|
-
message: 'User already exists.',
|
|
536
|
-
headers: { 'x-request-id': 'req_123' },
|
|
537
|
-
})
|
|
538
|
-
|
|
539
|
-
expect(result.status).toBe(HttpStatus.CONFLICT)
|
|
540
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
541
|
-
expect(result.body).toEqual({
|
|
542
|
-
success: false,
|
|
543
|
-
error: {
|
|
544
|
-
code: 'conflict',
|
|
545
|
-
message: 'User already exists.',
|
|
546
|
-
},
|
|
547
|
-
})
|
|
548
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'conflict'>>>(true)
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
it('uses default message when none provided', () => {
|
|
552
|
-
const result = conflict()
|
|
553
|
-
|
|
554
|
-
expect(result.status).toBe(HttpStatus.CONFLICT)
|
|
555
|
-
expect(result.body).toEqual({
|
|
556
|
-
success: false,
|
|
557
|
-
error: {
|
|
558
|
-
code: 'conflict',
|
|
559
|
-
message: 'Conflict',
|
|
560
|
-
},
|
|
561
|
-
})
|
|
562
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'conflict'>>>(true)
|
|
563
|
-
})
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
describe(rateLimited.name, () => {
|
|
567
|
-
it('creates a rate limited problem response envelope with string message', () => {
|
|
568
|
-
const result = rateLimited('Too many requests.', {
|
|
569
|
-
headers: { 'x-request-id': 'req_123' },
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
expect(result.status).toBe(HttpStatus.TOO_MANY_REQUESTS)
|
|
573
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
574
|
-
expect(result.body).toEqual({
|
|
575
|
-
success: false,
|
|
576
|
-
error: {
|
|
577
|
-
code: 'rate_limited',
|
|
578
|
-
message: 'Too many requests.',
|
|
579
|
-
},
|
|
580
|
-
})
|
|
581
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'rate_limited'>>>(true)
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
it('creates a rate limited problem response envelope with options object', () => {
|
|
585
|
-
const result = rateLimited({
|
|
586
|
-
message: 'Rate limit exceeded.',
|
|
587
|
-
headers: { 'x-request-id': 'req_123' },
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
expect(result.status).toBe(HttpStatus.TOO_MANY_REQUESTS)
|
|
591
|
-
expect(result.headers.get('x-request-id')).toBe('req_123')
|
|
592
|
-
expect(result.body).toEqual({
|
|
593
|
-
success: false,
|
|
594
|
-
error: {
|
|
595
|
-
code: 'rate_limited',
|
|
596
|
-
message: 'Rate limit exceeded.',
|
|
597
|
-
},
|
|
598
|
-
})
|
|
599
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'rate_limited'>>>(true)
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
it('uses default message when none provided', () => {
|
|
603
|
-
const result = rateLimited()
|
|
604
|
-
|
|
605
|
-
expect(result.status).toBe(HttpStatus.TOO_MANY_REQUESTS)
|
|
606
|
-
expect(result.body).toEqual({
|
|
607
|
-
success: false,
|
|
608
|
-
error: {
|
|
609
|
-
code: 'rate_limited',
|
|
610
|
-
message: 'Rate limited',
|
|
611
|
-
},
|
|
612
|
-
})
|
|
613
|
-
expectType<TypeEqual<typeof result.body, ProblemResponse<'rate_limited'>>>(true)
|
|
614
|
-
})
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
describe(validationProblem.name, () => {
|
|
618
|
-
it('creates a validation problem response envelope', () => {
|
|
619
|
-
const result = validationProblem([
|
|
620
|
-
{
|
|
621
|
-
code: 'required',
|
|
622
|
-
message: 'Email is required',
|
|
623
|
-
path: ['body', 'email'],
|
|
624
|
-
},
|
|
625
|
-
])
|
|
626
|
-
|
|
627
|
-
expect(result.status).toBe(HttpStatus.BAD_REQUEST)
|
|
628
|
-
expect(result.body).toEqual({
|
|
629
|
-
success: false,
|
|
630
|
-
error: {
|
|
631
|
-
code: 'validation_failed',
|
|
632
|
-
message: 'Validation failed',
|
|
633
|
-
},
|
|
634
|
-
issues: [
|
|
635
|
-
{
|
|
636
|
-
code: 'required',
|
|
637
|
-
message: 'Email is required',
|
|
638
|
-
path: ['body', 'email'],
|
|
639
|
-
},
|
|
640
|
-
],
|
|
641
|
-
})
|
|
642
|
-
})
|
|
643
|
-
})
|
|
644
|
-
|
|
645
|
-
describe(problemIssueSchema.name, () => {
|
|
646
|
-
it('builds a reusable issue schema with typed codes', () => {
|
|
647
|
-
const schema = problemIssueSchema(v.picklist(['required', 'invalid_type']))
|
|
648
|
-
const parsed = v.parse(schema, {
|
|
649
|
-
code: 'required',
|
|
650
|
-
message: 'Email is required',
|
|
651
|
-
path: ['body', 'email'],
|
|
652
|
-
expected: 'string',
|
|
653
|
-
received: 'undefined',
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
expect(parsed).toEqual({
|
|
657
|
-
code: 'required',
|
|
658
|
-
message: 'Email is required',
|
|
659
|
-
path: ['body', 'email'],
|
|
660
|
-
expected: 'string',
|
|
661
|
-
received: 'undefined',
|
|
662
|
-
})
|
|
663
|
-
expectType<
|
|
664
|
-
TypeEqual<
|
|
665
|
-
typeof parsed,
|
|
666
|
-
{
|
|
667
|
-
code: 'required' | 'invalid_type'
|
|
668
|
-
message: string
|
|
669
|
-
path?: readonly (string | number)[] | undefined
|
|
670
|
-
expected?: string | undefined
|
|
671
|
-
received?: string | undefined
|
|
672
|
-
}
|
|
673
|
-
>
|
|
674
|
-
>(true)
|
|
675
|
-
})
|
|
676
|
-
})
|
|
677
|
-
|
|
678
|
-
describe(okSchema.name, () => {
|
|
679
|
-
it('builds a reusable success response schema', () => {
|
|
680
|
-
const schema = okSchema(v.object({ id: v.string() }), v.object({ requestId: v.string() }))
|
|
681
|
-
const parsed = v.parse(schema, {
|
|
682
|
-
success: true,
|
|
683
|
-
data: { id: 'user_123' },
|
|
684
|
-
meta: { requestId: 'req_123' },
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
expect(parsed).toEqual({
|
|
688
|
-
success: true,
|
|
689
|
-
data: { id: 'user_123' },
|
|
690
|
-
meta: { requestId: 'req_123' },
|
|
691
|
-
})
|
|
692
|
-
expectType<
|
|
693
|
-
TypeEqual<
|
|
694
|
-
typeof parsed,
|
|
695
|
-
{
|
|
696
|
-
success: true
|
|
697
|
-
data: { id: string }
|
|
698
|
-
meta?: { requestId: string } | undefined
|
|
699
|
-
}
|
|
700
|
-
>
|
|
701
|
-
>(true)
|
|
702
|
-
})
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
describe(standardResponseSchema.name, () => {
|
|
706
|
-
it('builds a reusable standard response union schema', () => {
|
|
707
|
-
const schema = standardResponseSchema(v.object({ id: v.string() }), {
|
|
708
|
-
code: v.picklist(['not_found', 'validation_failed']),
|
|
709
|
-
})
|
|
710
|
-
|
|
711
|
-
expect(v.parse(schema, { success: true, data: { id: 'user_123' } })).toEqual({
|
|
712
|
-
success: true,
|
|
713
|
-
data: { id: 'user_123' },
|
|
714
|
-
})
|
|
715
|
-
expect(
|
|
716
|
-
v.parse(schema, {
|
|
717
|
-
success: false,
|
|
718
|
-
error: {
|
|
719
|
-
code: 'not_found',
|
|
720
|
-
message: 'User not found',
|
|
721
|
-
},
|
|
722
|
-
}),
|
|
723
|
-
).toEqual({
|
|
724
|
-
success: false,
|
|
725
|
-
error: {
|
|
726
|
-
code: 'not_found',
|
|
727
|
-
message: 'User not found',
|
|
728
|
-
},
|
|
729
|
-
})
|
|
730
|
-
})
|
|
731
|
-
})
|
|
732
|
-
|
|
733
|
-
describe(issueFromValibotIssue.name, () => {
|
|
734
|
-
it('converts Valibot issues to standard response issues', () => {
|
|
735
|
-
const parsed = v.safeParse(v.object({ email: v.string() }), { email: 123 })
|
|
736
|
-
if (parsed.success) throw new Error('Expected parsing to fail')
|
|
737
|
-
|
|
738
|
-
const issue = issueFromValibotIssue(parsed.issues[0])
|
|
739
|
-
|
|
740
|
-
expect(issue).toMatchObject({
|
|
741
|
-
code: 'string',
|
|
742
|
-
path: ['email'],
|
|
743
|
-
expected: 'string',
|
|
744
|
-
})
|
|
745
|
-
expect(typeof issue.message).toBe('string')
|
|
746
|
-
expect(typeof issue.received).toBe('string')
|
|
747
|
-
})
|
|
748
|
-
})
|
|
749
|
-
|
|
750
|
-
describe(issuesFromValibotIssues.name, () => {
|
|
751
|
-
it('converts Valibot issue lists to standard response issues', () => {
|
|
752
|
-
const parsed = v.safeParse(v.object({ user: v.object({ email: v.string() }) }), {
|
|
753
|
-
user: { email: 123 },
|
|
754
|
-
})
|
|
755
|
-
if (parsed.success) throw new Error('Expected parsing to fail')
|
|
756
|
-
|
|
757
|
-
const issues = issuesFromValibotIssues(parsed.issues)
|
|
758
|
-
|
|
759
|
-
expect(issues).toHaveLength(1)
|
|
760
|
-
expect(issues[0]).toMatchObject({
|
|
761
|
-
code: 'string',
|
|
762
|
-
path: ['user', 'email'],
|
|
763
|
-
expected: 'string',
|
|
764
|
-
})
|
|
765
|
-
})
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
describe(validationProblemFromValibotIssues.name, () => {
|
|
769
|
-
it('creates validation problem responses from Valibot issues', () => {
|
|
770
|
-
const parsed = v.safeParse(v.object({ email: v.string() }), { email: 123 })
|
|
771
|
-
if (parsed.success) throw new Error('Expected parsing to fail')
|
|
772
|
-
|
|
773
|
-
const result = validationProblemFromValibotIssues(parsed.issues)
|
|
774
|
-
|
|
775
|
-
expect(result.status).toBe(HttpStatus.BAD_REQUEST)
|
|
776
|
-
expect(result.body.error.code).toBe('validation_failed')
|
|
777
|
-
expect(result.body.issues?.[0]).toMatchObject({
|
|
778
|
-
code: 'string',
|
|
779
|
-
path: ['email'],
|
|
780
|
-
})
|
|
781
|
-
})
|
|
782
|
-
})
|
|
783
|
-
|
|
784
|
-
describe(problemValidationErrorHandler.name, () => {
|
|
785
|
-
it('returns 422 and validation_failed:body for REQUEST_BODY component', () => {
|
|
786
|
-
const issues: [v.BaseIssue<unknown>, ...v.BaseIssue<unknown>[]] = [
|
|
787
|
-
{
|
|
788
|
-
kind: 'schema',
|
|
789
|
-
type: 'string',
|
|
790
|
-
input: 123,
|
|
791
|
-
expected: 'string',
|
|
792
|
-
received: 'number',
|
|
793
|
-
message: 'Expected a string',
|
|
794
|
-
},
|
|
795
|
-
]
|
|
796
|
-
const result = problemValidationErrorHandler(0, issues) // ValibotValidationError.REQUEST_BODY = 0
|
|
797
|
-
|
|
798
|
-
expect(result.status).toBe(HttpStatus.UNPROCESSABLE_ENTITY)
|
|
799
|
-
expect(result.body.error.code).toBe('validation_failed:body')
|
|
800
|
-
})
|
|
801
|
-
|
|
802
|
-
it('returns 400 and validation_failed:query for QUERY_PARAMETERS component', () => {
|
|
803
|
-
const issues: [v.BaseIssue<unknown>, ...v.BaseIssue<unknown>[]] = [
|
|
804
|
-
{
|
|
805
|
-
kind: 'schema',
|
|
806
|
-
type: 'string',
|
|
807
|
-
input: 123,
|
|
808
|
-
expected: 'string',
|
|
809
|
-
received: 'number',
|
|
810
|
-
message: 'Expected a string',
|
|
811
|
-
},
|
|
812
|
-
]
|
|
813
|
-
const result = problemValidationErrorHandler(2, issues) // ValibotValidationError.QUERY_PARAMETERS = 2
|
|
814
|
-
|
|
815
|
-
expect(result.status).toBe(HttpStatus.BAD_REQUEST)
|
|
816
|
-
expect(result.body.error.code).toBe('validation_failed:query')
|
|
817
|
-
})
|
|
818
|
-
})
|
|
819
|
-
|
|
820
|
-
describe(createValibotRouteBuilder.name, () => {
|
|
821
|
-
it('creates a builder that automatically includes problem error schemas for 400 and 422', () => {
|
|
822
|
-
const builder = createValibotRouteBuilder()
|
|
823
|
-
const routeOptions = builder({
|
|
824
|
-
schema: {
|
|
825
|
-
request: {
|
|
826
|
-
body: v.object({ title: v.string() }),
|
|
827
|
-
},
|
|
828
|
-
},
|
|
829
|
-
handler: () => ({ success: true }),
|
|
830
|
-
})
|
|
831
|
-
|
|
832
|
-
const router = new Router().get('/example', routeOptions)
|
|
833
|
-
const bodySchemas = router.getRoutes()[0]!.schema!.response!.body!
|
|
834
|
-
expect(bodySchemas).toBeDefined()
|
|
835
|
-
expect(bodySchemas[400]).toBeDefined()
|
|
836
|
-
expect(bodySchemas[422]).toBeDefined()
|
|
837
|
-
|
|
838
|
-
const schema400 = bodySchemas[400] as Record<string, unknown>
|
|
839
|
-
const schema422 = bodySchemas[422] as Record<string, unknown>
|
|
840
|
-
|
|
841
|
-
// 400 schema has anyOf representing query and path validation error types
|
|
842
|
-
expect(schema400.anyOf).toBeDefined()
|
|
843
|
-
// 422 schema has body validation error type
|
|
844
|
-
const errorSchema = (schema422.properties as Record<string, unknown>)?.error as Record<
|
|
845
|
-
string,
|
|
846
|
-
unknown
|
|
847
|
-
>
|
|
848
|
-
const codeSchema = (errorSchema?.properties as Record<string, unknown>)?.code as Record<
|
|
849
|
-
string,
|
|
850
|
-
unknown
|
|
851
|
-
>
|
|
852
|
-
expect(codeSchema?.const).toBe('validation_failed:body')
|
|
853
|
-
})
|
|
854
|
-
|
|
855
|
-
it('merges overlapping custom response schemas correctly with v.union', () => {
|
|
856
|
-
const builder = createValibotRouteBuilder()
|
|
857
|
-
const routeOptions = builder({
|
|
858
|
-
schema: {
|
|
859
|
-
response: {
|
|
860
|
-
body: {
|
|
861
|
-
422: problemSchema({ code: v.literal('custom_business_error') }),
|
|
862
|
-
},
|
|
863
|
-
},
|
|
864
|
-
},
|
|
865
|
-
handler: () => ({ success: true }),
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
const router = new Router().get('/example', routeOptions)
|
|
869
|
-
const bodySchemas = router.getRoutes()[0]!.schema!.response!.body!
|
|
870
|
-
expect(bodySchemas).toBeDefined()
|
|
871
|
-
expect(bodySchemas[422]).toBeDefined()
|
|
872
|
-
const schema422 = bodySchemas[422] as Record<string, unknown>
|
|
873
|
-
// Merged schema for 422 status code should have anyOf with both error types
|
|
874
|
-
expect(schema422.anyOf).toBeDefined()
|
|
875
|
-
const anyOf = schema422.anyOf as Record<string, unknown>[]
|
|
876
|
-
expect(anyOf).toHaveLength(2)
|
|
877
|
-
const codes = anyOf.map((s) => {
|
|
878
|
-
const err = (s.properties as Record<string, unknown>)?.error as Record<string, unknown>
|
|
879
|
-
const code = (err?.properties as Record<string, unknown>)?.code as Record<
|
|
880
|
-
string,
|
|
881
|
-
unknown
|
|
882
|
-
>
|
|
883
|
-
return code?.const
|
|
884
|
-
})
|
|
885
|
-
expect(codes).toContain('validation_failed:body')
|
|
886
|
-
expect(codes).toContain('custom_business_error')
|
|
887
|
-
})
|
|
888
|
-
})
|