@decohere/http 0.0.1-dev.126 → 0.0.1-dev.5
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/README.md +2 -2
- package/package.json +4 -13
- package/src/app/app.ts +147 -0
- package/src/app/module.ts +19 -0
- package/src/auth/ability.ts +56 -0
- package/src/auth/auth.config.ts +34 -0
- package/src/auth/auth.extensions.ts +115 -0
- package/src/auth/auth.types.ts +124 -0
- package/src/auth/casl-authorization.service.ts +62 -0
- package/{types/src/auth/principal-provider.d.ts → src/auth/principal-provider.ts} +13 -5
- package/src/auth/safe-outbound-url.ts +117 -0
- package/src/auth/token-service.ts +192 -0
- package/src/builder/builder.ts +118 -0
- package/{es6/src/const.js → src/const.ts} +25 -17
- package/src/health/health.controller.ts +34 -0
- package/src/health/health.extensions.ts +53 -0
- package/src/health/readiness.service.ts +33 -0
- package/{types/src/health/readiness.types.d.ts → src/health/readiness.types.ts} +4 -8
- package/src/hosting/hosted-service-host.ts +102 -0
- package/src/hosting/hosted-service.ts +31 -0
- package/src/http/http.compression.ts +38 -0
- package/src/http/http.extensions.ts +244 -0
- package/{types/src/http/http.options.store.d.ts → src/http/http.options.store.ts} +11 -1
- package/src/http/http.options.ts +41 -0
- package/{types/src/http/http.security.d.ts → src/http/http.security.ts} +16 -10
- package/src/http/http.static.ts +359 -0
- package/{es6/src/index.js → src/index.ts} +1 -0
- package/src/openapi/index.ts +22 -0
- package/src/openapi/openapi.generator.ts +78 -0
- package/src/openapi/openapi.types.ts +60 -0
- package/src/openapi/request-schema.ts +155 -0
- package/src/openapi/request-validator.ts +95 -0
- package/src/providers/fastify.provider.ts +454 -0
- package/src/routing/anvil.types.ts +150 -0
- package/src/routing/decorators/argument.decorator.ts +33 -0
- package/src/routing/decorators/auth.decorator.ts +79 -0
- package/src/routing/decorators/controller.decorator.ts +9 -0
- package/src/routing/decorators/field.decorator.ts +15 -0
- package/src/routing/decorators/request.decorator.ts +34 -0
- package/src/routing/decorators/response.decorator.ts +18 -0
- package/src/routing/decorators/route.decorator.ts +52 -0
- package/src/routing/filters/exception-filter-host.ts +90 -0
- package/src/routing/filters/exception-filter.ts +29 -0
- package/src/routing/middleware/http-access-log.middleware.ts +36 -0
- package/src/routing/middleware/middleware.ts +26 -0
- package/src/routing/middleware/request-id.ts +18 -0
- package/src/routing/path.util.ts +38 -0
- package/src/routing/pipes/pipes.ts +132 -0
- package/src/routing/services/route-argument-resolver.ts +92 -0
- package/src/routing/services/route-registry.ts +229 -0
- package/src/routing/services/router.ts +166 -0
- package/src/sse/sse.decorator.ts +41 -0
- package/src/sse/sse.encoder.ts +86 -0
- package/src/sse/sse.types.ts +47 -0
- package/src/sse/sse.wire.ts +34 -0
- package/src/testing/boot-module.ts +63 -0
- package/src/testing/sse-reader.ts +120 -0
- package/tests/app.hosted-services.spec.ts +292 -0
- package/tests/app.shutdown.integration.spec.ts +160 -0
- package/tests/app.spec.ts +132 -0
- package/tests/auth.ability.spec.ts +48 -0
- package/tests/auth.decorators.spec.ts +20 -0
- package/tests/auth.fail-closed.pentest.spec.ts +77 -0
- package/tests/auth.integration.spec.ts +234 -0
- package/tests/auth.jwks-cache.spec.ts +52 -0
- package/tests/auth.missing-auth.pentest.spec.ts +56 -0
- package/tests/auth.oidc-ssrf.pentest.spec.ts +72 -0
- package/tests/auth.oidc.integration.spec.ts +161 -0
- package/tests/auth.token-service.spec.ts +251 -0
- package/tests/builder.spec.ts +12 -0
- package/tests/health.integration.spec.ts +111 -0
- package/tests/helpers/listen-ephemeral.ts +13 -0
- package/tests/helpers/local-oidc-server.ts +100 -0
- package/tests/http.access-log.integration.spec.ts +111 -0
- package/tests/http.boolean-query.integration.spec.ts +59 -0
- package/tests/http.compression.integration.spec.ts +604 -0
- package/tests/http.date-query.integration.spec.ts +71 -0
- package/tests/http.global-prefix.integration.spec.ts +284 -0
- package/tests/http.number-query.integration.spec.ts +65 -0
- package/tests/http.pino-roll-hmr.integration.spec.ts +79 -0
- package/tests/http.pipeline-order.integration.spec.ts +95 -0
- package/tests/http.request-cleanup.integration.spec.ts +57 -0
- package/tests/http.request-id-scope.pentest.spec.ts +50 -0
- package/tests/http.security.integration.spec.ts +106 -0
- package/tests/http.static.integration.spec.ts +847 -0
- package/tests/http.static.spec.ts +111 -0
- package/tests/http.surface.integration.spec.ts +167 -0
- package/tests/openapi.integration.spec.ts +193 -0
- package/tests/openapi.validator-cache.integration.spec.ts +55 -0
- package/tests/routing.decorators.spec.ts +15 -0
- package/tests/routing.registry.spec.ts +44 -0
- package/tests/routing.resolver.spec.ts +92 -0
- package/tests/sse.integration.spec.ts +421 -0
- package/tests/sse.wire.spec.ts +33 -0
- package/LICENSE +0 -15
- package/commonjs/src/app/app.js +0 -166
- package/commonjs/src/app/index.js +0 -19
- package/commonjs/src/app/module.js +0 -17
- package/commonjs/src/auth/ability.js +0 -54
- package/commonjs/src/auth/auth.config.js +0 -23
- package/commonjs/src/auth/auth.extensions.js +0 -106
- package/commonjs/src/auth/auth.types.js +0 -65
- package/commonjs/src/auth/casl-authorization.service.js +0 -36
- package/commonjs/src/auth/index.js +0 -23
- package/commonjs/src/auth/principal-provider.js +0 -19
- package/commonjs/src/auth/safe-outbound-url.js +0 -102
- package/commonjs/src/auth/token-service.js +0 -151
- package/commonjs/src/builder/builder.js +0 -95
- package/commonjs/src/builder/index.js +0 -18
- package/commonjs/src/const.js +0 -87
- package/commonjs/src/health/health.controller.js +0 -75
- package/commonjs/src/health/health.extensions.js +0 -50
- package/commonjs/src/health/index.js +0 -21
- package/commonjs/src/health/readiness.service.js +0 -67
- package/commonjs/src/health/readiness.types.js +0 -7
- package/commonjs/src/hosting/hosted-service-host.js +0 -88
- package/commonjs/src/hosting/hosted-service.js +0 -11
- package/commonjs/src/hosting/index.js +0 -19
- package/commonjs/src/http/http.compression.js +0 -27
- package/commonjs/src/http/http.extensions.js +0 -269
- package/commonjs/src/http/http.options.js +0 -26
- package/commonjs/src/http/http.options.store.js +0 -20
- package/commonjs/src/http/http.security.js +0 -27
- package/commonjs/src/http/http.static.js +0 -248
- package/commonjs/src/http/index.js +0 -22
- package/commonjs/src/index.js +0 -27
- package/commonjs/src/openapi/index.js +0 -45
- package/commonjs/src/openapi/openapi.generator.js +0 -103
- package/commonjs/src/openapi/openapi.types.js +0 -4
- package/commonjs/src/openapi/request-schema.js +0 -187
- package/commonjs/src/openapi/request-validator.js +0 -85
- package/commonjs/src/providers/fastify.provider.js +0 -391
- package/commonjs/src/providers/index.js +0 -18
- package/commonjs/src/routing/anvil.types.js +0 -99
- package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
- package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
- package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
- package/commonjs/src/routing/decorators/field.decorator.js +0 -25
- package/commonjs/src/routing/decorators/index.js +0 -24
- package/commonjs/src/routing/decorators/request.decorator.js +0 -30
- package/commonjs/src/routing/decorators/response.decorator.js +0 -34
- package/commonjs/src/routing/decorators/route.decorator.js +0 -67
- package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
- package/commonjs/src/routing/filters/exception-filter.js +0 -28
- package/commonjs/src/routing/index.js +0 -25
- package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
- package/commonjs/src/routing/middleware/middleware.js +0 -20
- package/commonjs/src/routing/middleware/request-id.js +0 -30
- package/commonjs/src/routing/path.util.js +0 -51
- package/commonjs/src/routing/pipes/index.js +0 -18
- package/commonjs/src/routing/pipes/pipes.js +0 -145
- package/commonjs/src/routing/services/index.js +0 -18
- package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
- package/commonjs/src/routing/services/route-registry.js +0 -194
- package/commonjs/src/routing/services/router.js +0 -158
- package/commonjs/src/sse/index.js +0 -26
- package/commonjs/src/sse/sse.decorator.js +0 -42
- package/commonjs/src/sse/sse.encoder.js +0 -73
- package/commonjs/src/sse/sse.types.js +0 -36
- package/commonjs/src/sse/sse.wire.js +0 -40
- package/commonjs/src/testing/boot-module.js +0 -38
- package/commonjs/src/testing/index.js +0 -20
- package/commonjs/src/testing/sse-reader.js +0 -98
- package/es6/src/app/app.js +0 -148
- package/es6/src/app/index.js +0 -2
- package/es6/src/app/module.js +0 -7
- package/es6/src/auth/ability.js +0 -45
- package/es6/src/auth/auth.config.js +0 -19
- package/es6/src/auth/auth.extensions.js +0 -83
- package/es6/src/auth/auth.types.js +0 -35
- package/es6/src/auth/casl-authorization.service.js +0 -40
- package/es6/src/auth/index.js +0 -6
- package/es6/src/auth/principal-provider.js +0 -14
- package/es6/src/auth/safe-outbound-url.js +0 -99
- package/es6/src/auth/token-service.js +0 -141
- package/es6/src/builder/builder.js +0 -85
- package/es6/src/builder/index.js +0 -1
- package/es6/src/health/health.controller.js +0 -65
- package/es6/src/health/health.extensions.js +0 -40
- package/es6/src/health/index.js +0 -4
- package/es6/src/health/readiness.service.js +0 -57
- package/es6/src/health/readiness.types.js +0 -4
- package/es6/src/hosting/hosted-service-host.js +0 -81
- package/es6/src/hosting/hosted-service.js +0 -8
- package/es6/src/hosting/index.js +0 -2
- package/es6/src/http/http.compression.js +0 -9
- package/es6/src/http/http.extensions.js +0 -259
- package/es6/src/http/http.options.js +0 -16
- package/es6/src/http/http.options.store.js +0 -13
- package/es6/src/http/http.security.js +0 -9
- package/es6/src/http/http.static.js +0 -224
- package/es6/src/http/index.js +0 -5
- package/es6/src/openapi/index.js +0 -3
- package/es6/src/openapi/openapi.generator.js +0 -82
- package/es6/src/openapi/openapi.types.js +0 -1
- package/es6/src/openapi/request-schema.js +0 -160
- package/es6/src/openapi/request-validator.js +0 -71
- package/es6/src/providers/fastify.provider.js +0 -376
- package/es6/src/providers/index.js +0 -1
- package/es6/src/routing/anvil.types.js +0 -57
- package/es6/src/routing/decorators/argument.decorator.js +0 -28
- package/es6/src/routing/decorators/auth.decorator.js +0 -58
- package/es6/src/routing/decorators/controller.decorator.js +0 -6
- package/es6/src/routing/decorators/field.decorator.js +0 -15
- package/es6/src/routing/decorators/index.js +0 -7
- package/es6/src/routing/decorators/request.decorator.js +0 -24
- package/es6/src/routing/decorators/response.decorator.js +0 -16
- package/es6/src/routing/decorators/route.decorator.js +0 -37
- package/es6/src/routing/filters/exception-filter-host.js +0 -127
- package/es6/src/routing/filters/exception-filter.js +0 -10
- package/es6/src/routing/index.js +0 -8
- package/es6/src/routing/middleware/http-access-log.middleware.js +0 -37
- package/es6/src/routing/middleware/middleware.js +0 -10
- package/es6/src/routing/middleware/request-id.js +0 -14
- package/es6/src/routing/path.util.js +0 -33
- package/es6/src/routing/pipes/index.js +0 -1
- package/es6/src/routing/pipes/pipes.js +0 -118
- package/es6/src/routing/services/index.js +0 -1
- package/es6/src/routing/services/route-argument-resolver.js +0 -67
- package/es6/src/routing/services/route-registry.js +0 -184
- package/es6/src/routing/services/router.js +0 -148
- package/es6/src/sse/index.js +0 -2
- package/es6/src/sse/sse.decorator.js +0 -31
- package/es6/src/sse/sse.encoder.js +0 -67
- package/es6/src/sse/sse.types.js +0 -22
- package/es6/src/sse/sse.wire.js +0 -30
- package/es6/src/testing/boot-module.js +0 -35
- package/es6/src/testing/index.js +0 -2
- package/es6/src/testing/sse-reader.js +0 -91
- package/types/src/app/app.d.ts +0 -41
- package/types/src/app/module.d.ts +0 -15
- package/types/src/auth/ability.d.ts +0 -19
- package/types/src/auth/auth.config.d.ts +0 -28
- package/types/src/auth/auth.extensions.d.ts +0 -53
- package/types/src/auth/auth.types.d.ts +0 -73
- package/types/src/auth/casl-authorization.service.d.ts +0 -28
- package/types/src/auth/safe-outbound-url.d.ts +0 -9
- package/types/src/auth/token-service.d.ts +0 -31
- package/types/src/builder/builder.d.ts +0 -36
- package/types/src/const.d.ts +0 -32
- package/types/src/health/health.controller.d.ts +0 -16
- package/types/src/health/health.extensions.d.ts +0 -31
- package/types/src/health/readiness.service.d.ts +0 -9
- package/types/src/hosting/hosted-service-host.d.ts +0 -37
- package/types/src/hosting/hosted-service.d.ts +0 -31
- package/types/src/http/http.compression.d.ts +0 -32
- package/types/src/http/http.extensions.d.ts +0 -79
- package/types/src/http/http.options.d.ts +0 -39
- package/types/src/http/http.static.d.ts +0 -160
- package/types/src/index.d.ts +0 -10
- package/types/src/openapi/index.d.ts +0 -6
- package/types/src/openapi/openapi.generator.d.ts +0 -6
- package/types/src/openapi/openapi.types.d.ts +0 -58
- package/types/src/openapi/request-schema.d.ts +0 -21
- package/types/src/openapi/request-validator.d.ts +0 -9
- package/types/src/providers/fastify.provider.d.ts +0 -56
- package/types/src/routing/anvil.types.d.ts +0 -119
- package/types/src/routing/decorators/argument.decorator.d.ts +0 -11
- package/types/src/routing/decorators/auth.decorator.d.ts +0 -30
- package/types/src/routing/decorators/controller.decorator.d.ts +0 -3
- package/types/src/routing/decorators/field.decorator.d.ts +0 -2
- package/types/src/routing/decorators/request.decorator.d.ts +0 -8
- package/types/src/routing/decorators/response.decorator.d.ts +0 -4
- package/types/src/routing/decorators/route.decorator.d.ts +0 -7
- package/types/src/routing/filters/exception-filter-host.d.ts +0 -19
- package/types/src/routing/filters/exception-filter.d.ts +0 -16
- package/types/src/routing/middleware/http-access-log.middleware.d.ts +0 -8
- package/types/src/routing/middleware/middleware.d.ts +0 -10
- package/types/src/routing/middleware/request-id.d.ts +0 -6
- package/types/src/routing/path.util.d.ts +0 -9
- package/types/src/routing/pipes/pipes.d.ts +0 -21
- package/types/src/routing/services/route-argument-resolver.d.ts +0 -10
- package/types/src/routing/services/route-registry.d.ts +0 -20
- package/types/src/routing/services/router.d.ts +0 -22
- package/types/src/sse/sse.decorator.d.ts +0 -17
- package/types/src/sse/sse.encoder.d.ts +0 -16
- package/types/src/sse/sse.types.d.ts +0 -37
- package/types/src/sse/sse.wire.d.ts +0 -9
- package/types/src/testing/boot-module.d.ts +0 -34
- package/types/src/testing/sse-reader.d.ts +0 -29
- /package/{types/src/app/index.d.ts → src/app/index.ts} +0 -0
- /package/{types/src/auth/index.d.ts → src/auth/index.ts} +0 -0
- /package/{types/src/builder/index.d.ts → src/builder/index.ts} +0 -0
- /package/{types/src/health/index.d.ts → src/health/index.ts} +0 -0
- /package/{types/src/hosting/index.d.ts → src/hosting/index.ts} +0 -0
- /package/{types/src/http/index.d.ts → src/http/index.ts} +0 -0
- /package/{types/src/providers/index.d.ts → src/providers/index.ts} +0 -0
- /package/{types/src/routing/decorators/index.d.ts → src/routing/decorators/index.ts} +0 -0
- /package/{types/src/routing/index.d.ts → src/routing/index.ts} +0 -0
- /package/{types/src/routing/pipes/index.d.ts → src/routing/pipes/index.ts} +0 -0
- /package/{types/src/routing/services/index.d.ts → src/routing/services/index.ts} +0 -0
- /package/{types/src/sse/index.d.ts → src/sse/index.ts} +0 -0
- /package/{types/src/testing/index.d.ts → src/testing/index.ts} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { STATIC_DEFAULT_IMMUTABLE, staticFiles } from "../src/http/http.static.js";
|
|
3
|
+
|
|
4
|
+
describe("static file rules", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
"assets/index-DiwrgTda.js",
|
|
7
|
+
"assets/index-3f9a8c1b.js",
|
|
8
|
+
"assets/style.3f9a8c1b.css",
|
|
9
|
+
"assets/vendor-x9Kd02_a.js.map",
|
|
10
|
+
"chunk-1a2b3c4d5e6f.js",
|
|
11
|
+
])("treats %s as hashed", (file) => {
|
|
12
|
+
expect(STATIC_DEFAULT_IMMUTABLE.test(file)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it.each([
|
|
16
|
+
"index.html",
|
|
17
|
+
"remoteEntry.js",
|
|
18
|
+
"favicon.ico",
|
|
19
|
+
"assets/logo-background.png",
|
|
20
|
+
"assets/short-abc123.js",
|
|
21
|
+
"fonts/inter.woff2",
|
|
22
|
+
"main.min.js",
|
|
23
|
+
])("does not treat %s as hashed", (file) => {
|
|
24
|
+
expect(STATIC_DEFAULT_IMMUTABLE.test(file)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each([
|
|
28
|
+
["/ui/", "/ui/"],
|
|
29
|
+
["/ui", "/ui/"],
|
|
30
|
+
["ui", "/ui/"],
|
|
31
|
+
["//apps//crypto/", "/apps/crypto/"],
|
|
32
|
+
["/", "/"],
|
|
33
|
+
["//", "/"],
|
|
34
|
+
])("mounts %s at %s", (given, expected) => {
|
|
35
|
+
expect(staticFiles.toMount(given, "ui").prefix).toBe(expected);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it.each(["", "/ui/../api", "/ui/*"])("refuses the prefix %j", (given) => {
|
|
39
|
+
expect(() => staticFiles.toMount(given, "ui")).toThrow(/withStatic/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each([
|
|
43
|
+
["/portfolio", false],
|
|
44
|
+
["/portfolio/42/edit", false],
|
|
45
|
+
["/portfolio/", false],
|
|
46
|
+
["/", false],
|
|
47
|
+
["/app.js", true],
|
|
48
|
+
["/assets/x.map", true],
|
|
49
|
+
["/.env", true],
|
|
50
|
+
["/.well-known/x", true],
|
|
51
|
+
["/v1.2/overview/", false],
|
|
52
|
+
])("treats %s as a file: %s", (pathname, expected) => {
|
|
53
|
+
expect(staticFiles.looksLikeFile(pathname)).toBe(expected);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it.each([
|
|
57
|
+
["text/html", true],
|
|
58
|
+
["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", true],
|
|
59
|
+
["application/json, TEXT/HTML;q=0.5", true],
|
|
60
|
+
["text/html;q=0", false],
|
|
61
|
+
["application/json", false],
|
|
62
|
+
["*/*", false],
|
|
63
|
+
["text/*", false],
|
|
64
|
+
["", false],
|
|
65
|
+
[undefined, false],
|
|
66
|
+
])("reads the Accept header %j as html: %s", (accept, expected) => {
|
|
67
|
+
expect(staticFiles.acceptsHtml(accept)).toBe(expected);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("takes the first segment of every route that is not a parameter", () => {
|
|
71
|
+
const segments = staticFiles.routeSegments([
|
|
72
|
+
"/lp/portfolio",
|
|
73
|
+
"/health",
|
|
74
|
+
"/:id",
|
|
75
|
+
"/*",
|
|
76
|
+
"/",
|
|
77
|
+
"/lp/x",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
expect([...segments].sort()).toEqual(["health", "lp"]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it.each([
|
|
84
|
+
["/lp/portfolo", true],
|
|
85
|
+
["/lp", true],
|
|
86
|
+
["/lp/a/b/c", true],
|
|
87
|
+
["/lpx", false],
|
|
88
|
+
["/portfolio", false],
|
|
89
|
+
["/", false],
|
|
90
|
+
])("decides whether %s is under a route: %s", (pathname, expected) => {
|
|
91
|
+
expect(staticFiles.isUnderRoute(pathname, new Set(["lp", "health"]))).toBe(expected);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.each([
|
|
95
|
+
[["/api/**"], "/api/x", true],
|
|
96
|
+
[["/api/**"], "/api", true],
|
|
97
|
+
[["/api/**"], "/apix", false],
|
|
98
|
+
[["/docs/*"], "/docs/a/b", false],
|
|
99
|
+
[["/docs/*"], "/docs/a", true],
|
|
100
|
+
[["/api/**", "/docs/**"], "/docs/a/b", true],
|
|
101
|
+
[["/api/**"], "/api/.hidden", true],
|
|
102
|
+
[[], "/api/x", false],
|
|
103
|
+
])("matches exclude %j against %s: %s", (patterns, pathname, expected) => {
|
|
104
|
+
expect(staticFiles.excluder(patterns)(pathname)).toBe(expected);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("normalises the path: no query, decoded, one slash", () => {
|
|
108
|
+
expect(staticFiles.normalizePathname("//api%2Fx//y?z=1")).toBe("/api/x/y");
|
|
109
|
+
expect(staticFiles.normalizePathname("/bad%zz")).toBe("/bad%zz");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { injectable } from "@decohere/di/decorators";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import type { App } from "../src/app/app.js";
|
|
5
|
+
import { Builder } from "../src/builder/builder.js";
|
|
6
|
+
import "../src/http/http.extensions.js";
|
|
7
|
+
import { FastifyProvider } from "../src/providers/fastify.provider.js";
|
|
8
|
+
import {
|
|
9
|
+
Catch,
|
|
10
|
+
Controller,
|
|
11
|
+
type ExceptionContext,
|
|
12
|
+
type ExceptionFilter,
|
|
13
|
+
type ExceptionResult,
|
|
14
|
+
Get,
|
|
15
|
+
Header,
|
|
16
|
+
HttpCode,
|
|
17
|
+
HttpError,
|
|
18
|
+
type Middleware,
|
|
19
|
+
type MiddlewareContext,
|
|
20
|
+
Post,
|
|
21
|
+
} from "../src/routing/index.js";
|
|
22
|
+
import { listenEphemeral } from "./helpers/listen-ephemeral.js";
|
|
23
|
+
|
|
24
|
+
class NotFoundError extends HttpError {
|
|
25
|
+
public constructor(message = "Not Found") {
|
|
26
|
+
super(message, 404);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Catch(NotFoundError)
|
|
31
|
+
@injectable()
|
|
32
|
+
class NotFoundFilter implements ExceptionFilter {
|
|
33
|
+
public catch(error: NotFoundError, _ctx: ExceptionContext): ExceptionResult {
|
|
34
|
+
return { status: error.status, body: { code: "NOT_FOUND", message: error.message } };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@injectable()
|
|
39
|
+
class TraceMiddleware implements Middleware {
|
|
40
|
+
public static calls = 0;
|
|
41
|
+
public async use(ctx: MiddlewareContext, next: () => Promise<unknown>) {
|
|
42
|
+
TraceMiddleware.calls += 1;
|
|
43
|
+
ctx.request.headers["x-mw"] = "1";
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Controller({ prefix: "/scope" })
|
|
49
|
+
class ScopedController {
|
|
50
|
+
private static nextId = 0;
|
|
51
|
+
private readonly id = ++ScopedController.nextId;
|
|
52
|
+
|
|
53
|
+
@Get()
|
|
54
|
+
public who() {
|
|
55
|
+
return { id: this.id };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Controller({ prefix: "/widgets" })
|
|
60
|
+
class WidgetController {
|
|
61
|
+
@HttpCode(201)
|
|
62
|
+
@Header("X-Widget", "yes")
|
|
63
|
+
@Post()
|
|
64
|
+
public create() {
|
|
65
|
+
return { id: 1 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Get("/missing")
|
|
69
|
+
public missing() {
|
|
70
|
+
throw new NotFoundError("widget gone");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Get("/boom")
|
|
74
|
+
public boom() {
|
|
75
|
+
throw new Error("kaboom");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Get()
|
|
79
|
+
public list() {
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("HTTP surface (filters, HttpCode/Header, middleware, CORS, global prefix)", () => {
|
|
85
|
+
let app: App;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
TraceMiddleware.calls = 0;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterEach(async () => {
|
|
92
|
+
await app?.dispose();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("applies global prefix, HttpCode, Header, middleware, CORS, and exception filters", async () => {
|
|
96
|
+
app = await Builder.create()
|
|
97
|
+
.withGlobalPrefix("/api")
|
|
98
|
+
.withCors({ origin: "https://example.com", credentials: true })
|
|
99
|
+
.withMiddleware(TraceMiddleware)
|
|
100
|
+
.withExceptionFilters(NotFoundFilter)
|
|
101
|
+
.withHttpProvider(FastifyProvider)
|
|
102
|
+
.withControllers(WidgetController)
|
|
103
|
+
.build();
|
|
104
|
+
axios.defaults.baseURL = await listenEphemeral(app);
|
|
105
|
+
|
|
106
|
+
const created = await axios.post(
|
|
107
|
+
"/api/widgets",
|
|
108
|
+
{},
|
|
109
|
+
{
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
expect(created.status).toBe(201);
|
|
114
|
+
expect(created.headers["x-widget"]).toBe("yes");
|
|
115
|
+
expect(created.data).toEqual({ id: 1 });
|
|
116
|
+
expect(TraceMiddleware.calls).toBeGreaterThanOrEqual(1);
|
|
117
|
+
|
|
118
|
+
const missing = await axios.get("/api/widgets/missing", { validateStatus: () => true });
|
|
119
|
+
expect(missing.status).toBe(404);
|
|
120
|
+
expect(missing.data).toEqual({ code: "NOT_FOUND", message: "widget gone" });
|
|
121
|
+
|
|
122
|
+
const boom = await axios.get("/api/widgets/boom", { validateStatus: () => true });
|
|
123
|
+
expect(boom.status).toBe(500);
|
|
124
|
+
expect(boom.data).toEqual({ message: "Internal Server Error" });
|
|
125
|
+
expect(JSON.stringify(boom.data)).not.toMatch(/kaboom/i);
|
|
126
|
+
|
|
127
|
+
const preflight = await axios.options("/api/widgets", {
|
|
128
|
+
headers: {
|
|
129
|
+
Origin: "https://example.com",
|
|
130
|
+
"Access-Control-Request-Method": "POST",
|
|
131
|
+
},
|
|
132
|
+
validateStatus: () => true,
|
|
133
|
+
});
|
|
134
|
+
expect(preflight.status).toBe(204);
|
|
135
|
+
expect(preflight.headers["access-control-allow-origin"]).toBe("https://example.com");
|
|
136
|
+
expect(preflight.headers["access-control-allow-credentials"]).toBe("true");
|
|
137
|
+
|
|
138
|
+
const listed = await axios.get("/api/widgets", {
|
|
139
|
+
headers: { Origin: "https://example.com" },
|
|
140
|
+
});
|
|
141
|
+
expect(listed.status).toBe(200);
|
|
142
|
+
expect(listed.headers["access-control-allow-origin"]).toBe("https://example.com");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("returns 404 for an unregistered path", async () => {
|
|
146
|
+
app = await Builder.create()
|
|
147
|
+
.withHttpProvider(FastifyProvider)
|
|
148
|
+
.withControllers(WidgetController)
|
|
149
|
+
.build();
|
|
150
|
+
axios.defaults.baseURL = await listenEphemeral(app);
|
|
151
|
+
|
|
152
|
+
const missing = await axios.get("/nope", { validateStatus: () => true });
|
|
153
|
+
expect(missing.status).toBe(404);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("creates a new controller instance per request", async () => {
|
|
157
|
+
app = await Builder.create()
|
|
158
|
+
.withHttpProvider(FastifyProvider)
|
|
159
|
+
.withControllers(ScopedController)
|
|
160
|
+
.build();
|
|
161
|
+
axios.defaults.baseURL = await listenEphemeral(app);
|
|
162
|
+
|
|
163
|
+
const first = await axios.get("/scope");
|
|
164
|
+
const second = await axios.get("/scope");
|
|
165
|
+
expect(first.data.id).not.toBe(second.data.id);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import type { App } from "../src/app/app.js";
|
|
4
|
+
import { Builder } from "../src/builder/builder.js";
|
|
5
|
+
import "../src/http/http.extensions.js";
|
|
6
|
+
import { FastifyProvider } from "../src/providers/fastify.provider.js";
|
|
7
|
+
import {
|
|
8
|
+
Controller,
|
|
9
|
+
Field,
|
|
10
|
+
Get,
|
|
11
|
+
Param,
|
|
12
|
+
Post,
|
|
13
|
+
Put,
|
|
14
|
+
Query,
|
|
15
|
+
RequestParts,
|
|
16
|
+
} from "../src/routing/index.js";
|
|
17
|
+
import { listenEphemeral } from "./helpers/listen-ephemeral.js";
|
|
18
|
+
|
|
19
|
+
class CreateBody {
|
|
20
|
+
@Field(RequestParts.body, "message")
|
|
21
|
+
public message!: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Controller({ prefix: "/widgets" })
|
|
25
|
+
class WidgetController {
|
|
26
|
+
@Get("/:id")
|
|
27
|
+
public getOne(@Param("id") id: string) {
|
|
28
|
+
return { id };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Post()
|
|
32
|
+
public create(body: CreateBody) {
|
|
33
|
+
return { message: body.message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Mirrors scaffolded HelloController + HelloBody for dogfood validation. */
|
|
38
|
+
class HelloBody {
|
|
39
|
+
@Field(RequestParts.body, "message")
|
|
40
|
+
public message!: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Controller({ prefix: "/hello" })
|
|
44
|
+
class HelloController {
|
|
45
|
+
@Get()
|
|
46
|
+
public getAll(@Query("filter") filter: string) {
|
|
47
|
+
return { filter, message: "hello" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Get("/:id")
|
|
51
|
+
public getOne(@Param("id") id: string) {
|
|
52
|
+
return { id, message: `Hello ${id}!` };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Post()
|
|
56
|
+
public create(body: HelloBody) {
|
|
57
|
+
return { message: body.message };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Put("/:id")
|
|
61
|
+
public update(@Param("id") id: string, body: HelloBody) {
|
|
62
|
+
return { id, message: body.message };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("OpenAPI integration", () => {
|
|
67
|
+
let app: App;
|
|
68
|
+
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
await app?.dispose();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("serves the OpenAPI document and rejects invalid bodies with typed 400", async () => {
|
|
74
|
+
app = await Builder.create()
|
|
75
|
+
.withOpenApi({
|
|
76
|
+
info: { title: "Widgets API", version: "1.0.0" },
|
|
77
|
+
path: "/openapi.json",
|
|
78
|
+
})
|
|
79
|
+
.withHttpProvider(FastifyProvider)
|
|
80
|
+
.withControllers(WidgetController)
|
|
81
|
+
.build();
|
|
82
|
+
axios.defaults.baseURL = await listenEphemeral(app);
|
|
83
|
+
|
|
84
|
+
const spec = await axios.get("/openapi.json");
|
|
85
|
+
expect(spec.status).toBe(200);
|
|
86
|
+
expect(spec.data.openapi).toBe("3.0.3");
|
|
87
|
+
expect(spec.data.paths["/widgets/{id}"].get).toBeDefined();
|
|
88
|
+
expect(spec.data.paths["/widgets"].post.requestBody).toBeDefined();
|
|
89
|
+
|
|
90
|
+
const bad = await axios.post(
|
|
91
|
+
"/widgets",
|
|
92
|
+
{},
|
|
93
|
+
{ headers: { "Content-Type": "application/json" }, validateStatus: () => true },
|
|
94
|
+
);
|
|
95
|
+
expect(bad.status).toBe(400);
|
|
96
|
+
expect(bad.data).toMatchObject({
|
|
97
|
+
statusCode: 400,
|
|
98
|
+
error: "Bad Request",
|
|
99
|
+
message: "Validation failed",
|
|
100
|
+
});
|
|
101
|
+
expect(bad.data.errors).toEqual(
|
|
102
|
+
expect.arrayContaining([
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
keyword: "required",
|
|
105
|
+
message: expect.stringContaining("message"),
|
|
106
|
+
}),
|
|
107
|
+
]),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const wrongType = await axios.post(
|
|
111
|
+
"/widgets",
|
|
112
|
+
{ message: 42 },
|
|
113
|
+
{ headers: { "Content-Type": "application/json" }, validateStatus: () => true },
|
|
114
|
+
);
|
|
115
|
+
expect(wrongType.status).toBe(400);
|
|
116
|
+
expect(wrongType.data.statusCode).toBe(400);
|
|
117
|
+
expect(wrongType.data.errors).toEqual(
|
|
118
|
+
expect.arrayContaining([
|
|
119
|
+
expect.objectContaining({
|
|
120
|
+
path: "/body/message",
|
|
121
|
+
keyword: "type",
|
|
122
|
+
}),
|
|
123
|
+
]),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const extra = await axios.post(
|
|
127
|
+
"/widgets",
|
|
128
|
+
{ message: "hi", surprise: true },
|
|
129
|
+
{ headers: { "Content-Type": "application/json" }, validateStatus: () => true },
|
|
130
|
+
);
|
|
131
|
+
expect(extra.status).toBe(400);
|
|
132
|
+
expect(extra.data.errors).toEqual(
|
|
133
|
+
expect.arrayContaining([
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
keyword: "additionalProperties",
|
|
136
|
+
}),
|
|
137
|
+
]),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const ok = await axios.post(
|
|
141
|
+
"/widgets",
|
|
142
|
+
{ message: "hi" },
|
|
143
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
144
|
+
);
|
|
145
|
+
expect(ok.status).toBe(200);
|
|
146
|
+
expect(ok.data).toEqual({ message: "hi" });
|
|
147
|
+
|
|
148
|
+
const one = await axios.get("/widgets/abc");
|
|
149
|
+
expect(one.status).toBe(200);
|
|
150
|
+
expect(one.data).toEqual({ id: "abc" });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("dogfoods Hello routes: fail-closed body validation with typed 400", async () => {
|
|
154
|
+
app = await Builder.create()
|
|
155
|
+
.withOpenApi({
|
|
156
|
+
info: { title: "hello.api", version: "1.0.0" },
|
|
157
|
+
})
|
|
158
|
+
.withHttpProvider(FastifyProvider)
|
|
159
|
+
.withControllers(HelloController)
|
|
160
|
+
.build();
|
|
161
|
+
axios.defaults.baseURL = await listenEphemeral(app);
|
|
162
|
+
|
|
163
|
+
const missing = await axios.post(
|
|
164
|
+
"/hello",
|
|
165
|
+
{},
|
|
166
|
+
{ headers: { "Content-Type": "application/json" }, validateStatus: () => true },
|
|
167
|
+
);
|
|
168
|
+
expect(missing.status).toBe(400);
|
|
169
|
+
expect(missing.data).toMatchObject({
|
|
170
|
+
statusCode: 400,
|
|
171
|
+
error: "Bad Request",
|
|
172
|
+
message: "Validation failed",
|
|
173
|
+
});
|
|
174
|
+
expect(Array.isArray(missing.data.errors)).toBe(true);
|
|
175
|
+
expect(missing.data.errors.length).toBeGreaterThan(0);
|
|
176
|
+
|
|
177
|
+
const created = await axios.post(
|
|
178
|
+
"/hello",
|
|
179
|
+
{ message: "hello" },
|
|
180
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
181
|
+
);
|
|
182
|
+
expect(created.status).toBe(200);
|
|
183
|
+
expect(created.data).toEqual({ message: "hello" });
|
|
184
|
+
|
|
185
|
+
const updated = await axios.put(
|
|
186
|
+
"/hello/1",
|
|
187
|
+
{ message: "updated" },
|
|
188
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
189
|
+
);
|
|
190
|
+
expect(updated.status).toBe(200);
|
|
191
|
+
expect(updated.data).toEqual({ id: "1", message: "updated" });
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Ajv } from "ajv";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { App } from "../src/app/app.js";
|
|
5
|
+
import { Builder } from "../src/builder/builder.js";
|
|
6
|
+
import "../src/http/http.extensions.js";
|
|
7
|
+
import { FastifyProvider } from "../src/providers/fastify.provider.js";
|
|
8
|
+
import { Controller, Field, Param, Post, RequestParts } from "../src/routing/index.js";
|
|
9
|
+
import { listenEphemeral } from "./helpers/listen-ephemeral.js";
|
|
10
|
+
|
|
11
|
+
class NoteBody {
|
|
12
|
+
@Field(RequestParts.body, "text")
|
|
13
|
+
public text!: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Controller({ prefix: "/notes" })
|
|
17
|
+
class NoteController {
|
|
18
|
+
@Post("/:id")
|
|
19
|
+
public save(@Param("id") id: string, body: NoteBody) {
|
|
20
|
+
return { id, text: body.text };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// #498: validators were compiled on every request, and AJV keeps every compiled schema,
|
|
25
|
+
// so memory grew with traffic. Each route's validators must be compiled once and reused.
|
|
26
|
+
describe("request validation compiles each route's schemas once", () => {
|
|
27
|
+
let app: App;
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await app?.dispose();
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not compile again for later requests to the same route", async () => {
|
|
35
|
+
app = await Builder.create()
|
|
36
|
+
.withOpenApi({ info: { title: "Notes", version: "1.0.0" }, path: "/openapi.json" })
|
|
37
|
+
.withHttpProvider(FastifyProvider)
|
|
38
|
+
.withControllers(NoteController)
|
|
39
|
+
.build();
|
|
40
|
+
const baseURL = await listenEphemeral(app);
|
|
41
|
+
const compile = vi.spyOn(Ajv.prototype, "compile");
|
|
42
|
+
const post = (body: object) =>
|
|
43
|
+
axios.post("/notes/1", body, { baseURL, validateStatus: () => true });
|
|
44
|
+
|
|
45
|
+
expect((await post({ text: "first" })).status).toBe(200);
|
|
46
|
+
const afterFirst = compile.mock.calls.length;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < 20; i++) {
|
|
49
|
+
expect((await post({ text: `note ${i}` })).status).toBe(200);
|
|
50
|
+
}
|
|
51
|
+
expect((await post({})).status).toBe(400);
|
|
52
|
+
|
|
53
|
+
expect(compile.mock.calls.length).toBe(afterFirst);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { RouteConfigurationError } from "../src/routing/anvil.types.js";
|
|
3
|
+
import { Get } from "../src/routing/decorators/index.js";
|
|
4
|
+
|
|
5
|
+
describe("routing decorators", () => {
|
|
6
|
+
it("throws RouteConfigurationError when a path does not start with /", () => {
|
|
7
|
+
expect(() => {
|
|
8
|
+
class BadController {
|
|
9
|
+
@Get("noslash")
|
|
10
|
+
public bad() {}
|
|
11
|
+
}
|
|
12
|
+
new BadController();
|
|
13
|
+
}).toThrow(RouteConfigurationError);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { RouteConfigurationError } from "../src/routing/anvil.types.js";
|
|
3
|
+
import { Argument, Controller, Field, Get } from "../src/routing/index.js";
|
|
4
|
+
import { RouteRegistry } from "../src/routing/services/route-registry.js";
|
|
5
|
+
|
|
6
|
+
describe("RouteRegistry", () => {
|
|
7
|
+
it("throws when a field has no location and no arg-level location", () => {
|
|
8
|
+
class BadParams {
|
|
9
|
+
@Field()
|
|
10
|
+
public q: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Controller()
|
|
14
|
+
class BadController {
|
|
15
|
+
@Get("/bad")
|
|
16
|
+
public bad(@Argument({}) _params: BadParams) {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const registry = new RouteRegistry();
|
|
20
|
+
expect(() => registry.addController(BadController)).toThrow(RouteConfigurationError);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// #503: two handlers for the same method and path used to overwrite each other silently,
|
|
24
|
+
// so one of them was never reachable.
|
|
25
|
+
it("throws when two handlers register the same method and path", () => {
|
|
26
|
+
@Controller({ prefix: "/items" })
|
|
27
|
+
class FirstController {
|
|
28
|
+
@Get()
|
|
29
|
+
public list() {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Controller({ prefix: "/items" })
|
|
33
|
+
class SecondController {
|
|
34
|
+
@Get()
|
|
35
|
+
public listAgain() {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const registry = new RouteRegistry();
|
|
39
|
+
registry.addController(FirstController);
|
|
40
|
+
expect(() => registry.addController(SecondController)).toThrow(
|
|
41
|
+
/GET \/items.*FirstController\.list.*SecondController\.listAgain/,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { HttpRequest, TransformError, ValidationError } from "../src/routing/anvil.types.js";
|
|
3
|
+
import { BooleanPipe, DatePipe, JsonPipe, NumberPipe } from "../src/routing/pipes/pipes.js";
|
|
4
|
+
import { RouteArgumentResolve } from "../src/routing/services/route-argument-resolver.js";
|
|
5
|
+
|
|
6
|
+
describe("BooleanPipe", () => {
|
|
7
|
+
const pipe = new BooleanPipe();
|
|
8
|
+
|
|
9
|
+
it("converts query-shaped strings including 1 / 0", () => {
|
|
10
|
+
expect(pipe.transform("true")).toBe(true);
|
|
11
|
+
expect(pipe.transform("FALSE")).toBe(false);
|
|
12
|
+
expect(pipe.transform("1")).toBe(true);
|
|
13
|
+
expect(pipe.transform("0")).toBe(false);
|
|
14
|
+
expect(pipe.transform(1)).toBe(true);
|
|
15
|
+
expect(pipe.transform(0)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("throws ValidationError for unrecognised values", () => {
|
|
19
|
+
expect(() => pipe.transform("yes")).toThrow(ValidationError);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("NumberPipe", () => {
|
|
24
|
+
const pipe = new NumberPipe();
|
|
25
|
+
|
|
26
|
+
it("throws ValidationError for non-numeric strings (#503)", () => {
|
|
27
|
+
expect(() => pipe.transform("abc")).toThrow(ValidationError);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("DatePipe", () => {
|
|
32
|
+
const pipe = new DatePipe();
|
|
33
|
+
|
|
34
|
+
it("converts an ISO date string to Date", () => {
|
|
35
|
+
const result = pipe.transform("2024-01-15T00:00:00.000Z");
|
|
36
|
+
expect(result).toBeInstanceOf(Date);
|
|
37
|
+
expect(result.getFullYear()).toBe(2024);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws ValidationError for an invalid or empty date string", () => {
|
|
41
|
+
expect(() => pipe.transform("not-a-date")).toThrow(ValidationError);
|
|
42
|
+
expect(() => pipe.transform("")).toThrow(ValidationError);
|
|
43
|
+
expect(() => pipe.transform(" ")).toThrow(ValidationError);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("JsonPipe", () => {
|
|
48
|
+
const pipe = new JsonPipe();
|
|
49
|
+
|
|
50
|
+
it("throws TransformError for invalid JSON", () => {
|
|
51
|
+
expect(() => pipe.transform("not json")).toThrow(TransformError);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("RouteArgumentResolve", () => {
|
|
56
|
+
const resolver = new RouteArgumentResolve();
|
|
57
|
+
|
|
58
|
+
it("passes fieldMap types into JsonPipe so Date fields revive", () => {
|
|
59
|
+
const iso = "2024-03-01T00:00:00.000Z";
|
|
60
|
+
const request = new HttpRequest({
|
|
61
|
+
requestId: "r1",
|
|
62
|
+
method: "GET",
|
|
63
|
+
url: "/",
|
|
64
|
+
params: {},
|
|
65
|
+
query: { payload: JSON.stringify({ birthday: iso }) },
|
|
66
|
+
body: {},
|
|
67
|
+
});
|
|
68
|
+
const args = resolver.resolveArguments(request, [
|
|
69
|
+
{
|
|
70
|
+
argumentIndex: 0,
|
|
71
|
+
argumentType: String,
|
|
72
|
+
argumentLocation: "query",
|
|
73
|
+
mappingKey: "payload",
|
|
74
|
+
pipes: [JsonPipe],
|
|
75
|
+
fieldMap: new Map([
|
|
76
|
+
[
|
|
77
|
+
"birthday",
|
|
78
|
+
{
|
|
79
|
+
fieldKey: "birthday",
|
|
80
|
+
fieldType: Date,
|
|
81
|
+
mappingKey: "birthday",
|
|
82
|
+
fieldLocation: "query" as const,
|
|
83
|
+
pipes: [],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
]),
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
expect((args[0] as { birthday: Date }).birthday).toBeInstanceOf(Date);
|
|
90
|
+
expect((args[0] as { birthday: Date }).birthday.toISOString()).toBe(iso);
|
|
91
|
+
});
|
|
92
|
+
});
|