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,339 @@
1
+ 'use strict'
2
+
3
+ const { describe, it, beforeEach, afterEach } = require('node:test')
4
+ const assert = require('node:assert')
5
+ const fs = require('node:fs')
6
+ const { MockAgent, setGlobalDispatcher, getGlobalDispatcher } = require('undici')
7
+
8
+ function loadValidateEcosystemLinksModule () {
9
+ const modulePath = require.resolve('../../scripts/validate-ecosystem-links')
10
+ delete require.cache[modulePath]
11
+ return require(modulePath)
12
+ }
13
+
14
+ describe('extractGitHubLinks', () => {
15
+ const { extractGitHubLinks } = loadValidateEcosystemLinksModule()
16
+
17
+ it('extracts simple GitHub repository links', () => {
18
+ const content = `
19
+ # Ecosystem
20
+
21
+ - [fastify-helmet](https://github.com/fastify/fastify-helmet) - Important security headers for Fastify
22
+ - [fastify-cors](https://github.com/fastify/fastify-cors) - CORS support
23
+ `
24
+ const links = extractGitHubLinks(content)
25
+
26
+ assert.strictEqual(links.length, 2)
27
+ assert.deepStrictEqual(links[0], {
28
+ name: 'fastify-helmet',
29
+ url: 'https://github.com/fastify/fastify-helmet',
30
+ owner: 'fastify',
31
+ repo: 'fastify-helmet'
32
+ })
33
+ assert.deepStrictEqual(links[1], {
34
+ name: 'fastify-cors',
35
+ url: 'https://github.com/fastify/fastify-cors',
36
+ owner: 'fastify',
37
+ repo: 'fastify-cors'
38
+ })
39
+ })
40
+
41
+ it('extracts links with different owner/repo combinations', () => {
42
+ const content = `
43
+ - [some-plugin](https://github.com/user123/awesome-plugin)
44
+ - [another-lib](https://github.com/org-name/lib-name)
45
+ `
46
+ const links = extractGitHubLinks(content)
47
+
48
+ assert.strictEqual(links.length, 2)
49
+ assert.strictEqual(links[0].owner, 'user123')
50
+ assert.strictEqual(links[0].repo, 'awesome-plugin')
51
+ assert.strictEqual(links[1].owner, 'org-name')
52
+ assert.strictEqual(links[1].repo, 'lib-name')
53
+ })
54
+
55
+ it('handles links with hash fragments', () => {
56
+ const content = `
57
+ - [project](https://github.com/owner/repo#readme)
58
+ `
59
+ const links = extractGitHubLinks(content)
60
+
61
+ assert.strictEqual(links.length, 1)
62
+ assert.strictEqual(links[0].repo, 'repo')
63
+ assert.strictEqual(links[0].url, 'https://github.com/owner/repo#readme')
64
+ })
65
+
66
+ it('handles links with query parameters', () => {
67
+ const content = `
68
+ - [project](https://github.com/owner/repo?tab=readme)
69
+ `
70
+ const links = extractGitHubLinks(content)
71
+
72
+ assert.strictEqual(links.length, 1)
73
+ assert.strictEqual(links[0].repo, 'repo')
74
+ })
75
+
76
+ it('handles links with subpaths', () => {
77
+ const content = `
78
+ - [docs](https://github.com/owner/repo/tree/main/docs)
79
+ `
80
+ const links = extractGitHubLinks(content)
81
+
82
+ assert.strictEqual(links.length, 1)
83
+ assert.strictEqual(links[0].owner, 'owner')
84
+ assert.strictEqual(links[0].repo, 'repo')
85
+ })
86
+
87
+ it('returns empty array for content with no GitHub links', () => {
88
+ const content = `
89
+ # No GitHub links here
90
+
91
+ Just some regular text and [a link](https://example.com).
92
+ `
93
+ const links = extractGitHubLinks(content)
94
+
95
+ assert.strictEqual(links.length, 0)
96
+ })
97
+
98
+ it('ignores non-GitHub links', () => {
99
+ const content = `
100
+ - [gitlab](https://gitlab.com/owner/repo)
101
+ - [github](https://github.com/owner/repo)
102
+ - [bitbucket](https://bitbucket.org/owner/repo)
103
+ `
104
+ const links = extractGitHubLinks(content)
105
+
106
+ assert.strictEqual(links.length, 1)
107
+ assert.strictEqual(links[0].owner, 'owner')
108
+ })
109
+
110
+ it('extracts multiple links from complex markdown', () => {
111
+ const content = `
112
+ ## Category 1
113
+
114
+ Some description [inline link](https://github.com/a/b).
115
+
116
+ | Plugin | Description |
117
+ |--------|-------------|
118
+ | [plugin1](https://github.com/x/y) | Desc 1 |
119
+ | [plugin2](https://github.com/z/w) | Desc 2 |
120
+ `
121
+ const links = extractGitHubLinks(content)
122
+
123
+ assert.strictEqual(links.length, 3)
124
+ })
125
+ })
126
+
127
+ describe('checkGitHubRepo', () => {
128
+ let originalDispatcher
129
+ let mockAgent
130
+ let originalFetch
131
+ let originalSetTimeout
132
+
133
+ beforeEach(() => {
134
+ delete process.env.GITHUB_TOKEN
135
+ originalDispatcher = getGlobalDispatcher()
136
+ mockAgent = new MockAgent()
137
+ mockAgent.disableNetConnect()
138
+ setGlobalDispatcher(mockAgent)
139
+ originalFetch = global.fetch
140
+ originalSetTimeout = global.setTimeout
141
+ })
142
+
143
+ afterEach(async () => {
144
+ global.fetch = originalFetch
145
+ global.setTimeout = originalSetTimeout
146
+ setGlobalDispatcher(originalDispatcher)
147
+ await mockAgent.close()
148
+ delete process.env.GITHUB_TOKEN
149
+ })
150
+
151
+ it('returns exists: true for status 200', async () => {
152
+ const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
153
+ const mockPool = mockAgent.get('https://api.github.com')
154
+ mockPool.intercept({
155
+ path: '/repos/fastify/fastify',
156
+ method: 'HEAD'
157
+ }).reply(200)
158
+
159
+ const result = await checkGitHubRepo('fastify', 'fastify')
160
+
161
+ assert.strictEqual(result.exists, true)
162
+ assert.strictEqual(result.status, 200)
163
+ assert.strictEqual(result.owner, 'fastify')
164
+ assert.strictEqual(result.repo, 'fastify')
165
+ })
166
+
167
+ it('returns exists: false for status 404', async () => {
168
+ const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
169
+ const mockPool = mockAgent.get('https://api.github.com')
170
+ mockPool.intercept({
171
+ path: '/repos/nonexistent/repo',
172
+ method: 'HEAD'
173
+ }).reply(404)
174
+
175
+ const result = await checkGitHubRepo('nonexistent', 'repo')
176
+
177
+ assert.strictEqual(result.exists, false)
178
+ assert.strictEqual(result.status, 404)
179
+ })
180
+
181
+ it('returns invalid status for malformed owner or repository names', async () => {
182
+ const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
183
+ let called = false
184
+
185
+ global.fetch = async () => {
186
+ called = true
187
+ return { status: 200 }
188
+ }
189
+
190
+ const result = await checkGitHubRepo('owner/evil', 'repo', 1)
191
+
192
+ assert.strictEqual(called, false)
193
+ assert.strictEqual(result.exists, false)
194
+ assert.strictEqual(result.status, 'invalid')
195
+ assert.strictEqual(result.error, 'Invalid GitHub repository identifier')
196
+ })
197
+
198
+ it('retries on rate limit responses', async () => {
199
+ const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
200
+ let attempts = 0
201
+
202
+ global.setTimeout = (fn) => {
203
+ fn()
204
+ return 0
205
+ }
206
+
207
+ global.fetch = async () => {
208
+ attempts++
209
+ return {
210
+ status: attempts === 1 ? 403 : 200
211
+ }
212
+ }
213
+
214
+ const result = await checkGitHubRepo('owner', 'repo', 1)
215
+
216
+ assert.strictEqual(attempts, 2)
217
+ assert.strictEqual(result.exists, true)
218
+ assert.strictEqual(result.status, 200)
219
+ })
220
+
221
+ it('adds authorization header when GITHUB_TOKEN is set', async () => {
222
+ process.env.GITHUB_TOKEN = 'my-token'
223
+ const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
224
+ let authorization
225
+
226
+ global.fetch = async (url, options) => {
227
+ authorization = options.headers.Authorization
228
+ return {
229
+ status: 200
230
+ }
231
+ }
232
+
233
+ const result = await checkGitHubRepo('owner', 'repo')
234
+
235
+ assert.strictEqual(authorization, 'token my-token')
236
+ assert.strictEqual(result.exists, true)
237
+ })
238
+
239
+ it('handles network errors', async () => {
240
+ const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
241
+ const mockPool = mockAgent.get('https://api.github.com')
242
+ mockPool.intercept({
243
+ path: '/repos/owner/repo',
244
+ method: 'HEAD'
245
+ }).replyWithError(new Error('Network error'))
246
+
247
+ const result = await checkGitHubRepo('owner', 'repo')
248
+
249
+ assert.strictEqual(result.exists, false)
250
+ assert.strictEqual(result.status, 'error')
251
+ assert.ok(result.error.length > 0)
252
+ })
253
+ })
254
+
255
+ describe('validateAllLinks', () => {
256
+ let originalReadFileSync
257
+ let originalFetch
258
+ let originalSetTimeout
259
+ let originalConsoleLog
260
+ let originalStdoutWrite
261
+
262
+ beforeEach(() => {
263
+ originalReadFileSync = fs.readFileSync
264
+ originalFetch = global.fetch
265
+ originalSetTimeout = global.setTimeout
266
+ originalConsoleLog = console.log
267
+ originalStdoutWrite = process.stdout.write
268
+
269
+ console.log = () => {}
270
+ process.stdout.write = () => true
271
+
272
+ global.setTimeout = (fn) => {
273
+ fn()
274
+ return 0
275
+ }
276
+ })
277
+
278
+ afterEach(() => {
279
+ fs.readFileSync = originalReadFileSync
280
+ global.fetch = originalFetch
281
+ global.setTimeout = originalSetTimeout
282
+ console.log = originalConsoleLog
283
+ process.stdout.write = originalStdoutWrite
284
+ })
285
+
286
+ it('validates links, deduplicates repositories and groups inaccessible links', async () => {
287
+ const { validateAllLinks } = loadValidateEcosystemLinksModule()
288
+
289
+ fs.readFileSync = () => `
290
+ - [repo one](https://github.com/owner/repo)
291
+ - [repo one duplicate](https://github.com/owner/repo)
292
+ - [repo two](https://github.com/another/project)
293
+ `
294
+
295
+ let requests = 0
296
+ global.fetch = async (url) => {
297
+ requests++
298
+ const pathname = new URL(url).pathname
299
+
300
+ if (pathname === '/repos/owner/repo') {
301
+ return { status: 404 }
302
+ }
303
+
304
+ if (pathname === '/repos/another/project') {
305
+ return { status: 200 }
306
+ }
307
+
308
+ throw new Error(`Unexpected url: ${url}`)
309
+ }
310
+
311
+ const result = await validateAllLinks()
312
+
313
+ assert.strictEqual(requests, 2)
314
+ assert.strictEqual(result.notFound.length, 1)
315
+ assert.strictEqual(result.found.length, 1)
316
+ assert.strictEqual(result.notFound[0].owner, 'owner')
317
+ assert.strictEqual(result.notFound[0].repo, 'repo')
318
+ assert.strictEqual(result.found[0].owner, 'another')
319
+ assert.strictEqual(result.found[0].repo, 'project')
320
+ })
321
+
322
+ it('returns empty result when no GitHub links are present', async () => {
323
+ const { validateAllLinks } = loadValidateEcosystemLinksModule()
324
+
325
+ fs.readFileSync = () => '# Ecosystem\nNo links here.'
326
+
327
+ let requests = 0
328
+ global.fetch = async () => {
329
+ requests++
330
+ return { status: 200 }
331
+ }
332
+
333
+ const result = await validateAllLinks()
334
+
335
+ assert.strictEqual(requests, 0)
336
+ assert.strictEqual(result.notFound.length, 0)
337
+ assert.strictEqual(result.found.length, 0)
338
+ })
339
+ })
@@ -0,0 +1,186 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const { S } = require('fluent-json-schema')
5
+ const Fastify = require('../fastify')
6
+ const sjson = require('secure-json-parse')
7
+
8
+ const BadRequestSchema = S.object()
9
+ .prop('statusCode', S.number())
10
+ .prop('error', S.string())
11
+ .prop('message', S.string())
12
+
13
+ const InternalServerErrorSchema = S.object()
14
+ .prop('statusCode', S.number())
15
+ .prop('error', S.string())
16
+ .prop('message', S.string())
17
+
18
+ const NotFoundSchema = S.object()
19
+ .prop('statusCode', S.number())
20
+ .prop('error', S.string())
21
+ .prop('message', S.string())
22
+
23
+ const options = {
24
+ schema: {
25
+ body: {
26
+ type: 'object',
27
+ properties: {
28
+ id: { type: 'string' }
29
+ }
30
+ },
31
+ response: {
32
+ 200: {
33
+ type: 'object',
34
+ properties: {
35
+ id: { type: 'string' }
36
+ }
37
+ },
38
+ 400: {
39
+ description: 'Bad Request',
40
+ content: {
41
+ 'application/json': {
42
+ schema: BadRequestSchema.valueOf()
43
+ }
44
+ }
45
+ },
46
+ 404: {
47
+ description: 'Resource not found',
48
+ content: {
49
+ 'application/json': {
50
+ schema: NotFoundSchema.valueOf(),
51
+ example: {
52
+ statusCode: 404,
53
+ error: 'Not Found',
54
+ message: 'Not Found'
55
+ }
56
+ }
57
+ }
58
+ },
59
+ 500: {
60
+ description: 'Internal Server Error',
61
+ content: {
62
+ 'application/json': {
63
+ schema: InternalServerErrorSchema.valueOf(),
64
+ example: {
65
+ message: 'Internal Server Error'
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ const handler = (request, reply) => {
75
+ if (request.body.id === '400') {
76
+ return reply.status(400).send({
77
+ statusCode: 400,
78
+ error: 'Bad Request',
79
+ message: 'Custom message',
80
+ extra: 'This should not be in the response'
81
+ })
82
+ }
83
+
84
+ if (request.body.id === '404') {
85
+ return reply.status(404).send({
86
+ statusCode: 404,
87
+ error: 'Not Found',
88
+ message: 'Custom Not Found',
89
+ extra: 'This should not be in the response'
90
+ })
91
+ }
92
+
93
+ if (request.body.id === '500') {
94
+ reply.status(500).send({
95
+ statusCode: 500,
96
+ error: 'Internal Server Error',
97
+ message: 'Custom Internal Server Error',
98
+ extra: 'This should not be in the response'
99
+ })
100
+ }
101
+
102
+ reply.send({
103
+ id: request.body.id,
104
+ extra: 'This should not be in the response'
105
+ })
106
+ }
107
+
108
+ test('serialize the response for a Bad Request error, as defined on the schema', async t => {
109
+ t.plan(2)
110
+
111
+ const fastify = Fastify({})
112
+
113
+ fastify.post('/', options, handler)
114
+ const response = await fastify.inject({
115
+ method: 'POST',
116
+ url: '/'
117
+ })
118
+
119
+ t.assert.strictEqual(response.statusCode, 400)
120
+ t.assert.deepStrictEqual(sjson(response.body), {
121
+ statusCode: 400,
122
+ error: 'Bad Request',
123
+ message: 'body must be object'
124
+ })
125
+ })
126
+
127
+ test('serialize the response for a Not Found error, as defined on the schema', async t => {
128
+ t.plan(2)
129
+
130
+ const fastify = Fastify({})
131
+
132
+ fastify.post('/', options, handler)
133
+
134
+ const response = await fastify.inject({
135
+ method: 'POST',
136
+ url: '/',
137
+ body: { id: '404' }
138
+ })
139
+
140
+ t.assert.strictEqual(response.statusCode, 404)
141
+ t.assert.deepStrictEqual(sjson(response.body), {
142
+ statusCode: 404,
143
+ error: 'Not Found',
144
+ message: 'Custom Not Found'
145
+ })
146
+ })
147
+
148
+ test('serialize the response for a Internal Server Error error, as defined on the schema', async t => {
149
+ t.plan(2)
150
+
151
+ const fastify = Fastify({})
152
+
153
+ fastify.post('/', options, handler)
154
+
155
+ const response = await fastify.inject({
156
+ method: 'POST',
157
+ url: '/',
158
+ body: { id: '500' }
159
+ })
160
+
161
+ t.assert.strictEqual(response.statusCode, 500)
162
+ t.assert.deepStrictEqual(sjson(response.body), {
163
+ statusCode: 500,
164
+ error: 'Internal Server Error',
165
+ message: 'Custom Internal Server Error'
166
+ })
167
+ })
168
+
169
+ test('serialize the success response, as defined on the schema', async t => {
170
+ t.plan(2)
171
+
172
+ const fastify = Fastify({})
173
+
174
+ fastify.post('/', options, handler)
175
+
176
+ const response = await fastify.inject({
177
+ method: 'POST',
178
+ url: '/',
179
+ body: { id: 'test' }
180
+ })
181
+
182
+ t.assert.strictEqual(response.statusCode, 200)
183
+ t.assert.deepStrictEqual(sjson(response.body), {
184
+ id: 'test'
185
+ })
186
+ })