node-fastify 5.8.3

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.
Files changed (354) hide show
  1. package/.borp.yaml +3 -0
  2. package/.markdownlint-cli2.yaml +22 -0
  3. package/.prettierignore +1 -0
  4. package/GOVERNANCE.md +4 -0
  5. package/LICENSE +21 -0
  6. package/PROJECT_CHARTER.md +126 -0
  7. package/README.md +423 -0
  8. package/SECURITY.md +220 -0
  9. package/SPONSORS.md +24 -0
  10. package/build/build-error-serializer.js +35 -0
  11. package/build/build-validation.js +169 -0
  12. package/build/sync-version.js +11 -0
  13. package/docs/Guides/Benchmarking.md +60 -0
  14. package/docs/Guides/Database.md +321 -0
  15. package/docs/Guides/Delay-Accepting-Requests.md +608 -0
  16. package/docs/Guides/Detecting-When-Clients-Abort.md +172 -0
  17. package/docs/Guides/Ecosystem.md +726 -0
  18. package/docs/Guides/Fluent-Schema.md +127 -0
  19. package/docs/Guides/Getting-Started.md +620 -0
  20. package/docs/Guides/Index.md +43 -0
  21. package/docs/Guides/Migration-Guide-V3.md +287 -0
  22. package/docs/Guides/Migration-Guide-V4.md +267 -0
  23. package/docs/Guides/Migration-Guide-V5.md +727 -0
  24. package/docs/Guides/Plugins-Guide.md +520 -0
  25. package/docs/Guides/Prototype-Poisoning.md +383 -0
  26. package/docs/Guides/Recommendations.md +378 -0
  27. package/docs/Guides/Serverless.md +604 -0
  28. package/docs/Guides/Style-Guide.md +246 -0
  29. package/docs/Guides/Testing.md +481 -0
  30. package/docs/Guides/Write-Plugin.md +103 -0
  31. package/docs/Guides/Write-Type-Provider.md +34 -0
  32. package/docs/Reference/ContentTypeParser.md +271 -0
  33. package/docs/Reference/Decorators.md +436 -0
  34. package/docs/Reference/Encapsulation.md +194 -0
  35. package/docs/Reference/Errors.md +377 -0
  36. package/docs/Reference/HTTP2.md +94 -0
  37. package/docs/Reference/Hooks.md +958 -0
  38. package/docs/Reference/Index.md +73 -0
  39. package/docs/Reference/LTS.md +86 -0
  40. package/docs/Reference/Lifecycle.md +99 -0
  41. package/docs/Reference/Logging.md +268 -0
  42. package/docs/Reference/Middleware.md +79 -0
  43. package/docs/Reference/Plugins.md +245 -0
  44. package/docs/Reference/Principles.md +73 -0
  45. package/docs/Reference/Reply.md +1001 -0
  46. package/docs/Reference/Request.md +295 -0
  47. package/docs/Reference/Routes.md +802 -0
  48. package/docs/Reference/Server.md +2389 -0
  49. package/docs/Reference/Type-Providers.md +256 -0
  50. package/docs/Reference/TypeScript.md +1729 -0
  51. package/docs/Reference/Validation-and-Serialization.md +1130 -0
  52. package/docs/Reference/Warnings.md +58 -0
  53. package/docs/index.md +24 -0
  54. package/docs/resources/encapsulation_context.drawio +1 -0
  55. package/docs/resources/encapsulation_context.svg +3 -0
  56. package/eslint.config.js +35 -0
  57. package/examples/asyncawait.js +38 -0
  58. package/examples/benchmark/body.json +3 -0
  59. package/examples/benchmark/hooks-benchmark-async-await.js +44 -0
  60. package/examples/benchmark/hooks-benchmark.js +52 -0
  61. package/examples/benchmark/parser.js +47 -0
  62. package/examples/benchmark/simple.js +30 -0
  63. package/examples/benchmark/webstream.js +27 -0
  64. package/examples/hooks.js +91 -0
  65. package/examples/http2.js +39 -0
  66. package/examples/https.js +38 -0
  67. package/examples/parser.js +53 -0
  68. package/examples/plugin.js +12 -0
  69. package/examples/route-prefix.js +38 -0
  70. package/examples/shared-schema.js +38 -0
  71. package/examples/simple-stream.js +20 -0
  72. package/examples/simple.js +32 -0
  73. package/examples/simple.mjs +27 -0
  74. package/examples/typescript-server.ts +79 -0
  75. package/examples/use-plugin.js +29 -0
  76. package/fastify.d.ts +253 -0
  77. package/fastify.js +985 -0
  78. package/integration/server.js +29 -0
  79. package/integration/test.sh +23 -0
  80. package/lib/config-validator.js +1266 -0
  81. package/lib/content-type-parser.js +413 -0
  82. package/lib/content-type.js +160 -0
  83. package/lib/context.js +98 -0
  84. package/lib/decorate.js +152 -0
  85. package/lib/error-handler.js +173 -0
  86. package/lib/error-serializer.js +134 -0
  87. package/lib/error-status.js +14 -0
  88. package/lib/errors.js +516 -0
  89. package/lib/four-oh-four.js +190 -0
  90. package/lib/handle-request.js +195 -0
  91. package/lib/head-route.js +45 -0
  92. package/lib/hooks.js +429 -0
  93. package/lib/initial-config-validation.js +37 -0
  94. package/lib/logger-factory.js +136 -0
  95. package/lib/logger-pino.js +68 -0
  96. package/lib/noop-set.js +10 -0
  97. package/lib/plugin-override.js +90 -0
  98. package/lib/plugin-utils.js +169 -0
  99. package/lib/promise.js +23 -0
  100. package/lib/reply.js +1030 -0
  101. package/lib/req-id-gen-factory.js +52 -0
  102. package/lib/request.js +391 -0
  103. package/lib/route.js +686 -0
  104. package/lib/schema-controller.js +164 -0
  105. package/lib/schemas.js +207 -0
  106. package/lib/server.js +441 -0
  107. package/lib/symbols.js +71 -0
  108. package/lib/validation.js +280 -0
  109. package/lib/warnings.js +57 -0
  110. package/lib/wrap-thenable.js +84 -0
  111. package/package.json +225 -0
  112. package/scripts/validate-ecosystem-links.js +179 -0
  113. package/test/404s.test.js +2035 -0
  114. package/test/500s.test.js +422 -0
  115. package/test/allow-unsafe-regex.test.js +92 -0
  116. package/test/als.test.js +65 -0
  117. package/test/async-await.test.js +705 -0
  118. package/test/async-dispose.test.js +20 -0
  119. package/test/async_hooks.test.js +52 -0
  120. package/test/body-limit.test.js +224 -0
  121. package/test/buffer.test.js +74 -0
  122. package/test/build/error-serializer.test.js +36 -0
  123. package/test/build/version.test.js +14 -0
  124. package/test/build-certificate.js +109 -0
  125. package/test/bundler/README.md +29 -0
  126. package/test/bundler/esbuild/bundler-test.js +32 -0
  127. package/test/bundler/esbuild/package.json +10 -0
  128. package/test/bundler/esbuild/src/fail-plugin-version.js +14 -0
  129. package/test/bundler/esbuild/src/index.js +9 -0
  130. package/test/bundler/webpack/bundler-test.js +32 -0
  131. package/test/bundler/webpack/package.json +11 -0
  132. package/test/bundler/webpack/src/fail-plugin-version.js +14 -0
  133. package/test/bundler/webpack/src/index.js +9 -0
  134. package/test/bundler/webpack/webpack.config.js +15 -0
  135. package/test/case-insensitive.test.js +102 -0
  136. package/test/chainable.test.js +40 -0
  137. package/test/child-logger-factory.test.js +128 -0
  138. package/test/client-timeout.test.js +38 -0
  139. package/test/close-pipelining.test.js +78 -0
  140. package/test/close.test.js +706 -0
  141. package/test/conditional-pino.test.js +47 -0
  142. package/test/connection-timeout.test.js +42 -0
  143. package/test/constrained-routes.test.js +1138 -0
  144. package/test/content-length.test.js +174 -0
  145. package/test/content-parser.test.js +739 -0
  146. package/test/content-type.test.js +181 -0
  147. package/test/context-config.test.js +164 -0
  148. package/test/custom-http-server.test.js +118 -0
  149. package/test/custom-parser-async.test.js +59 -0
  150. package/test/custom-parser.0.test.js +701 -0
  151. package/test/custom-parser.1.test.js +266 -0
  152. package/test/custom-parser.2.test.js +91 -0
  153. package/test/custom-parser.3.test.js +208 -0
  154. package/test/custom-parser.4.test.js +218 -0
  155. package/test/custom-parser.5.test.js +130 -0
  156. package/test/custom-querystring-parser.test.js +129 -0
  157. package/test/decorator.test.js +1330 -0
  158. package/test/delete.test.js +344 -0
  159. package/test/diagnostics-channel/404.test.js +49 -0
  160. package/test/diagnostics-channel/async-delay-request.test.js +65 -0
  161. package/test/diagnostics-channel/async-request.test.js +64 -0
  162. package/test/diagnostics-channel/error-before-handler.test.js +35 -0
  163. package/test/diagnostics-channel/error-request.test.js +53 -0
  164. package/test/diagnostics-channel/error-status.test.js +123 -0
  165. package/test/diagnostics-channel/init.test.js +50 -0
  166. package/test/diagnostics-channel/sync-delay-request.test.js +49 -0
  167. package/test/diagnostics-channel/sync-request-reply.test.js +51 -0
  168. package/test/diagnostics-channel/sync-request.test.js +54 -0
  169. package/test/encapsulated-child-logger-factory.test.js +69 -0
  170. package/test/encapsulated-error-handler.test.js +237 -0
  171. package/test/esm/errorCodes.test.mjs +10 -0
  172. package/test/esm/esm.test.mjs +13 -0
  173. package/test/esm/index.test.js +8 -0
  174. package/test/esm/named-exports.mjs +14 -0
  175. package/test/esm/other.mjs +8 -0
  176. package/test/esm/plugin.mjs +8 -0
  177. package/test/fastify-instance.test.js +300 -0
  178. package/test/find-route.test.js +152 -0
  179. package/test/fluent-schema.test.js +209 -0
  180. package/test/genReqId.test.js +426 -0
  181. package/test/handler-context.test.js +45 -0
  182. package/test/handler-timeout.test.js +367 -0
  183. package/test/has-route.test.js +88 -0
  184. package/test/header-overflow.test.js +55 -0
  185. package/test/helper.js +496 -0
  186. package/test/hooks-async.test.js +1099 -0
  187. package/test/hooks.on-listen.test.js +1162 -0
  188. package/test/hooks.on-ready.test.js +421 -0
  189. package/test/hooks.test.js +3578 -0
  190. package/test/http-methods/copy.test.js +35 -0
  191. package/test/http-methods/custom-http-methods.test.js +114 -0
  192. package/test/http-methods/get.test.js +412 -0
  193. package/test/http-methods/head.test.js +263 -0
  194. package/test/http-methods/lock.test.js +108 -0
  195. package/test/http-methods/mkcalendar.test.js +143 -0
  196. package/test/http-methods/mkcol.test.js +35 -0
  197. package/test/http-methods/move.test.js +42 -0
  198. package/test/http-methods/propfind.test.js +136 -0
  199. package/test/http-methods/proppatch.test.js +105 -0
  200. package/test/http-methods/report.test.js +142 -0
  201. package/test/http-methods/search.test.js +233 -0
  202. package/test/http-methods/trace.test.js +21 -0
  203. package/test/http-methods/unlock.test.js +38 -0
  204. package/test/http2/closing.test.js +270 -0
  205. package/test/http2/constraint.test.js +109 -0
  206. package/test/http2/head.test.js +34 -0
  207. package/test/http2/plain.test.js +68 -0
  208. package/test/http2/secure-with-fallback.test.js +113 -0
  209. package/test/http2/secure.test.js +67 -0
  210. package/test/http2/unknown-http-method.test.js +34 -0
  211. package/test/https/custom-https-server.test.js +58 -0
  212. package/test/https/https.test.js +136 -0
  213. package/test/imports.test.js +17 -0
  214. package/test/inject.test.js +502 -0
  215. package/test/input-validation.js +335 -0
  216. package/test/internals/all.test.js +38 -0
  217. package/test/internals/content-type-parser.test.js +111 -0
  218. package/test/internals/context.test.js +31 -0
  219. package/test/internals/decorator.test.js +156 -0
  220. package/test/internals/errors.test.js +982 -0
  221. package/test/internals/handle-request.test.js +270 -0
  222. package/test/internals/hook-runner.test.js +449 -0
  223. package/test/internals/hooks.test.js +96 -0
  224. package/test/internals/initial-config.test.js +383 -0
  225. package/test/internals/logger.test.js +163 -0
  226. package/test/internals/plugin.test.js +170 -0
  227. package/test/internals/promise.test.js +63 -0
  228. package/test/internals/reply-serialize.test.js +714 -0
  229. package/test/internals/reply.test.js +1920 -0
  230. package/test/internals/req-id-gen-factory.test.js +133 -0
  231. package/test/internals/request-validate.test.js +1402 -0
  232. package/test/internals/request.test.js +506 -0
  233. package/test/internals/schema-controller-perf.test.js +40 -0
  234. package/test/internals/server.test.js +91 -0
  235. package/test/internals/validation.test.js +352 -0
  236. package/test/issue-4959.test.js +118 -0
  237. package/test/keep-alive-timeout.test.js +42 -0
  238. package/test/listen.1.test.js +154 -0
  239. package/test/listen.2.test.js +113 -0
  240. package/test/listen.3.test.js +83 -0
  241. package/test/listen.4.test.js +168 -0
  242. package/test/listen.5.test.js +122 -0
  243. package/test/logger/instantiation.test.js +341 -0
  244. package/test/logger/logger-test-utils.js +47 -0
  245. package/test/logger/logging.test.js +460 -0
  246. package/test/logger/options.test.js +579 -0
  247. package/test/logger/request.test.js +292 -0
  248. package/test/logger/response.test.js +183 -0
  249. package/test/logger/tap-parallel-not-ok +0 -0
  250. package/test/max-requests-per-socket.test.js +113 -0
  251. package/test/middleware.test.js +37 -0
  252. package/test/noop-set.test.js +19 -0
  253. package/test/nullable-validation.test.js +187 -0
  254. package/test/options.error-handler.test.js +5 -0
  255. package/test/options.test.js +5 -0
  256. package/test/output-validation.test.js +140 -0
  257. package/test/patch.error-handler.test.js +5 -0
  258. package/test/patch.test.js +5 -0
  259. package/test/plugin.1.test.js +230 -0
  260. package/test/plugin.2.test.js +314 -0
  261. package/test/plugin.3.test.js +287 -0
  262. package/test/plugin.4.test.js +504 -0
  263. package/test/plugin.helper.js +8 -0
  264. package/test/plugin.name.display.js +10 -0
  265. package/test/post-empty-body.test.js +38 -0
  266. package/test/pretty-print.test.js +366 -0
  267. package/test/promises.test.js +125 -0
  268. package/test/proto-poisoning.test.js +145 -0
  269. package/test/put.error-handler.test.js +5 -0
  270. package/test/put.test.js +5 -0
  271. package/test/register.test.js +184 -0
  272. package/test/reply-code.test.js +148 -0
  273. package/test/reply-early-hints.test.js +100 -0
  274. package/test/reply-error.test.js +815 -0
  275. package/test/reply-trailers.test.js +445 -0
  276. package/test/reply-web-stream-locked.test.js +37 -0
  277. package/test/request-error.test.js +624 -0
  278. package/test/request-header-host.test.js +339 -0
  279. package/test/request-id.test.js +118 -0
  280. package/test/request-timeout.test.js +53 -0
  281. package/test/route-hooks.test.js +635 -0
  282. package/test/route-prefix.test.js +904 -0
  283. package/test/route-shorthand.test.js +48 -0
  284. package/test/route.1.test.js +259 -0
  285. package/test/route.2.test.js +100 -0
  286. package/test/route.3.test.js +213 -0
  287. package/test/route.4.test.js +127 -0
  288. package/test/route.5.test.js +211 -0
  289. package/test/route.6.test.js +306 -0
  290. package/test/route.7.test.js +406 -0
  291. package/test/route.8.test.js +225 -0
  292. package/test/router-options.test.js +1108 -0
  293. package/test/same-shape.test.js +124 -0
  294. package/test/schema-examples.test.js +661 -0
  295. package/test/schema-feature.test.js +2198 -0
  296. package/test/schema-serialization.test.js +1171 -0
  297. package/test/schema-special-usage.test.js +1348 -0
  298. package/test/schema-validation.test.js +1572 -0
  299. package/test/scripts/validate-ecosystem-links.test.js +339 -0
  300. package/test/serialize-response.test.js +186 -0
  301. package/test/server.test.js +347 -0
  302. package/test/set-error-handler.test.js +69 -0
  303. package/test/skip-reply-send.test.js +317 -0
  304. package/test/stream-serializers.test.js +40 -0
  305. package/test/stream.1.test.js +94 -0
  306. package/test/stream.2.test.js +129 -0
  307. package/test/stream.3.test.js +198 -0
  308. package/test/stream.4.test.js +176 -0
  309. package/test/stream.5.test.js +188 -0
  310. package/test/sync-routes.test.js +32 -0
  311. package/test/throw.test.js +359 -0
  312. package/test/toolkit.js +63 -0
  313. package/test/trust-proxy.test.js +162 -0
  314. package/test/type-provider.test.js +22 -0
  315. package/test/types/content-type-parser.test-d.ts +72 -0
  316. package/test/types/decorate-request-reply.test-d.ts +18 -0
  317. package/test/types/dummy-plugin.ts +9 -0
  318. package/test/types/errors.test-d.ts +90 -0
  319. package/test/types/fastify.test-d.ts +352 -0
  320. package/test/types/hooks.test-d.ts +550 -0
  321. package/test/types/import.ts +2 -0
  322. package/test/types/instance.test-d.ts +588 -0
  323. package/test/types/logger.test-d.ts +277 -0
  324. package/test/types/plugin.test-d.ts +97 -0
  325. package/test/types/register.test-d.ts +237 -0
  326. package/test/types/reply.test-d.ts +254 -0
  327. package/test/types/request.test-d.ts +188 -0
  328. package/test/types/route.test-d.ts +553 -0
  329. package/test/types/schema.test-d.ts +135 -0
  330. package/test/types/serverFactory.test-d.ts +37 -0
  331. package/test/types/type-provider.test-d.ts +1213 -0
  332. package/test/types/using.test-d.ts +17 -0
  333. package/test/upgrade.test.js +52 -0
  334. package/test/url-rewriting.test.js +122 -0
  335. package/test/use-semicolon-delimiter.test.js +168 -0
  336. package/test/validation-error-handling.test.js +900 -0
  337. package/test/versioned-routes.test.js +603 -0
  338. package/test/web-api.test.js +616 -0
  339. package/test/wrap-thenable.test.js +30 -0
  340. package/types/content-type-parser.d.ts +75 -0
  341. package/types/context.d.ts +22 -0
  342. package/types/errors.d.ts +92 -0
  343. package/types/hooks.d.ts +875 -0
  344. package/types/instance.d.ts +609 -0
  345. package/types/logger.d.ts +107 -0
  346. package/types/plugin.d.ts +44 -0
  347. package/types/register.d.ts +42 -0
  348. package/types/reply.d.ts +81 -0
  349. package/types/request.d.ts +95 -0
  350. package/types/route.d.ts +199 -0
  351. package/types/schema.d.ts +61 -0
  352. package/types/server-factory.d.ts +19 -0
  353. package/types/type-provider.d.ts +130 -0
  354. package/types/utils.d.ts +98 -0
@@ -0,0 +1,1348 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const Joi = require('joi')
5
+ const yup = require('yup')
6
+ const AJV = require('ajv')
7
+ const S = require('fluent-json-schema')
8
+ const Fastify = require('..')
9
+ const ajvMergePatch = require('ajv-merge-patch')
10
+ const ajvErrors = require('ajv-errors')
11
+ const proxyquire = require('proxyquire')
12
+ const { waitForCb } = require('./toolkit')
13
+
14
+ test('Ajv plugins array parameter', (t, testDone) => {
15
+ t.plan(3)
16
+ const fastify = Fastify({
17
+ ajv: {
18
+ customOptions: {
19
+ allErrors: true
20
+ },
21
+ plugins: [
22
+ [ajvErrors, { singleError: '@@@@' }]
23
+ ]
24
+ }
25
+ })
26
+
27
+ fastify.post('/', {
28
+ schema: {
29
+ body: {
30
+ type: 'object',
31
+ properties: {
32
+ foo: {
33
+ type: 'number',
34
+ minimum: 2,
35
+ maximum: 10,
36
+ multipleOf: 2,
37
+ errorMessage: {
38
+ type: 'should be number',
39
+ minimum: 'should be >= 2',
40
+ maximum: 'should be <= 10',
41
+ multipleOf: 'should be multipleOf 2'
42
+ }
43
+ }
44
+ }
45
+ }
46
+ },
47
+ handler (req, reply) { reply.send({ ok: 1 }) }
48
+ })
49
+
50
+ fastify.inject({
51
+ method: 'POST',
52
+ url: '/',
53
+ payload: { foo: 99 }
54
+ }, (err, res) => {
55
+ t.assert.ifError(err)
56
+ t.assert.strictEqual(res.statusCode, 400)
57
+ t.assert.strictEqual(res.json().message, 'body/foo should be <= 10@@@@should be multipleOf 2')
58
+ testDone()
59
+ })
60
+ })
61
+
62
+ test('Should handle root $merge keywords in header', (t, testDone) => {
63
+ t.plan(5)
64
+ const fastify = Fastify({
65
+ ajv: {
66
+ plugins: [
67
+ ajvMergePatch
68
+ ]
69
+ }
70
+ })
71
+
72
+ fastify.route({
73
+ method: 'GET',
74
+ url: '/',
75
+ schema: {
76
+ headers: {
77
+ $merge: {
78
+ source: {
79
+ type: 'object',
80
+ properties: {
81
+ q: { type: 'string' }
82
+ }
83
+ },
84
+ with: { required: ['q'] }
85
+ }
86
+ }
87
+ },
88
+ handler (req, reply) { reply.send({ ok: 1 }) }
89
+ })
90
+
91
+ fastify.ready(err => {
92
+ t.assert.ifError(err)
93
+
94
+ fastify.inject({
95
+ method: 'GET',
96
+ url: '/'
97
+ }, (err, res) => {
98
+ t.assert.ifError(err)
99
+ t.assert.strictEqual(res.statusCode, 400)
100
+ })
101
+
102
+ fastify.inject({
103
+ method: 'GET',
104
+ url: '/',
105
+ headers: { q: 'foo' }
106
+ }, (err, res) => {
107
+ t.assert.ifError(err)
108
+ t.assert.strictEqual(res.statusCode, 200)
109
+ testDone()
110
+ })
111
+ })
112
+ })
113
+
114
+ test('Should handle root $patch keywords in header', (t, testDone) => {
115
+ t.plan(5)
116
+ const fastify = Fastify({
117
+ ajv: {
118
+ plugins: [
119
+ ajvMergePatch
120
+ ]
121
+ }
122
+ })
123
+
124
+ fastify.route({
125
+ method: 'GET',
126
+ url: '/',
127
+ schema: {
128
+ headers: {
129
+ $patch: {
130
+ source: {
131
+ type: 'object',
132
+ properties: {
133
+ q: { type: 'string' }
134
+ }
135
+ },
136
+ with: [
137
+ {
138
+ op: 'add',
139
+ path: '/properties/q',
140
+ value: { type: 'number' }
141
+ }
142
+ ]
143
+ }
144
+ }
145
+ },
146
+ handler (req, reply) { reply.send({ ok: 1 }) }
147
+ })
148
+
149
+ fastify.ready(err => {
150
+ t.assert.ifError(err)
151
+
152
+ fastify.inject({
153
+ method: 'GET',
154
+ url: '/',
155
+ headers: {
156
+ q: 'foo'
157
+ }
158
+ }, (err, res) => {
159
+ t.assert.ifError(err)
160
+ t.assert.strictEqual(res.statusCode, 400)
161
+ })
162
+
163
+ fastify.inject({
164
+ method: 'GET',
165
+ url: '/',
166
+ headers: { q: 10 }
167
+ }, (err, res) => {
168
+ t.assert.ifError(err)
169
+ t.assert.strictEqual(res.statusCode, 200)
170
+ testDone()
171
+ })
172
+ })
173
+ })
174
+
175
+ test('Should handle $merge keywords in body', (t, testDone) => {
176
+ t.plan(5)
177
+ const fastify = Fastify({
178
+ ajv: {
179
+ plugins: [ajvMergePatch]
180
+ }
181
+ })
182
+
183
+ fastify.post('/', {
184
+ schema: {
185
+ body: {
186
+ $merge: {
187
+ source: {
188
+ type: 'object',
189
+ properties: {
190
+ q: {
191
+ type: 'string'
192
+ }
193
+ }
194
+ },
195
+ with: {
196
+ required: ['q']
197
+ }
198
+ }
199
+ }
200
+ },
201
+ handler (req, reply) { reply.send({ ok: 1 }) }
202
+ })
203
+
204
+ fastify.ready(err => {
205
+ t.assert.ifError(err)
206
+
207
+ fastify.inject({
208
+ method: 'POST',
209
+ url: '/'
210
+ }, (err, res) => {
211
+ t.assert.ifError(err)
212
+ t.assert.strictEqual(res.statusCode, 400)
213
+ })
214
+
215
+ fastify.inject({
216
+ method: 'POST',
217
+ url: '/',
218
+ payload: { q: 'foo' }
219
+ }, (err, res) => {
220
+ t.assert.ifError(err)
221
+ t.assert.strictEqual(res.statusCode, 200)
222
+ testDone()
223
+ })
224
+ })
225
+ })
226
+
227
+ test('Should handle $patch keywords in body', (t, testDone) => {
228
+ t.plan(5)
229
+ const fastify = Fastify({
230
+ ajv: {
231
+ plugins: [ajvMergePatch]
232
+ }
233
+ })
234
+
235
+ fastify.post('/', {
236
+ schema: {
237
+ body: {
238
+ $patch: {
239
+ source: {
240
+ type: 'object',
241
+ properties: {
242
+ q: {
243
+ type: 'string'
244
+ }
245
+ }
246
+ },
247
+ with: [
248
+ {
249
+ op: 'add',
250
+ path: '/properties/q',
251
+ value: { type: 'number' }
252
+ }
253
+ ]
254
+ }
255
+ }
256
+ },
257
+ handler (req, reply) { reply.send({ ok: 1 }) }
258
+ })
259
+
260
+ fastify.ready(err => {
261
+ t.assert.ifError(err)
262
+
263
+ const completion = waitForCb({ steps: 2 })
264
+ fastify.inject({
265
+ method: 'POST',
266
+ url: '/',
267
+ payload: { q: 'foo' }
268
+ }, (err, res) => {
269
+ t.assert.ifError(err)
270
+ t.assert.strictEqual(res.statusCode, 400)
271
+ completion.stepIn()
272
+ })
273
+ fastify.inject({
274
+ method: 'POST',
275
+ url: '/',
276
+ payload: { q: 10 }
277
+ }, (err, res) => {
278
+ t.assert.ifError(err)
279
+ t.assert.strictEqual(res.statusCode, 200)
280
+ completion.stepIn()
281
+ })
282
+ completion.patience.then(testDone)
283
+ })
284
+ })
285
+
286
+ test("serializer read validator's schemas", (t, testDone) => {
287
+ t.plan(4)
288
+ const ajvInstance = new AJV()
289
+
290
+ const baseSchema = {
291
+ $id: 'http://fastify.test/schemas/base',
292
+ definitions: {
293
+ hello: { type: 'string' }
294
+ },
295
+ type: 'object',
296
+ properties: {
297
+ hello: { $ref: '#/definitions/hello' }
298
+ }
299
+ }
300
+
301
+ const refSchema = {
302
+ $id: 'http://fastify.test/schemas/ref',
303
+ type: 'object',
304
+ properties: {
305
+ hello: { $ref: 'http://fastify.test/schemas/base#/definitions/hello' }
306
+ }
307
+ }
308
+
309
+ ajvInstance.addSchema(baseSchema)
310
+ ajvInstance.addSchema(refSchema)
311
+
312
+ const fastify = Fastify({
313
+ schemaController: {
314
+ bucket: function factory (storeInit) {
315
+ t.assert.ok(!storeInit, 'is always empty because fastify.addSchema is not called')
316
+ return {
317
+ getSchemas () {
318
+ return {
319
+ [baseSchema.$id]: ajvInstance.getSchema(baseSchema.$id).schema,
320
+ [refSchema.$id]: ajvInstance.getSchema(refSchema.$id).schema
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+ })
327
+
328
+ fastify.setValidatorCompiler(function ({ schema }) {
329
+ return ajvInstance.compile(schema)
330
+ })
331
+
332
+ fastify.get('/', {
333
+ schema: {
334
+ response: {
335
+ '2xx': ajvInstance.getSchema('http://fastify.test/schemas/ref').schema
336
+ }
337
+ },
338
+ handler (req, res) { res.send({ hello: 'world', evict: 'this' }) }
339
+ })
340
+
341
+ fastify.inject('/', (err, res) => {
342
+ t.assert.ifError(err)
343
+ t.assert.strictEqual(res.statusCode, 200)
344
+ t.assert.deepStrictEqual(res.json(), { hello: 'world' })
345
+ testDone()
346
+ })
347
+ })
348
+
349
+ test('setSchemaController in a plugin', (t, testDone) => {
350
+ t.plan(5)
351
+ const baseSchema = {
352
+ $id: 'urn:schema:base',
353
+ definitions: {
354
+ hello: { type: 'string' }
355
+ },
356
+ type: 'object',
357
+ properties: {
358
+ hello: { $ref: '#/definitions/hello' }
359
+ }
360
+ }
361
+
362
+ const refSchema = {
363
+ $id: 'urn:schema:ref',
364
+ type: 'object',
365
+ properties: {
366
+ hello: { $ref: 'urn:schema:base#/definitions/hello' }
367
+ }
368
+ }
369
+
370
+ const ajvInstance = new AJV()
371
+ ajvInstance.addSchema(baseSchema)
372
+ ajvInstance.addSchema(refSchema)
373
+
374
+ const fastify = Fastify({ exposeHeadRoutes: false })
375
+ fastify.register(schemaPlugin)
376
+ fastify.get('/', {
377
+ schema: {
378
+ query: ajvInstance.getSchema('urn:schema:ref').schema,
379
+ response: {
380
+ '2xx': ajvInstance.getSchema('urn:schema:ref').schema
381
+ }
382
+ },
383
+ handler (req, res) {
384
+ res.send({ hello: 'world', evict: 'this' })
385
+ }
386
+ })
387
+
388
+ fastify.inject('/', (err, res) => {
389
+ t.assert.ifError(err)
390
+ t.assert.strictEqual(res.statusCode, 200)
391
+ t.assert.deepStrictEqual(res.json(), { hello: 'world' })
392
+ testDone()
393
+ })
394
+
395
+ async function schemaPlugin (server) {
396
+ server.setSchemaController({
397
+ bucket () {
398
+ t.assert.ok('the bucket is created')
399
+ return {
400
+ addSchema (source) {
401
+ ajvInstance.addSchema(source)
402
+ },
403
+ getSchema (id) {
404
+ return ajvInstance.getSchema(id).schema
405
+ },
406
+ getSchemas () {
407
+ return {
408
+ 'urn:schema:base': baseSchema,
409
+ 'urn:schema:ref': refSchema
410
+ }
411
+ }
412
+ }
413
+ }
414
+ })
415
+ server.setValidatorCompiler(function ({ schema }) {
416
+ t.assert.ok('the querystring schema is compiled')
417
+ return ajvInstance.compile(schema)
418
+ })
419
+ }
420
+ schemaPlugin[Symbol.for('skip-override')] = true
421
+ })
422
+
423
+ test('side effect on schema let the server crash', async t => {
424
+ const firstSchema = {
425
+ $id: 'example1',
426
+ type: 'object',
427
+ properties: {
428
+ name: {
429
+ type: 'string'
430
+ }
431
+ }
432
+ }
433
+
434
+ const reusedSchema = {
435
+ $id: 'example2',
436
+ type: 'object',
437
+ properties: {
438
+ name: {
439
+ oneOf: [
440
+ {
441
+ $ref: 'example1'
442
+ }
443
+ ]
444
+ }
445
+ }
446
+ }
447
+
448
+ const fastify = Fastify()
449
+ fastify.addSchema(firstSchema)
450
+
451
+ fastify.post('/a', {
452
+ handler: async () => 'OK',
453
+ schema: {
454
+ body: reusedSchema,
455
+ response: { 200: reusedSchema }
456
+ }
457
+ })
458
+ fastify.post('/b', {
459
+ handler: async () => 'OK',
460
+ schema: {
461
+ body: reusedSchema,
462
+ response: { 200: reusedSchema }
463
+ }
464
+ })
465
+
466
+ await fastify.ready()
467
+ })
468
+
469
+ test('only response schema trigger AJV pollution', async t => {
470
+ const ShowSchema = S.object().id('ShowSchema').prop('name', S.string())
471
+ const ListSchema = S.array().id('ListSchema').items(S.ref('ShowSchema#'))
472
+
473
+ const fastify = Fastify()
474
+ fastify.addSchema(ListSchema)
475
+ fastify.addSchema(ShowSchema)
476
+
477
+ const routeResponseSchemas = {
478
+ schema: { response: { 200: S.ref('ListSchema#') } }
479
+ }
480
+
481
+ fastify.register(
482
+ async (app) => { app.get('/resource/', routeResponseSchemas, () => ({})) },
483
+ { prefix: '/prefix1' }
484
+ )
485
+ fastify.register(
486
+ async (app) => { app.get('/resource/', routeResponseSchemas, () => ({})) },
487
+ { prefix: '/prefix2' }
488
+ )
489
+
490
+ await fastify.ready()
491
+ })
492
+
493
+ test('only response schema trigger AJV pollution #2', async t => {
494
+ const ShowSchema = S.object().id('ShowSchema').prop('name', S.string())
495
+ const ListSchema = S.array().id('ListSchema').items(S.ref('ShowSchema#'))
496
+
497
+ const fastify = Fastify()
498
+ fastify.addSchema(ListSchema)
499
+ fastify.addSchema(ShowSchema)
500
+
501
+ const routeResponseSchemas = {
502
+ schema: {
503
+ params: S.ref('ListSchema#'),
504
+ response: { 200: S.ref('ListSchema#') }
505
+ }
506
+ }
507
+
508
+ fastify.register(
509
+ async (app) => { app.get('/resource/', routeResponseSchemas, () => ({})) },
510
+ { prefix: '/prefix1' }
511
+ )
512
+ fastify.register(
513
+ async (app) => { app.get('/resource/', routeResponseSchemas, () => ({})) },
514
+ { prefix: '/prefix2' }
515
+ )
516
+
517
+ await fastify.ready()
518
+ })
519
+
520
+ test('setSchemaController in a plugin with head routes', (t, testDone) => {
521
+ t.plan(6)
522
+ const baseSchema = {
523
+ $id: 'urn:schema:base',
524
+ definitions: {
525
+ hello: { type: 'string' }
526
+ },
527
+ type: 'object',
528
+ properties: {
529
+ hello: { $ref: '#/definitions/hello' }
530
+ }
531
+ }
532
+
533
+ const refSchema = {
534
+ $id: 'urn:schema:ref',
535
+ type: 'object',
536
+ properties: {
537
+ hello: { $ref: 'urn:schema:base#/definitions/hello' }
538
+ }
539
+ }
540
+
541
+ const ajvInstance = new AJV()
542
+ ajvInstance.addSchema(baseSchema)
543
+ ajvInstance.addSchema(refSchema)
544
+
545
+ const fastify = Fastify({ exposeHeadRoutes: true })
546
+ fastify.register(schemaPlugin)
547
+ fastify.get('/', {
548
+ schema: {
549
+ query: ajvInstance.getSchema('urn:schema:ref').schema,
550
+ response: {
551
+ '2xx': ajvInstance.getSchema('urn:schema:ref').schema
552
+ }
553
+ },
554
+ handler (req, res) {
555
+ res.send({ hello: 'world', evict: 'this' })
556
+ }
557
+ })
558
+
559
+ fastify.inject('/', (err, res) => {
560
+ t.assert.ifError(err)
561
+ t.assert.strictEqual(res.statusCode, 200)
562
+ t.assert.deepStrictEqual(res.json(), { hello: 'world' })
563
+ testDone()
564
+ })
565
+
566
+ async function schemaPlugin (server) {
567
+ server.setSchemaController({
568
+ bucket () {
569
+ t.assert.ok('the bucket is created')
570
+ return {
571
+ addSchema (source) {
572
+ ajvInstance.addSchema(source)
573
+ },
574
+ getSchema (id) {
575
+ return ajvInstance.getSchema(id).schema
576
+ },
577
+ getSchemas () {
578
+ return {
579
+ 'urn:schema:base': baseSchema,
580
+ 'urn:schema:ref': refSchema
581
+ }
582
+ }
583
+ }
584
+ }
585
+ })
586
+ server.setValidatorCompiler(function ({ schema }) {
587
+ if (schema.$id) {
588
+ const stored = ajvInstance.getSchema(schema.$id)
589
+ if (stored) {
590
+ t.assert.ok('the schema is reused')
591
+ return stored
592
+ }
593
+ }
594
+ t.assert.ok('the schema is compiled')
595
+
596
+ return ajvInstance.compile(schema)
597
+ })
598
+ }
599
+ schemaPlugin[Symbol.for('skip-override')] = true
600
+ })
601
+
602
+ test('multiple refs with the same ids', (t, testDone) => {
603
+ t.plan(3)
604
+ const baseSchema = {
605
+ $id: 'urn:schema:base',
606
+ definitions: {
607
+ hello: { type: 'string' }
608
+ },
609
+ type: 'object',
610
+ properties: {
611
+ hello: { $ref: '#/definitions/hello' }
612
+ }
613
+ }
614
+
615
+ const refSchema = {
616
+ $id: 'urn:schema:ref',
617
+ type: 'object',
618
+ properties: {
619
+ hello: { $ref: 'urn:schema:base#/definitions/hello' }
620
+ }
621
+ }
622
+
623
+ const fastify = Fastify()
624
+
625
+ fastify.addSchema(baseSchema)
626
+ fastify.addSchema(refSchema)
627
+
628
+ fastify.head('/', {
629
+ schema: {
630
+ query: refSchema,
631
+ response: {
632
+ '2xx': refSchema
633
+ }
634
+ },
635
+ handler (req, res) {
636
+ res.send({ hello: 'world', evict: 'this' })
637
+ }
638
+ })
639
+
640
+ fastify.get('/', {
641
+ schema: {
642
+ query: refSchema,
643
+ response: {
644
+ '2xx': refSchema
645
+ }
646
+ },
647
+ handler (req, res) {
648
+ res.send({ hello: 'world', evict: 'this' })
649
+ }
650
+ })
651
+
652
+ fastify.inject('/', (err, res) => {
653
+ t.assert.ifError(err)
654
+ t.assert.strictEqual(res.statusCode, 200)
655
+ t.assert.deepStrictEqual(res.json(), { hello: 'world' })
656
+ testDone()
657
+ })
658
+ })
659
+
660
+ test('JOI validation overwrite request headers', (t, testDone) => {
661
+ t.plan(3)
662
+ const schemaValidator = ({ schema }) => data => {
663
+ const validationResult = schema.validate(data)
664
+ return validationResult
665
+ }
666
+
667
+ const fastify = Fastify()
668
+ fastify.setValidatorCompiler(schemaValidator)
669
+
670
+ fastify.get('/', {
671
+ schema: {
672
+ headers: Joi.object({
673
+ 'user-agent': Joi.string().required(),
674
+ host: Joi.string().required()
675
+ })
676
+ }
677
+ }, (request, reply) => {
678
+ reply.send(request.headers)
679
+ })
680
+
681
+ fastify.inject('/', (err, res) => {
682
+ t.assert.ifError(err)
683
+ t.assert.strictEqual(res.statusCode, 200)
684
+ t.assert.deepStrictEqual(res.json(), {
685
+ 'user-agent': 'lightMyRequest',
686
+ host: 'localhost:80'
687
+ })
688
+ testDone()
689
+ })
690
+ })
691
+
692
+ test('Custom schema object should not trigger FST_ERR_SCH_DUPLICATE', async t => {
693
+ const fastify = Fastify()
694
+ const handler = () => { }
695
+
696
+ fastify.get('/the/url', {
697
+ schema: {
698
+ query: yup.object({
699
+ foo: yup.string()
700
+ })
701
+ },
702
+ validatorCompiler: ({ schema, method, url, httpPart }) => {
703
+ return function (data) {
704
+ // with option strict = false, yup `validateSync` function returns the coerced value if validation was successful, or throws if validation failed
705
+ try {
706
+ const result = schema.validateSync(data, {})
707
+ return { value: result }
708
+ } catch (e) {
709
+ return { error: e }
710
+ }
711
+ }
712
+ },
713
+ handler
714
+ })
715
+
716
+ await fastify.ready()
717
+ t.assert.ok('fastify is ready')
718
+ })
719
+
720
+ test('The default schema compilers should not be called when overwritten by the user', async t => {
721
+ const Fastify = proxyquire('../', {
722
+ '@fastify/ajv-compiler': () => {
723
+ t.assert.fail('The default validator compiler should not be called')
724
+ },
725
+ '@fastify/fast-json-stringify-compiler': () => {
726
+ t.assert.fail('The default serializer compiler should not be called')
727
+ }
728
+ })
729
+
730
+ const fastify = Fastify({
731
+ schemaController: {
732
+ compilersFactory: {
733
+ buildValidator: function factory () {
734
+ t.assert.ok('The custom validator compiler should be called')
735
+ return function validatorCompiler () {
736
+ return () => { return true }
737
+ }
738
+ },
739
+ buildSerializer: function factory () {
740
+ t.assert.ok('The custom serializer compiler should be called')
741
+ return function serializerCompiler () {
742
+ return () => { return true }
743
+ }
744
+ }
745
+ }
746
+ }
747
+ })
748
+
749
+ fastify.get('/',
750
+ {
751
+ schema: {
752
+ query: { foo: { type: 'string' } },
753
+ response: {
754
+ 200: { type: 'object' }
755
+ }
756
+ }
757
+ }, () => { })
758
+
759
+ await fastify.ready()
760
+ })
761
+
762
+ test('Supports async JOI validation', (t, testDone) => {
763
+ t.plan(7)
764
+
765
+ const schemaValidator = ({ schema }) => async data => {
766
+ const validationResult = await schema.validateAsync(data)
767
+ return validationResult
768
+ }
769
+
770
+ const fastify = Fastify({
771
+ exposeHeadRoutes: false
772
+ })
773
+ fastify.setValidatorCompiler(schemaValidator)
774
+
775
+ fastify.get('/', {
776
+ schema: {
777
+ headers: Joi.object({
778
+ 'user-agent': Joi.string().external(async (val) => {
779
+ if (val !== 'lightMyRequest') {
780
+ throw new Error('Invalid user-agent')
781
+ }
782
+
783
+ t.assert.strictEqual(val, 'lightMyRequest')
784
+ return val
785
+ }),
786
+ host: Joi.string().required()
787
+ })
788
+ }
789
+ }, (request, reply) => {
790
+ reply.send(request.headers)
791
+ })
792
+
793
+ const completion = waitForCb({ steps: 2 })
794
+ fastify.inject('/', (err, res) => {
795
+ t.assert.ifError(err)
796
+ t.assert.strictEqual(res.statusCode, 200)
797
+ t.assert.deepStrictEqual(res.json(), {
798
+ 'user-agent': 'lightMyRequest',
799
+ host: 'localhost:80'
800
+ })
801
+ completion.stepIn()
802
+ })
803
+ fastify.inject({
804
+ url: '/',
805
+ headers: {
806
+ 'user-agent': 'invalid'
807
+ }
808
+ }, (err, res) => {
809
+ t.assert.ifError(err)
810
+ t.assert.strictEqual(res.statusCode, 400)
811
+ t.assert.deepStrictEqual(res.json(), {
812
+ statusCode: 400,
813
+ code: 'FST_ERR_VALIDATION',
814
+ error: 'Bad Request',
815
+ message: 'Invalid user-agent (user-agent)'
816
+ })
817
+ completion.stepIn()
818
+ })
819
+
820
+ completion.patience.then(testDone)
821
+ })
822
+
823
+ test('Supports async AJV validation', (t, testDone) => {
824
+ t.plan(12)
825
+
826
+ const fastify = Fastify({
827
+ exposeHeadRoutes: false,
828
+ ajv: {
829
+ customOptions: {
830
+ allErrors: true,
831
+ keywords: [
832
+ {
833
+ keyword: 'idExists',
834
+ async: true,
835
+ type: 'number',
836
+ validate: checkIdExists
837
+ }
838
+ ]
839
+ },
840
+ plugins: [
841
+ [ajvErrors, { singleError: '@@@@' }]
842
+ ]
843
+ }
844
+ })
845
+
846
+ async function checkIdExists (schema, data) {
847
+ const res = await Promise.resolve(data)
848
+ switch (res) {
849
+ case 42:
850
+ return true
851
+
852
+ case 500:
853
+ throw new Error('custom error')
854
+
855
+ default:
856
+ return false
857
+ }
858
+ }
859
+
860
+ const schema = {
861
+ $async: true,
862
+ type: 'object',
863
+ properties: {
864
+ userId: {
865
+ type: 'integer',
866
+ idExists: { table: 'users' }
867
+ },
868
+ postId: {
869
+ type: 'integer',
870
+ idExists: { table: 'posts' }
871
+ }
872
+ }
873
+ }
874
+
875
+ fastify.post('/', {
876
+ schema: {
877
+ body: schema
878
+ },
879
+ handler (req, reply) { reply.send(req.body) }
880
+ })
881
+
882
+ const completion = waitForCb({ steps: 4 })
883
+
884
+ fastify.inject({
885
+ method: 'POST',
886
+ url: '/',
887
+ payload: { userId: 99 }
888
+ }, (err, res) => {
889
+ t.assert.ifError(err)
890
+ t.assert.strictEqual(res.statusCode, 400)
891
+ t.assert.deepStrictEqual(res.json(), {
892
+ statusCode: 400,
893
+ code: 'FST_ERR_VALIDATION',
894
+ error: 'Bad Request',
895
+ message: 'validation failed'
896
+ })
897
+ completion.stepIn()
898
+ })
899
+ fastify.inject({
900
+ method: 'POST',
901
+ url: '/',
902
+ payload: { userId: 500 }
903
+ }, (err, res) => {
904
+ t.assert.ifError(err)
905
+ t.assert.strictEqual(res.statusCode, 400)
906
+ t.assert.deepStrictEqual(res.json(), {
907
+ statusCode: 400,
908
+ code: 'FST_ERR_VALIDATION',
909
+ error: 'Bad Request',
910
+ message: 'custom error'
911
+ })
912
+ completion.stepIn()
913
+ })
914
+ fastify.inject({
915
+ method: 'POST',
916
+ url: '/',
917
+ payload: { userId: 42 }
918
+ }, (err, res) => {
919
+ t.assert.ifError(err)
920
+ t.assert.strictEqual(res.statusCode, 200)
921
+ t.assert.deepStrictEqual(res.json(), { userId: 42 })
922
+ completion.stepIn()
923
+ })
924
+ fastify.inject({
925
+ method: 'POST',
926
+ url: '/',
927
+ payload: { userId: 42, postId: 19 }
928
+ }, (err, res) => {
929
+ t.assert.ifError(err)
930
+ t.assert.strictEqual(res.statusCode, 400)
931
+ t.assert.deepStrictEqual(res.json(), {
932
+ statusCode: 400,
933
+ code: 'FST_ERR_VALIDATION',
934
+ error: 'Bad Request',
935
+ message: 'validation failed'
936
+ })
937
+ completion.stepIn()
938
+ })
939
+ completion.patience.then(testDone)
940
+ })
941
+
942
+ test('Check all the async AJV validation paths', async (t) => {
943
+ const fastify = Fastify({
944
+ exposeHeadRoutes: false,
945
+ ajv: {
946
+ customOptions: {
947
+ allErrors: true,
948
+ keywords: [
949
+ {
950
+ keyword: 'idExists',
951
+ async: true,
952
+ type: 'number',
953
+ validate: checkIdExists
954
+ }
955
+ ]
956
+ }
957
+ }
958
+ })
959
+
960
+ async function checkIdExists (schema, data) {
961
+ const res = await Promise.resolve(data)
962
+ switch (res) {
963
+ case 200:
964
+ return true
965
+
966
+ default:
967
+ return false
968
+ }
969
+ }
970
+
971
+ const schema = {
972
+ $async: true,
973
+ type: 'object',
974
+ properties: {
975
+ id: {
976
+ type: 'integer',
977
+ idExists: { table: 'posts' }
978
+ }
979
+ }
980
+ }
981
+
982
+ fastify.post('/:id', {
983
+ schema: {
984
+ params: schema,
985
+ body: schema,
986
+ query: schema,
987
+ headers: schema
988
+ },
989
+ handler (req, reply) { reply.send(req.body) }
990
+ })
991
+
992
+ const testCases = [
993
+ {
994
+ params: 400,
995
+ body: 200,
996
+ querystring: 200,
997
+ headers: 200,
998
+ response: 400
999
+ },
1000
+ {
1001
+ params: 200,
1002
+ body: 400,
1003
+ querystring: 200,
1004
+ headers: 200,
1005
+ response: 400
1006
+ },
1007
+ {
1008
+ params: 200,
1009
+ body: 200,
1010
+ querystring: 400,
1011
+ headers: 200,
1012
+ response: 400
1013
+ },
1014
+ {
1015
+ params: 200,
1016
+ body: 200,
1017
+ querystring: 200,
1018
+ headers: 400,
1019
+ response: 400
1020
+ },
1021
+ {
1022
+ params: 200,
1023
+ body: 200,
1024
+ querystring: 200,
1025
+ headers: 200,
1026
+ response: 200
1027
+ }
1028
+ ]
1029
+ t.plan(testCases.length)
1030
+ for (const testCase of testCases) {
1031
+ await validate(testCase)
1032
+ }
1033
+
1034
+ async function validate ({
1035
+ params,
1036
+ body,
1037
+ querystring,
1038
+ headers,
1039
+ response
1040
+ }) {
1041
+ try {
1042
+ const res = await fastify.inject({
1043
+ method: 'POST',
1044
+ url: `/${params}`,
1045
+ headers: { id: headers },
1046
+ query: { id: querystring },
1047
+ payload: { id: body }
1048
+ })
1049
+ t.assert.strictEqual(res.statusCode, response)
1050
+ } catch (error) {
1051
+ t.assert.fail('should not throw')
1052
+ }
1053
+ }
1054
+ })
1055
+
1056
+ test('Check mixed sync and async AJV validations', async (t) => {
1057
+ const fastify = Fastify({
1058
+ exposeHeadRoutes: false,
1059
+ ajv: {
1060
+ customOptions: {
1061
+ allErrors: true,
1062
+ keywords: [
1063
+ {
1064
+ keyword: 'idExists',
1065
+ async: true,
1066
+ type: 'number',
1067
+ validate: checkIdExists
1068
+ }
1069
+ ]
1070
+ }
1071
+ }
1072
+ })
1073
+
1074
+ async function checkIdExists (schema, data) {
1075
+ const res = await Promise.resolve(data)
1076
+ switch (res) {
1077
+ case 200:
1078
+ return true
1079
+
1080
+ default:
1081
+ return false
1082
+ }
1083
+ }
1084
+
1085
+ const schemaSync = {
1086
+ type: 'object',
1087
+ properties: {
1088
+ id: { type: 'integer' }
1089
+ }
1090
+ }
1091
+
1092
+ const schemaAsync = {
1093
+ $async: true,
1094
+ type: 'object',
1095
+ properties: {
1096
+ id: {
1097
+ type: 'integer',
1098
+ idExists: { table: 'posts' }
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+ fastify.post('/queryAsync/:id', {
1104
+ schema: {
1105
+ params: schemaSync,
1106
+ body: schemaSync,
1107
+ query: schemaAsync,
1108
+ headers: schemaSync
1109
+ },
1110
+ handler (req, reply) { reply.send(req.body) }
1111
+ })
1112
+
1113
+ fastify.post('/paramsAsync/:id', {
1114
+ schema: {
1115
+ params: schemaAsync,
1116
+ body: schemaSync
1117
+ },
1118
+ handler (req, reply) { reply.send(req.body) }
1119
+ })
1120
+
1121
+ fastify.post('/bodyAsync/:id', {
1122
+ schema: {
1123
+ params: schemaAsync,
1124
+ body: schemaAsync,
1125
+ query: schemaSync
1126
+ },
1127
+ handler (req, reply) { reply.send(req.body) }
1128
+ })
1129
+
1130
+ fastify.post('/headersSync/:id', {
1131
+ schema: {
1132
+ params: schemaSync,
1133
+ body: schemaSync,
1134
+ query: schemaAsync,
1135
+ headers: schemaSync
1136
+ },
1137
+ handler (req, reply) { reply.send(req.body) }
1138
+ })
1139
+
1140
+ fastify.post('/noHeader/:id', {
1141
+ schema: {
1142
+ params: schemaSync,
1143
+ body: schemaSync,
1144
+ query: schemaAsync
1145
+ },
1146
+ handler (req, reply) { reply.send(req.body) }
1147
+ })
1148
+
1149
+ fastify.post('/noBody/:id', {
1150
+ schema: {
1151
+ params: schemaSync,
1152
+ query: schemaAsync,
1153
+ headers: schemaSync
1154
+ },
1155
+ handler (req, reply) { reply.send(req.body) }
1156
+ })
1157
+
1158
+ const testCases = [
1159
+ {
1160
+ url: '/queryAsync',
1161
+ params: 200,
1162
+ body: 200,
1163
+ querystring: 200,
1164
+ headers: 'not a number sync',
1165
+ response: 400
1166
+ },
1167
+ {
1168
+ url: '/paramsAsync',
1169
+ params: 200,
1170
+ body: 'not a number sync',
1171
+ querystring: 200,
1172
+ headers: 200,
1173
+ response: 400
1174
+ },
1175
+ {
1176
+ url: '/bodyAsync',
1177
+ params: 200,
1178
+ body: 200,
1179
+ querystring: 'not a number sync',
1180
+ headers: 200,
1181
+ response: 400
1182
+ },
1183
+ {
1184
+ url: '/headersSync',
1185
+ params: 200,
1186
+ body: 200,
1187
+ querystring: 200,
1188
+ headers: 'not a number sync',
1189
+ response: 400
1190
+ },
1191
+ {
1192
+ url: '/noHeader',
1193
+ params: 200,
1194
+ body: 200,
1195
+ querystring: 200,
1196
+ headers: 'not a number sync, but not validated',
1197
+ response: 200
1198
+ },
1199
+ {
1200
+ url: '/noBody',
1201
+ params: 200,
1202
+ body: 'not a number sync, but not validated',
1203
+ querystring: 200,
1204
+ headers: 'not a number sync',
1205
+ response: 400
1206
+ }
1207
+ ]
1208
+ t.plan(testCases.length)
1209
+ for (const testCase of testCases) {
1210
+ await validate(testCase)
1211
+ }
1212
+
1213
+ async function validate ({
1214
+ url,
1215
+ params,
1216
+ body,
1217
+ querystring,
1218
+ headers,
1219
+ response
1220
+ }) {
1221
+ try {
1222
+ const res = await fastify.inject({
1223
+ method: 'POST',
1224
+ url: `${url}/${params || ''}`,
1225
+ headers: { id: headers },
1226
+ query: { id: querystring },
1227
+ payload: { id: body }
1228
+ })
1229
+ t.assert.strictEqual(res.statusCode, response)
1230
+ } catch (error) {
1231
+ t.assert.fail('should not fail')
1232
+ }
1233
+ }
1234
+ })
1235
+
1236
+ test('Check if hooks and attachValidation work with AJV validations', async (t) => {
1237
+ const fastify = Fastify({
1238
+ exposeHeadRoutes: false,
1239
+ ajv: {
1240
+ customOptions: {
1241
+ allErrors: true,
1242
+ keywords: [
1243
+ {
1244
+ keyword: 'idExists',
1245
+ async: true,
1246
+ type: 'number',
1247
+ validate: checkIdExists
1248
+ }
1249
+ ]
1250
+ }
1251
+ }
1252
+ })
1253
+
1254
+ async function checkIdExists (schema, data) {
1255
+ const res = await Promise.resolve(data)
1256
+ switch (res) {
1257
+ case 200:
1258
+ return true
1259
+
1260
+ default:
1261
+ return false
1262
+ }
1263
+ }
1264
+
1265
+ const schemaAsync = {
1266
+ $async: true,
1267
+ type: 'object',
1268
+ properties: {
1269
+ id: {
1270
+ type: 'integer',
1271
+ idExists: { table: 'posts' }
1272
+ }
1273
+ }
1274
+ }
1275
+
1276
+ fastify.post('/:id', {
1277
+ preHandler: function hook (request, reply, done) {
1278
+ t.assert.strictEqual(request.validationError.message, 'validation failed')
1279
+ t.assert.ok('preHandler called')
1280
+
1281
+ reply.code(400).send(request.body)
1282
+ },
1283
+ attachValidation: true,
1284
+ schema: {
1285
+ params: schemaAsync,
1286
+ body: schemaAsync,
1287
+ query: schemaAsync,
1288
+ headers: schemaAsync
1289
+ },
1290
+ handler (req, reply) { reply.send(req.body) }
1291
+ })
1292
+
1293
+ const testCases = [
1294
+ {
1295
+ params: 200,
1296
+ body: 200,
1297
+ querystring: 200,
1298
+ headers: 400,
1299
+ response: 400
1300
+ },
1301
+ {
1302
+ params: 200,
1303
+ body: 400,
1304
+ querystring: 200,
1305
+ headers: 200,
1306
+ response: 400
1307
+ },
1308
+ {
1309
+ params: 200,
1310
+ body: 200,
1311
+ querystring: 400,
1312
+ headers: 200,
1313
+ response: 400
1314
+ },
1315
+ {
1316
+ params: 200,
1317
+ body: 200,
1318
+ querystring: 200,
1319
+ headers: 400,
1320
+ response: 400
1321
+ }
1322
+ ]
1323
+ t.plan(testCases.length * 3)
1324
+ for (const testCase of testCases) {
1325
+ await validate(testCase)
1326
+ }
1327
+
1328
+ async function validate ({
1329
+ params,
1330
+ body,
1331
+ querystring,
1332
+ headers,
1333
+ response
1334
+ }) {
1335
+ try {
1336
+ const res = await fastify.inject({
1337
+ method: 'POST',
1338
+ url: `/${params}`,
1339
+ headers: { id: headers },
1340
+ query: { id: querystring },
1341
+ payload: { id: body }
1342
+ })
1343
+ t.assert.strictEqual(res.statusCode, response)
1344
+ } catch (error) {
1345
+ t.assert.fail('should not fail')
1346
+ }
1347
+ }
1348
+ })