@stacksjs/bun-router 0.0.14 → 0.0.15
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 +261 -584
- package/bin/cli.ts +9 -0
- package/dist/auth.d.ts +134 -0
- package/dist/cache/lru-cache.d.ts +139 -0
- package/dist/cache/middleware-memoization.d.ts +151 -0
- package/dist/cache/route-cache-warmer.d.ts +161 -0
- package/dist/cache/sqlite-cache.d.ts +209 -0
- package/dist/cache/streaming-cache.d.ts +132 -0
- package/dist/chunk-j0e7z7hd.js +18053 -0
- package/dist/cli/colors.d.ts +15 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/middleware.d.ts +31 -0
- package/dist/cli/openapi.d.ts +17 -0
- package/dist/cli/router.d.ts +15 -0
- package/dist/cli/routes.d.ts +29 -0
- package/dist/cli/utils.d.ts +74 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3360 -0
- package/dist/config.d.ts +7 -0
- package/dist/container/container.d.ts +273 -0
- package/dist/container/contextual-binding.d.ts +240 -0
- package/dist/container/decorators.d.ts +141 -0
- package/dist/container/service-provider.d.ts +285 -0
- package/dist/development/hot-reload.d.ts +187 -0
- package/dist/development/index.d.ts +167 -0
- package/dist/development/performance-profiler.d.ts +217 -0
- package/dist/development/route-debugger.d.ts +154 -0
- package/dist/development/route-inspector.d.ts +211 -0
- package/dist/development/typescript-utilities.d.ts +209 -0
- package/dist/docs.d.ts +11 -0
- package/dist/errors/circuit-breaker.d.ts +195 -0
- package/dist/errors/error-handler.d.ts +88 -0
- package/dist/errors/error-reporting.d.ts +144 -0
- package/dist/errors/exceptions.d.ts +184 -0
- package/dist/errors/graceful-degradation.d.ts +154 -0
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/router-errors.d.ts +110 -0
- package/dist/file-serving/static-files.d.ts +143 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13968 -0
- package/dist/middleware/auth.d.ts +63 -0
- package/dist/middleware/content_security_policy.d.ts +53 -0
- package/dist/middleware/cors.d.ts +5 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +39 -0
- package/dist/middleware/file_security.d.ts +26 -0
- package/dist/middleware/file_upload.d.ts +38 -0
- package/dist/middleware/helmet.d.ts +54 -0
- package/dist/middleware/index.d.ts +44 -0
- package/dist/middleware/input_validation.d.ts +45 -0
- package/dist/middleware/json_body.d.ts +4 -0
- package/dist/middleware/performance_alerting.d.ts +87 -0
- package/dist/middleware/performance_dashboard.d.ts +87 -0
- package/dist/middleware/performance_monitor.d.ts +209 -0
- package/dist/middleware/pipeline.d.ts +131 -0
- package/dist/middleware/rate_limit.d.ts +36 -0
- package/dist/middleware/request_id.d.ts +4 -0
- package/dist/middleware/request_signing.d.ts +153 -0
- package/dist/middleware/request_tracer.d.ts +72 -0
- package/dist/middleware/response_cache.d.ts +97 -0
- package/dist/middleware/security.d.ts +69 -0
- package/dist/middleware/security_suite.d.ts +46 -0
- package/dist/middleware/session.d.ts +23 -0
- package/dist/model-binding/index.d.ts +2 -0
- package/dist/model-binding/model-middleware.d.ts +118 -0
- package/dist/model-binding/model-registry.d.ts +164 -0
- package/dist/model-binding.d.ts +185 -0
- package/dist/model-resolver-factory.d.ts +31 -0
- package/dist/observability/correlation.d.ts +187 -0
- package/dist/observability/health-checks.d.ts +186 -0
- package/dist/observability/index.d.ts +60 -0
- package/dist/observability/integration.d.ts +147 -0
- package/dist/observability/metrics.d.ts +184 -0
- package/dist/observability/tracing.d.ts +187 -0
- package/dist/optimization/bun-utilities.d.ts +224 -0
- package/dist/query-builder-integration.d.ts +36 -0
- package/dist/request/context.d.ts +34 -0
- package/dist/request/enhanced-request.d.ts +213 -0
- package/dist/request/macros.d.ts +300 -0
- package/dist/response/macros.d.ts +256 -0
- package/dist/response/response-factory.d.ts +133 -0
- package/dist/router/api-routes.d.ts +5 -0
- package/dist/router/file-based-routing.d.ts +118 -0
- package/dist/router/file-streaming.d.ts +5 -0
- package/dist/router/fluent-router.d.ts +315 -0
- package/dist/router/fluent-routing.d.ts +271 -0
- package/dist/router/group-organization.d.ts +5 -0
- package/dist/router/handler-resolver.d.ts +24 -0
- package/dist/router/http-methods.d.ts +10 -0
- package/dist/router/index.d.ts +114 -0
- package/dist/router/middleware-groups.d.ts +94 -0
- package/dist/router/middleware-integration.d.ts +112 -0
- package/dist/router/middleware.d.ts +5 -0
- package/dist/router/model-binding.d.ts +5 -0
- package/dist/router/optimized-route-matching.d.ts +6 -0
- package/dist/router/route-building.d.ts +5 -0
- package/dist/router/route-compiler.d.ts +153 -0
- package/dist/router/route-matching.d.ts +5 -0
- package/dist/router/route-trie.d.ts +114 -0
- package/dist/router/router.d.ts +317 -0
- package/dist/router/server.d.ts +5 -0
- package/dist/router/validation-integration.d.ts +170 -0
- package/dist/router/view-rendering.d.ts +5 -0
- package/dist/router/websocket.d.ts +5 -0
- package/dist/routing/route-caching.d.ts +129 -0
- package/dist/routing/route-throttling.d.ts +150 -0
- package/dist/routing/subdomain-routing.d.ts +206 -0
- package/dist/session/database-store.d.ts +51 -0
- package/dist/session/file-store.d.ts +22 -0
- package/dist/session/index.d.ts +106 -0
- package/dist/session/memory-store.d.ts +21 -0
- package/dist/session/redis-store.d.ts +31 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/sse-handler.d.ts +138 -0
- package/dist/streaming/stream-handler.d.ts +114 -0
- package/dist/testing/auth-testing.d.ts +156 -0
- package/dist/testing/file-upload-testing.d.ts +190 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/middleware-testing.d.ts +138 -0
- package/dist/testing/model-binding-testing.d.ts +186 -0
- package/dist/testing/performance-testing.d.ts +235 -0
- package/dist/testing/test-client.d.ts +117 -0
- package/dist/testing/test-request.d.ts +85 -0
- package/dist/testing/test-response.d.ts +90 -0
- package/dist/testing/types.d.ts +207 -0
- package/dist/testing/websocket-testing.d.ts +227 -0
- package/dist/types/controller-types.d.ts +208 -0
- package/dist/types/core.d.ts +543 -0
- package/dist/types/middleware-types.d.ts +227 -0
- package/dist/types/request-response-augmentation.d.ts +261 -0
- package/dist/types/route-inference.d.ts +168 -0
- package/dist/types.d.ts +1794 -0
- package/dist/url.d.ts +56 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/query-preservation.d.ts +48 -0
- package/dist/utils.d.ts +69 -0
- package/dist/validation/validator.d.ts +140 -0
- package/dist/websocket/clustering.d.ts +185 -0
- package/package.json +45 -44
- package/src/auth.ts +469 -0
- package/src/cache/lru-cache.ts +457 -0
- package/src/cache/middleware-memoization.ts +531 -0
- package/src/cache/route-cache-warmer.ts +486 -0
- package/src/cache/sqlite-cache.ts +783 -0
- package/src/cache/streaming-cache.ts +572 -0
- package/src/cli/colors.ts +29 -0
- package/src/cli/index.ts +287 -0
- package/src/cli/middleware.ts +291 -0
- package/src/cli/openapi.ts +407 -0
- package/src/cli/router.ts +188 -0
- package/src/cli/routes.ts +265 -0
- package/src/cli/utils.ts +531 -0
- package/src/cli.ts +5 -0
- package/src/config.ts +322 -0
- package/src/container/container.ts +740 -0
- package/src/container/contextual-binding.ts +603 -0
- package/src/container/decorators.ts +359 -0
- package/src/container/service-provider.ts +596 -0
- package/src/development/hot-reload.ts +673 -0
- package/src/development/index.ts +499 -0
- package/src/development/performance-profiler.ts +717 -0
- package/src/development/route-debugger.ts +527 -0
- package/src/development/route-inspector.ts +749 -0
- package/src/development/typescript-utilities.ts +682 -0
- package/src/docs.ts +397 -0
- package/src/errors/circuit-breaker.ts +732 -0
- package/src/errors/error-handler.ts +569 -0
- package/src/errors/error-reporting.ts +672 -0
- package/src/errors/exceptions.ts +536 -0
- package/src/errors/graceful-degradation.ts +621 -0
- package/src/errors/index.ts +21 -0
- package/src/errors/router-errors.ts +632 -0
- package/src/file-serving/static-files.ts +581 -0
- package/src/index.ts +14 -0
- package/src/middleware/auth.ts +220 -0
- package/src/middleware/content_security_policy.ts +215 -0
- package/src/middleware/cors.ts +74 -0
- package/src/middleware/csrf.ts +108 -0
- package/src/middleware/ddos_protection.ts +255 -0
- package/src/middleware/file_security.ts +191 -0
- package/src/middleware/file_upload.ts +275 -0
- package/src/middleware/helmet.ts +268 -0
- package/src/middleware/index.ts +117 -0
- package/src/middleware/input_validation.ts +449 -0
- package/src/middleware/json_body.ts +37 -0
- package/src/middleware/performance_alerting.ts +538 -0
- package/src/middleware/performance_dashboard.ts +661 -0
- package/src/middleware/performance_monitor.ts +943 -0
- package/src/middleware/pipeline.ts +489 -0
- package/src/middleware/rate_limit.ts +245 -0
- package/src/middleware/request_id.ts +36 -0
- package/src/middleware/request_signing.ts +636 -0
- package/src/middleware/request_tracer.ts +636 -0
- package/src/middleware/response_cache.ts +743 -0
- package/src/middleware/security.ts +482 -0
- package/src/middleware/security_suite.ts +257 -0
- package/src/middleware/session.ts +91 -0
- package/src/model-binding/index.ts +18 -0
- package/src/model-binding/model-middleware.ts +425 -0
- package/src/model-binding/model-registry.ts +550 -0
- package/src/model-binding.ts +370 -0
- package/src/model-resolver-factory.ts +106 -0
- package/src/observability/correlation.ts +691 -0
- package/src/observability/health-checks.ts +729 -0
- package/src/observability/index.ts +184 -0
- package/src/observability/integration.ts +548 -0
- package/src/observability/metrics.ts +753 -0
- package/src/observability/tracing.ts +638 -0
- package/src/optimization/bun-utilities.ts +778 -0
- package/src/query-builder-integration.ts +137 -0
- package/src/request/context.ts +62 -0
- package/src/request/enhanced-request.ts +857 -0
- package/src/request/macros.ts +688 -0
- package/src/response/macros.ts +665 -0
- package/src/response/response-factory.ts +596 -0
- package/src/router/api-routes.ts +243 -0
- package/src/router/file-based-routing.ts +690 -0
- package/src/router/file-streaming.ts +383 -0
- package/src/router/fluent-router.ts +927 -0
- package/src/router/fluent-routing.ts +797 -0
- package/src/router/group-organization.ts +213 -0
- package/src/router/handler-resolver.ts +291 -0
- package/src/router/http-methods.ts +377 -0
- package/src/router/index.ts +187 -0
- package/src/router/middleware-groups.ts +222 -0
- package/src/router/middleware-integration.ts +399 -0
- package/src/router/middleware.ts +231 -0
- package/src/router/model-binding.ts +215 -0
- package/src/router/optimized-route-matching.ts +253 -0
- package/src/router/route-building.ts +221 -0
- package/src/router/route-compiler.ts +702 -0
- package/src/router/route-matching.ts +349 -0
- package/src/router/route-trie.ts +464 -0
- package/src/router/router.ts +1604 -0
- package/src/router/server.ts +462 -0
- package/src/router/validation-integration.ts +443 -0
- package/src/router/view-rendering.ts +233 -0
- package/src/router/websocket.ts +100 -0
- package/src/routing/route-caching.ts +402 -0
- package/src/routing/route-throttling.ts +469 -0
- package/src/routing/subdomain-routing.ts +492 -0
- package/src/session/database-store.ts +109 -0
- package/src/session/file-store.ts +148 -0
- package/src/session/index.ts +244 -0
- package/src/session/memory-store.ts +88 -0
- package/src/session/redis-store.ts +93 -0
- package/src/streaming/index.ts +17 -0
- package/src/streaming/sse-handler.ts +482 -0
- package/src/streaming/stream-handler.ts +552 -0
- package/src/testing/auth-testing.ts +446 -0
- package/src/testing/file-upload-testing.ts +543 -0
- package/src/testing/index.ts +10 -0
- package/src/testing/middleware-testing.ts +322 -0
- package/src/testing/model-binding-testing.ts +645 -0
- package/src/testing/performance-testing.ts +738 -0
- package/src/testing/test-client.ts +310 -0
- package/src/testing/test-request.ts +315 -0
- package/src/testing/test-response.ts +332 -0
- package/src/testing/types.ts +224 -0
- package/src/testing/websocket-testing.ts +590 -0
- package/src/types/controller-types.ts +385 -0
- package/src/types/core.ts +683 -0
- package/src/types/middleware-types.ts +419 -0
- package/src/types/request-response-augmentation.ts +489 -0
- package/src/types/route-inference.ts +357 -0
- package/src/types.ts +2067 -0
- package/src/url.ts +126 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/query-preservation.ts +201 -0
- package/src/utils.ts +327 -0
- package/src/validation/validator.ts +685 -0
- package/src/websocket/clustering.ts +762 -0
- package/CHANGELOG.md +0 -770
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { Router } from '../router/router'
|
|
2
|
+
import type { EnhancedRequest } from '../types'
|
|
3
|
+
import type { TestResponseWrapper } from './test-response'
|
|
4
|
+
import type { TestClientConfig, TestRequestOptions } from './types'
|
|
5
|
+
import { TestRequestBuilder } from './test-request'
|
|
6
|
+
import { createTestResponse } from './test-response'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test client for making HTTP requests to router instances
|
|
10
|
+
*/
|
|
11
|
+
export class TestClient {
|
|
12
|
+
private router: Router
|
|
13
|
+
private config: TestClientConfig
|
|
14
|
+
private defaultHeaders: Record<string, string>
|
|
15
|
+
|
|
16
|
+
constructor(router: Router, config: TestClientConfig = {}) {
|
|
17
|
+
this.router = router
|
|
18
|
+
this.config = {
|
|
19
|
+
baseUrl: 'http://localhost:3000',
|
|
20
|
+
timeout: 5000,
|
|
21
|
+
followRedirects: true,
|
|
22
|
+
validateStatus: (status: number) => status >= 200 && status < 300,
|
|
23
|
+
...config,
|
|
24
|
+
}
|
|
25
|
+
this.defaultHeaders = config.defaultHeaders || {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Make a GET request
|
|
30
|
+
*/
|
|
31
|
+
async get(path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
32
|
+
return this.request('GET', path, options)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Make a POST request
|
|
37
|
+
*/
|
|
38
|
+
async post(path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
39
|
+
return this.request('POST', path, options)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Make a PUT request
|
|
44
|
+
*/
|
|
45
|
+
async put(path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
46
|
+
return this.request('PUT', path, options)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Make a PATCH request
|
|
51
|
+
*/
|
|
52
|
+
async patch(path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
53
|
+
return this.request('PATCH', path, options)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Make a DELETE request
|
|
58
|
+
*/
|
|
59
|
+
async delete(path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
60
|
+
return this.request('DELETE', path, options)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Make an OPTIONS request
|
|
65
|
+
*/
|
|
66
|
+
async options(path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
67
|
+
return this.request('OPTIONS', path, options)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Make a HEAD request
|
|
72
|
+
*/
|
|
73
|
+
async head(path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
74
|
+
return this.request('HEAD', path, options)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Make a request with custom method
|
|
79
|
+
*/
|
|
80
|
+
async request(method: string, path: string, options: TestRequestOptions = {}): Promise<TestResponseWrapper> {
|
|
81
|
+
// Merge default headers
|
|
82
|
+
const headers = { ...this.defaultHeaders, ...options.headers }
|
|
83
|
+
const requestOptions = { ...options, headers }
|
|
84
|
+
|
|
85
|
+
// Build the test request
|
|
86
|
+
const request = new TestRequestBuilder(method, path, requestOptions).build()
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Handle the request through the router
|
|
90
|
+
const response = await this.handleRequest(request)
|
|
91
|
+
return await createTestResponse(response)
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new Error(`Test request failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle request through the router
|
|
100
|
+
*/
|
|
101
|
+
private async handleRequest(request: EnhancedRequest): Promise<Response> {
|
|
102
|
+
// Find matching route
|
|
103
|
+
const url = new URL(request.url)
|
|
104
|
+
const matchResult = this.findMatchingRoute(request.method, url.pathname)
|
|
105
|
+
|
|
106
|
+
if (!matchResult) {
|
|
107
|
+
return new Response('Not Found', { status: 404 })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { route, params } = matchResult
|
|
111
|
+
|
|
112
|
+
// Update request with route params
|
|
113
|
+
request.params = { ...request.params, ...params }
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Execute middleware chain
|
|
117
|
+
const response = await this.executeMiddlewareChain(request, route)
|
|
118
|
+
return response || new Response('No response', { status: 500 })
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return new Response(
|
|
122
|
+
JSON.stringify({ error: error instanceof Error ? error.message : 'Internal Server Error' }),
|
|
123
|
+
{
|
|
124
|
+
status: 500,
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find matching route for the request
|
|
133
|
+
*/
|
|
134
|
+
private findMatchingRoute(method: string, pathname: string): { route: any, params: Record<string, string> } | null {
|
|
135
|
+
for (const route of this.router.routes) {
|
|
136
|
+
if (route.method.toLowerCase() !== method.toLowerCase()) {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Simple pattern matching for testing
|
|
141
|
+
const params = this.matchPath(route.path, pathname)
|
|
142
|
+
if (params !== null) {
|
|
143
|
+
return { route, params }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Simple path matching with parameter extraction
|
|
151
|
+
*/
|
|
152
|
+
private matchPath(pattern: string, pathname: string): Record<string, string> | null {
|
|
153
|
+
const patternParts = pattern.split('/')
|
|
154
|
+
const pathParts = pathname.split('/')
|
|
155
|
+
|
|
156
|
+
if (patternParts.length !== pathParts.length) {
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const params: Record<string, string> = {}
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
163
|
+
const patternPart = patternParts[i]
|
|
164
|
+
const pathPart = pathParts[i]
|
|
165
|
+
|
|
166
|
+
if (patternPart.startsWith(':')) {
|
|
167
|
+
// Parameter
|
|
168
|
+
const paramName = patternPart.slice(1)
|
|
169
|
+
params[paramName] = pathPart
|
|
170
|
+
}
|
|
171
|
+
else if (patternPart !== pathPart) {
|
|
172
|
+
// Literal mismatch
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return params
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Execute middleware chain and route handler
|
|
182
|
+
*/
|
|
183
|
+
private async executeMiddlewareChain(request: EnhancedRequest, route: any): Promise<Response> {
|
|
184
|
+
const middlewares = [...this.router.globalMiddleware, ...route.middleware]
|
|
185
|
+
let index = 0
|
|
186
|
+
|
|
187
|
+
const next = async (): Promise<Response | null> => {
|
|
188
|
+
if (index < middlewares.length) {
|
|
189
|
+
const middleware = middlewares[index++]
|
|
190
|
+
return await middleware(request, next)
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Execute route handler
|
|
194
|
+
return await this.executeHandler(route.handler, request)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const result = await next()
|
|
199
|
+
return result || new Response('No response', { status: 500 })
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Execute route handler
|
|
204
|
+
*/
|
|
205
|
+
private async executeHandler(handler: any, request: EnhancedRequest): Promise<Response> {
|
|
206
|
+
if (typeof handler === 'function') {
|
|
207
|
+
return await handler(request)
|
|
208
|
+
}
|
|
209
|
+
else if (typeof handler === 'string') {
|
|
210
|
+
// Handle string-based handlers (would normally load from file)
|
|
211
|
+
throw new TypeError(`String handlers not supported in test environment: ${handler}`)
|
|
212
|
+
}
|
|
213
|
+
else if (typeof handler === 'object' && handler.handle) {
|
|
214
|
+
// Handle class-based handlers
|
|
215
|
+
const instance = new (handler as any)()
|
|
216
|
+
return await instance.handle(request)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error('Invalid handler type')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Set default headers for all requests
|
|
224
|
+
*/
|
|
225
|
+
setDefaultHeaders(headers: Record<string, string>): TestClient {
|
|
226
|
+
this.defaultHeaders = { ...this.defaultHeaders, ...headers }
|
|
227
|
+
return this
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Set authorization header for all requests
|
|
232
|
+
*/
|
|
233
|
+
auth(token: string, type: 'Bearer' | 'Basic' = 'Bearer'): TestClient {
|
|
234
|
+
this.defaultHeaders.Authorization = `${type} ${token}`
|
|
235
|
+
return this
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Set user agent for all requests
|
|
240
|
+
*/
|
|
241
|
+
userAgent(userAgent: string): TestClient {
|
|
242
|
+
this.defaultHeaders['User-Agent'] = userAgent
|
|
243
|
+
return this
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Create a new test client with additional configuration
|
|
248
|
+
*/
|
|
249
|
+
withConfig(config: Partial<TestClientConfig>): TestClient {
|
|
250
|
+
return new TestClient(this.router, { ...this.config, ...config })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create a new test client with additional headers
|
|
255
|
+
*/
|
|
256
|
+
withHeaders(headers: Record<string, string>): TestClient {
|
|
257
|
+
return new TestClient(this.router, {
|
|
258
|
+
...this.config,
|
|
259
|
+
defaultHeaders: { ...this.defaultHeaders, ...headers },
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the router instance
|
|
265
|
+
*/
|
|
266
|
+
getRouter(): Router {
|
|
267
|
+
return this.router
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get client configuration
|
|
272
|
+
*/
|
|
273
|
+
getConfig(): TestClientConfig {
|
|
274
|
+
return { ...this.config }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Create a test client for a router
|
|
280
|
+
*/
|
|
281
|
+
export function createTestClient(router: Router, config?: TestClientConfig): TestClient {
|
|
282
|
+
return new TestClient(router, config)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Test client factory with fluent API
|
|
287
|
+
*/
|
|
288
|
+
export const testClient = {
|
|
289
|
+
/**
|
|
290
|
+
* Create a test client for a router
|
|
291
|
+
*/
|
|
292
|
+
for: (router: Router, config?: TestClientConfig): TestClient => createTestClient(router, config),
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Create a test client with specific configuration
|
|
296
|
+
*/
|
|
297
|
+
withConfig: (config: TestClientConfig): (router: Router) => TestClient => (router: Router): TestClient => createTestClient(router, config),
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create a test client with authentication
|
|
301
|
+
*/
|
|
302
|
+
withAuth: (token: string, type: 'Bearer' | 'Basic' = 'Bearer'): (router: Router) => TestClient => (router: Router): TestClient =>
|
|
303
|
+
createTestClient(router).auth(token, type),
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create a test client with custom headers
|
|
307
|
+
*/
|
|
308
|
+
withHeaders: (headers: Record<string, string>): (router: Router) => TestClient => (router: Router): TestClient =>
|
|
309
|
+
createTestClient(router).withHeaders(headers),
|
|
310
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import type { CookieAccessor, CookieOptions, EnhancedRequest, UploadedFile } from '../types'
|
|
2
|
+
import type { TestFile, TestRequestOptions } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a CookieAccessor from a plain Record<string, string>
|
|
6
|
+
*/
|
|
7
|
+
function createCookieAccessor(cookies: Record<string, string>): CookieAccessor {
|
|
8
|
+
const cookieStore = { ...cookies }
|
|
9
|
+
return {
|
|
10
|
+
get: (name: string) => cookieStore[name],
|
|
11
|
+
set: (name: string, value: string, _options?: CookieOptions) => {
|
|
12
|
+
cookieStore[name] = value
|
|
13
|
+
},
|
|
14
|
+
delete: (name: string, _options?: CookieOptions) => {
|
|
15
|
+
delete cookieStore[name]
|
|
16
|
+
},
|
|
17
|
+
getAll: () => ({ ...cookieStore }),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a mock EnhancedRequest for testing
|
|
23
|
+
*/
|
|
24
|
+
export class TestRequestBuilder {
|
|
25
|
+
private request: Partial<EnhancedRequest>
|
|
26
|
+
private url: URL
|
|
27
|
+
|
|
28
|
+
constructor(method: string, path: string, options: TestRequestOptions = {}) {
|
|
29
|
+
this.url = new URL(path, 'http://localhost:3000')
|
|
30
|
+
|
|
31
|
+
// Apply query parameters
|
|
32
|
+
if (options.query) {
|
|
33
|
+
Object.entries(options.query).forEach(([key, value]) => {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
value.forEach(v => this.url.searchParams.append(key, v))
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.url.searchParams.set(key, value)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.request = {
|
|
44
|
+
method,
|
|
45
|
+
url: this.url.toString(),
|
|
46
|
+
headers: new Headers(options.headers || {}),
|
|
47
|
+
params: {},
|
|
48
|
+
query: this.parseQuery(this.url.searchParams),
|
|
49
|
+
jsonBody: undefined,
|
|
50
|
+
formBody: options.body && typeof options.body === 'object' ? options.body : undefined,
|
|
51
|
+
files: options.files ? this.convertTestFiles(options.files) : undefined,
|
|
52
|
+
session: options.session,
|
|
53
|
+
user: options.user,
|
|
54
|
+
context: options.context || {},
|
|
55
|
+
requestId: `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
56
|
+
ip: '127.0.0.1',
|
|
57
|
+
userAgent: () => 'BunRouter-TestClient/1.0',
|
|
58
|
+
cookies: createCookieAccessor(options.cookies || {}),
|
|
59
|
+
startTime: Date.now(),
|
|
60
|
+
traceId: `trace-${Date.now()}`,
|
|
61
|
+
spanId: `span-${Date.now()}`,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle body based on content type
|
|
65
|
+
this.handleBody(options.body)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {
|
|
69
|
+
const query: Record<string, string | string[]> = {}
|
|
70
|
+
|
|
71
|
+
for (const [key, value] of searchParams.entries()) {
|
|
72
|
+
if (query[key]) {
|
|
73
|
+
if (Array.isArray(query[key])) {
|
|
74
|
+
(query[key] as string[]).push(value)
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
query[key] = [query[key] as string, value]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
query[key] = value
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return query
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private convertTestFiles(testFiles: TestFile[]): UploadedFile[] {
|
|
89
|
+
return testFiles.map(file => ({
|
|
90
|
+
fieldName: file.fieldName,
|
|
91
|
+
originalName: file.filename,
|
|
92
|
+
filename: `test-${Date.now()}-${file.filename}`,
|
|
93
|
+
path: `/tmp/test-uploads/${file.filename}`,
|
|
94
|
+
size: file.content instanceof ArrayBuffer
|
|
95
|
+
? file.content.byteLength
|
|
96
|
+
: new TextEncoder().encode(file.content.toString()).length,
|
|
97
|
+
mimetype: file.mimetype || 'application/octet-stream',
|
|
98
|
+
buffer: file.content instanceof ArrayBuffer
|
|
99
|
+
? file.content
|
|
100
|
+
: new TextEncoder().encode(file.content.toString()).buffer as ArrayBuffer,
|
|
101
|
+
}))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private handleBody(body: any): void {
|
|
105
|
+
if (!body)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
const contentType = this.request.headers?.get('content-type') || ''
|
|
109
|
+
|
|
110
|
+
if (contentType.includes('application/json') || typeof body === 'object') {
|
|
111
|
+
this.request.jsonBody = body
|
|
112
|
+
if (!this.request.headers?.has('content-type')) {
|
|
113
|
+
this.request.headers?.set('content-type', 'application/json')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
117
|
+
this.request.formBody = body
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Set route parameters
|
|
123
|
+
*/
|
|
124
|
+
params(params: Record<string, string>): TestRequestBuilder {
|
|
125
|
+
this.request.params = { ...this.request.params, ...params }
|
|
126
|
+
return this
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Set request headers
|
|
131
|
+
*/
|
|
132
|
+
headers(headers: Record<string, string>): TestRequestBuilder {
|
|
133
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
134
|
+
this.request.headers?.set(key, value)
|
|
135
|
+
})
|
|
136
|
+
return this
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set authorization header
|
|
141
|
+
*/
|
|
142
|
+
auth(token: string, type: 'Bearer' | 'Basic' = 'Bearer'): TestRequestBuilder {
|
|
143
|
+
this.request.headers?.set('Authorization', `${type} ${token}`)
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Set user context
|
|
149
|
+
*/
|
|
150
|
+
user(user: any): TestRequestBuilder {
|
|
151
|
+
this.request.user = user
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Set session data
|
|
157
|
+
*/
|
|
158
|
+
session(session: any): TestRequestBuilder {
|
|
159
|
+
this.request.session = session
|
|
160
|
+
return this
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set cookies
|
|
165
|
+
*/
|
|
166
|
+
cookies(cookies: Record<string, string>): TestRequestBuilder {
|
|
167
|
+
const existingCookies = this.request.cookies?.getAll() || {}
|
|
168
|
+
this.request.cookies = createCookieAccessor({ ...existingCookies, ...cookies })
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Set context data
|
|
174
|
+
*/
|
|
175
|
+
context(context: Record<string, any>): TestRequestBuilder {
|
|
176
|
+
this.request.context = { ...this.request.context, ...context }
|
|
177
|
+
return this
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Set JSON body
|
|
182
|
+
*/
|
|
183
|
+
json(data: any): TestRequestBuilder {
|
|
184
|
+
this.request.jsonBody = data
|
|
185
|
+
this.request.headers?.set('content-type', 'application/json')
|
|
186
|
+
return this
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Set form body
|
|
191
|
+
*/
|
|
192
|
+
form(data: Record<string, any>): TestRequestBuilder {
|
|
193
|
+
this.request.formBody = data
|
|
194
|
+
this.request.headers?.set('content-type', 'application/x-www-form-urlencoded')
|
|
195
|
+
return this
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Add uploaded files
|
|
200
|
+
*/
|
|
201
|
+
files(files: TestFile[]): TestRequestBuilder {
|
|
202
|
+
this.request.files = this.convertTestFiles(files)
|
|
203
|
+
this.request.headers?.set('content-type', 'multipart/form-data')
|
|
204
|
+
return this
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Set IP address
|
|
209
|
+
*/
|
|
210
|
+
ip(ip: string): TestRequestBuilder {
|
|
211
|
+
this.request.ip = ip
|
|
212
|
+
return this
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Set user agent
|
|
217
|
+
*/
|
|
218
|
+
userAgent(userAgent: string): TestRequestBuilder {
|
|
219
|
+
this.request.userAgent = () => userAgent
|
|
220
|
+
this.request.headers?.set('User-Agent', userAgent)
|
|
221
|
+
return this
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Set CSRF token
|
|
226
|
+
*/
|
|
227
|
+
csrf(token: string): TestRequestBuilder {
|
|
228
|
+
this.request.csrfToken = token
|
|
229
|
+
this.request.headers?.set('X-CSRF-Token', token)
|
|
230
|
+
return this
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Build the final request object
|
|
235
|
+
*/
|
|
236
|
+
build(): EnhancedRequest {
|
|
237
|
+
// Create a proper Request object
|
|
238
|
+
const init: RequestInit = {
|
|
239
|
+
method: this.request.method,
|
|
240
|
+
headers: this.request.headers,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (this.request.jsonBody) {
|
|
244
|
+
init.body = JSON.stringify(this.request.jsonBody)
|
|
245
|
+
}
|
|
246
|
+
else if (this.request.formBody) {
|
|
247
|
+
const formData = new FormData()
|
|
248
|
+
Object.entries(this.request.formBody).forEach(([key, value]) => {
|
|
249
|
+
formData.append(key, value as string)
|
|
250
|
+
})
|
|
251
|
+
init.body = formData
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const baseRequest = new Request(this.url.toString(), init)
|
|
255
|
+
|
|
256
|
+
// Create enhanced request by copying properties to a new object
|
|
257
|
+
// We can't use Proxy due to Request immutability constraints in Bun
|
|
258
|
+
const enhancedRequest = Object.assign(baseRequest, {
|
|
259
|
+
params: this.request.params || {},
|
|
260
|
+
query: this.request.query || {},
|
|
261
|
+
jsonBody: this.request.jsonBody,
|
|
262
|
+
formBody: this.request.formBody,
|
|
263
|
+
files: this.request.files,
|
|
264
|
+
session: this.request.session,
|
|
265
|
+
user: this.request.user,
|
|
266
|
+
context: this.request.context || {},
|
|
267
|
+
requestId: this.request.requestId,
|
|
268
|
+
ip: this.request.ip,
|
|
269
|
+
userAgent: this.request.userAgent,
|
|
270
|
+
cookies: this.request.cookies || {},
|
|
271
|
+
startTime: this.request.startTime,
|
|
272
|
+
traceId: this.request.traceId,
|
|
273
|
+
spanId: this.request.spanId,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return enhancedRequest as EnhancedRequest
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Helper functions for creating test requests
|
|
282
|
+
*/
|
|
283
|
+
export const testRequest = {
|
|
284
|
+
get: (path: string, options?: TestRequestOptions): TestRequestBuilder =>
|
|
285
|
+
new TestRequestBuilder('GET', path, options),
|
|
286
|
+
|
|
287
|
+
post: (path: string, options?: TestRequestOptions): TestRequestBuilder =>
|
|
288
|
+
new TestRequestBuilder('POST', path, options),
|
|
289
|
+
|
|
290
|
+
put: (path: string, options?: TestRequestOptions): TestRequestBuilder =>
|
|
291
|
+
new TestRequestBuilder('PUT', path, options),
|
|
292
|
+
|
|
293
|
+
patch: (path: string, options?: TestRequestOptions): TestRequestBuilder =>
|
|
294
|
+
new TestRequestBuilder('PATCH', path, options),
|
|
295
|
+
|
|
296
|
+
delete: (path: string, options?: TestRequestOptions): TestRequestBuilder =>
|
|
297
|
+
new TestRequestBuilder('DELETE', path, options),
|
|
298
|
+
|
|
299
|
+
options: (path: string, options?: TestRequestOptions): TestRequestBuilder =>
|
|
300
|
+
new TestRequestBuilder('OPTIONS', path, options),
|
|
301
|
+
|
|
302
|
+
head: (path: string, options?: TestRequestOptions): TestRequestBuilder =>
|
|
303
|
+
new TestRequestBuilder('HEAD', path, options),
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create a mock request with minimal setup
|
|
308
|
+
*/
|
|
309
|
+
export function createMockRequest(
|
|
310
|
+
method: string = 'GET',
|
|
311
|
+
path: string = '/',
|
|
312
|
+
options: TestRequestOptions = {},
|
|
313
|
+
): EnhancedRequest {
|
|
314
|
+
return new TestRequestBuilder(method, path, options).build()
|
|
315
|
+
}
|