@hazeljs/core 0.2.0-alpha.1
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/LICENSE +192 -0
- package/README.md +560 -0
- package/dist/__tests__/container.test.d.ts +2 -0
- package/dist/__tests__/container.test.d.ts.map +1 -0
- package/dist/__tests__/container.test.js +454 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +1237 -0
- package/dist/__tests__/errors/http.error.test.d.ts +2 -0
- package/dist/__tests__/errors/http.error.test.d.ts.map +1 -0
- package/dist/__tests__/errors/http.error.test.js +117 -0
- package/dist/__tests__/filters/exception-filter.test.d.ts +2 -0
- package/dist/__tests__/filters/exception-filter.test.d.ts.map +1 -0
- package/dist/__tests__/filters/exception-filter.test.js +135 -0
- package/dist/__tests__/filters/http-exception.filter.test.d.ts +2 -0
- package/dist/__tests__/filters/http-exception.filter.test.d.ts.map +1 -0
- package/dist/__tests__/filters/http-exception.filter.test.js +119 -0
- package/dist/__tests__/hazel-app.test.d.ts +2 -0
- package/dist/__tests__/hazel-app.test.d.ts.map +1 -0
- package/dist/__tests__/hazel-app.test.js +810 -0
- package/dist/__tests__/hazel-module.test.d.ts +2 -0
- package/dist/__tests__/hazel-module.test.d.ts.map +1 -0
- package/dist/__tests__/hazel-module.test.js +408 -0
- package/dist/__tests__/hazel-response.test.d.ts +2 -0
- package/dist/__tests__/hazel-response.test.d.ts.map +1 -0
- package/dist/__tests__/hazel-response.test.js +138 -0
- package/dist/__tests__/health.test.d.ts +2 -0
- package/dist/__tests__/health.test.d.ts.map +1 -0
- package/dist/__tests__/health.test.js +147 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +239 -0
- package/dist/__tests__/interceptors/interceptor.test.d.ts +2 -0
- package/dist/__tests__/interceptors/interceptor.test.d.ts.map +1 -0
- package/dist/__tests__/interceptors/interceptor.test.js +166 -0
- package/dist/__tests__/logger.test.d.ts +2 -0
- package/dist/__tests__/logger.test.d.ts.map +1 -0
- package/dist/__tests__/logger.test.js +141 -0
- package/dist/__tests__/middleware/cors.test.d.ts +2 -0
- package/dist/__tests__/middleware/cors.test.d.ts.map +1 -0
- package/dist/__tests__/middleware/cors.test.js +129 -0
- package/dist/__tests__/middleware/csrf.test.d.ts +2 -0
- package/dist/__tests__/middleware/csrf.test.d.ts.map +1 -0
- package/dist/__tests__/middleware/csrf.test.js +247 -0
- package/dist/__tests__/middleware/global-middleware.test.d.ts +2 -0
- package/dist/__tests__/middleware/global-middleware.test.d.ts.map +1 -0
- package/dist/__tests__/middleware/global-middleware.test.js +259 -0
- package/dist/__tests__/middleware/rate-limit.test.d.ts +2 -0
- package/dist/__tests__/middleware/rate-limit.test.d.ts.map +1 -0
- package/dist/__tests__/middleware/rate-limit.test.js +264 -0
- package/dist/__tests__/middleware/security-headers.test.d.ts +2 -0
- package/dist/__tests__/middleware/security-headers.test.d.ts.map +1 -0
- package/dist/__tests__/middleware/security-headers.test.js +229 -0
- package/dist/__tests__/middleware/timeout.test.d.ts +2 -0
- package/dist/__tests__/middleware/timeout.test.d.ts.map +1 -0
- package/dist/__tests__/middleware/timeout.test.js +132 -0
- package/dist/__tests__/middleware.test.d.ts +2 -0
- package/dist/__tests__/middleware.test.d.ts.map +1 -0
- package/dist/__tests__/middleware.test.js +180 -0
- package/dist/__tests__/pipes/pipe.test.d.ts +2 -0
- package/dist/__tests__/pipes/pipe.test.d.ts.map +1 -0
- package/dist/__tests__/pipes/pipe.test.js +245 -0
- package/dist/__tests__/pipes/validation.pipe.test.d.ts +2 -0
- package/dist/__tests__/pipes/validation.pipe.test.d.ts.map +1 -0
- package/dist/__tests__/pipes/validation.pipe.test.js +297 -0
- package/dist/__tests__/request-parser.test.d.ts +2 -0
- package/dist/__tests__/request-parser.test.d.ts.map +1 -0
- package/dist/__tests__/request-parser.test.js +182 -0
- package/dist/__tests__/router.test.d.ts +2 -0
- package/dist/__tests__/router.test.d.ts.map +1 -0
- package/dist/__tests__/router.test.js +1183 -0
- package/dist/__tests__/routing/route-matcher.test.d.ts +2 -0
- package/dist/__tests__/routing/route-matcher.test.d.ts.map +1 -0
- package/dist/__tests__/routing/route-matcher.test.js +219 -0
- package/dist/__tests__/routing/version.decorator.test.d.ts +2 -0
- package/dist/__tests__/routing/version.decorator.test.d.ts.map +1 -0
- package/dist/__tests__/routing/version.decorator.test.js +298 -0
- package/dist/__tests__/service.test.d.ts +2 -0
- package/dist/__tests__/service.test.d.ts.map +1 -0
- package/dist/__tests__/service.test.js +121 -0
- package/dist/__tests__/shutdown.test.d.ts +2 -0
- package/dist/__tests__/shutdown.test.d.ts.map +1 -0
- package/dist/__tests__/shutdown.test.js +250 -0
- package/dist/__tests__/testing/testing.module.test.d.ts +2 -0
- package/dist/__tests__/testing/testing.module.test.d.ts.map +1 -0
- package/dist/__tests__/testing/testing.module.test.js +370 -0
- package/dist/__tests__/upload/file-upload.test.d.ts +2 -0
- package/dist/__tests__/upload/file-upload.test.d.ts.map +1 -0
- package/dist/__tests__/upload/file-upload.test.js +498 -0
- package/dist/__tests__/utils/sanitize.test.d.ts +2 -0
- package/dist/__tests__/utils/sanitize.test.d.ts.map +1 -0
- package/dist/__tests__/utils/sanitize.test.js +291 -0
- package/dist/__tests__/validator.test.d.ts +2 -0
- package/dist/__tests__/validator.test.d.ts.map +1 -0
- package/dist/__tests__/validator.test.js +300 -0
- package/dist/container.d.ts +80 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +271 -0
- package/dist/decorators.d.ts +166 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +538 -0
- package/dist/errors/http.error.d.ts +34 -0
- package/dist/errors/http.error.d.ts.map +1 -0
- package/dist/errors/http.error.js +69 -0
- package/dist/filters/exception-filter.d.ts +39 -0
- package/dist/filters/exception-filter.d.ts.map +1 -0
- package/dist/filters/exception-filter.js +38 -0
- package/dist/filters/http-exception.filter.d.ts +9 -0
- package/dist/filters/http-exception.filter.d.ts.map +1 -0
- package/dist/filters/http-exception.filter.js +42 -0
- package/dist/hazel-app.d.ts +94 -0
- package/dist/hazel-app.d.ts.map +1 -0
- package/dist/hazel-app.js +516 -0
- package/dist/hazel-module.d.ts +29 -0
- package/dist/hazel-module.d.ts.map +1 -0
- package/dist/hazel-module.js +137 -0
- package/dist/hazel-response.d.ts +25 -0
- package/dist/hazel-response.d.ts.map +1 -0
- package/dist/hazel-response.js +89 -0
- package/dist/health.d.ts +73 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +174 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +159 -0
- package/dist/interceptors/interceptor.d.ts +30 -0
- package/dist/interceptors/interceptor.d.ts.map +1 -0
- package/dist/interceptors/interceptor.js +71 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +261 -0
- package/dist/middleware/cors.middleware.d.ts +44 -0
- package/dist/middleware/cors.middleware.d.ts.map +1 -0
- package/dist/middleware/cors.middleware.js +118 -0
- package/dist/middleware/csrf.middleware.d.ts +82 -0
- package/dist/middleware/csrf.middleware.d.ts.map +1 -0
- package/dist/middleware/csrf.middleware.js +183 -0
- package/dist/middleware/global-middleware.d.ts +111 -0
- package/dist/middleware/global-middleware.d.ts.map +1 -0
- package/dist/middleware/global-middleware.js +179 -0
- package/dist/middleware/rate-limit.middleware.d.ts +73 -0
- package/dist/middleware/rate-limit.middleware.d.ts.map +1 -0
- package/dist/middleware/rate-limit.middleware.js +124 -0
- package/dist/middleware/security-headers.middleware.d.ts +76 -0
- package/dist/middleware/security-headers.middleware.d.ts.map +1 -0
- package/dist/middleware/security-headers.middleware.js +123 -0
- package/dist/middleware/timeout.middleware.d.ts +25 -0
- package/dist/middleware/timeout.middleware.d.ts.map +1 -0
- package/dist/middleware/timeout.middleware.js +74 -0
- package/dist/middleware.d.ts +13 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +47 -0
- package/dist/pipes/pipe.d.ts +50 -0
- package/dist/pipes/pipe.d.ts.map +1 -0
- package/dist/pipes/pipe.js +96 -0
- package/dist/pipes/validation.pipe.d.ts +6 -0
- package/dist/pipes/validation.pipe.d.ts.map +1 -0
- package/dist/pipes/validation.pipe.js +61 -0
- package/dist/request-context.d.ts +22 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +2 -0
- package/dist/request-parser.d.ts +7 -0
- package/dist/request-parser.d.ts.map +1 -0
- package/dist/request-parser.js +60 -0
- package/dist/router.d.ts +33 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +506 -0
- package/dist/routing/route-matcher.d.ts +39 -0
- package/dist/routing/route-matcher.d.ts.map +1 -0
- package/dist/routing/route-matcher.js +93 -0
- package/dist/routing/version.decorator.d.ts +36 -0
- package/dist/routing/version.decorator.d.ts.map +1 -0
- package/dist/routing/version.decorator.js +89 -0
- package/dist/service.d.ts +9 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +39 -0
- package/dist/shutdown.d.ts +32 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/shutdown.js +109 -0
- package/dist/testing/testing.module.d.ts +83 -0
- package/dist/testing/testing.module.d.ts.map +1 -0
- package/dist/testing/testing.module.js +164 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/upload/file-upload.d.ts +75 -0
- package/dist/upload/file-upload.d.ts.map +1 -0
- package/dist/upload/file-upload.js +261 -0
- package/dist/utils/sanitize.d.ts +45 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +165 -0
- package/dist/validator.d.ts +7 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +119 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const router_1 = require("../router");
|
|
4
|
+
const container_1 = require("../container");
|
|
5
|
+
const pipe_1 = require("../pipes/pipe");
|
|
6
|
+
const http_error_1 = require("../errors/http.error");
|
|
7
|
+
require("reflect-metadata");
|
|
8
|
+
// Mock logger
|
|
9
|
+
jest.mock('../logger', () => ({
|
|
10
|
+
info: jest.fn(),
|
|
11
|
+
debug: jest.fn(),
|
|
12
|
+
warn: jest.fn(),
|
|
13
|
+
error: jest.fn(),
|
|
14
|
+
isDebugEnabled: jest.fn().mockReturnValue(false),
|
|
15
|
+
}));
|
|
16
|
+
// Mock RequestParser
|
|
17
|
+
jest.mock('../request-parser', () => ({
|
|
18
|
+
RequestParser: {
|
|
19
|
+
parseRequest: jest.fn().mockResolvedValue({
|
|
20
|
+
params: {},
|
|
21
|
+
query: {},
|
|
22
|
+
body: {},
|
|
23
|
+
headers: {},
|
|
24
|
+
method: 'GET',
|
|
25
|
+
url: '/',
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
describe('Router', () => {
|
|
30
|
+
let router;
|
|
31
|
+
let container;
|
|
32
|
+
let mockReq;
|
|
33
|
+
let mockRes;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
container = container_1.Container.createTestInstance();
|
|
36
|
+
router = new router_1.Router(container);
|
|
37
|
+
mockReq = {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
url: '/test',
|
|
40
|
+
headers: {},
|
|
41
|
+
params: {},
|
|
42
|
+
query: {},
|
|
43
|
+
body: {},
|
|
44
|
+
};
|
|
45
|
+
mockRes = {
|
|
46
|
+
setHeader: jest.fn(),
|
|
47
|
+
status: jest.fn().mockReturnThis(),
|
|
48
|
+
json: jest.fn(),
|
|
49
|
+
send: jest.fn(),
|
|
50
|
+
end: jest.fn(),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
describe('normalizePath', () => {
|
|
54
|
+
it('should normalize paths correctly', () => {
|
|
55
|
+
// Access private method via any
|
|
56
|
+
const normalize = router.normalizePath.bind(router);
|
|
57
|
+
expect(normalize('/test')).toBe('/test');
|
|
58
|
+
expect(normalize('test')).toBe('/test');
|
|
59
|
+
expect(normalize('/test/')).toBe('/test');
|
|
60
|
+
expect(normalize('/')).toBe('/');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('matchPath', () => {
|
|
64
|
+
it('should match exact paths', () => {
|
|
65
|
+
const matchPath = router.matchPath.bind(router);
|
|
66
|
+
expect(matchPath('/users', '/users')).toBe(true);
|
|
67
|
+
expect(matchPath('/users', '/posts')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('should match paths with parameters', () => {
|
|
70
|
+
const matchPath = router.matchPath.bind(router);
|
|
71
|
+
expect(matchPath('/users/123', '/users/:id')).toBe(true);
|
|
72
|
+
expect(matchPath('/users/123/posts/456', '/users/:userId/posts/:postId')).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
it('should not match paths with different lengths', () => {
|
|
75
|
+
const matchPath = router.matchPath.bind(router);
|
|
76
|
+
expect(matchPath('/users/123', '/users')).toBe(false);
|
|
77
|
+
expect(matchPath('/users', '/users/123')).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('extractParams', () => {
|
|
81
|
+
it('should extract route parameters', () => {
|
|
82
|
+
const extractParams = router.extractParams.bind(router);
|
|
83
|
+
const params = extractParams('/users/123', '/users/:id');
|
|
84
|
+
expect(params).toEqual({ id: '123' });
|
|
85
|
+
});
|
|
86
|
+
it('should extract multiple parameters', () => {
|
|
87
|
+
const extractParams = router.extractParams.bind(router);
|
|
88
|
+
const params = extractParams('/users/123/posts/456', '/users/:userId/posts/:postId');
|
|
89
|
+
expect(params).toEqual({ userId: '123', postId: '456' });
|
|
90
|
+
});
|
|
91
|
+
it('should return empty object for paths without parameters', () => {
|
|
92
|
+
const extractParams = router.extractParams.bind(router);
|
|
93
|
+
const params = extractParams('/users', '/users');
|
|
94
|
+
expect(params).toEqual({});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('createRoutePattern', () => {
|
|
98
|
+
it('should create pattern for root path', () => {
|
|
99
|
+
const createPattern = router.createRoutePattern.bind(router);
|
|
100
|
+
const pattern = createPattern('/');
|
|
101
|
+
expect(pattern.test('/')).toBe(true);
|
|
102
|
+
expect(pattern.test('')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it('should create pattern for static paths', () => {
|
|
105
|
+
const createPattern = router.createRoutePattern.bind(router);
|
|
106
|
+
const pattern = createPattern('/users');
|
|
107
|
+
expect(pattern.test('/users')).toBe(true);
|
|
108
|
+
expect(pattern.test('/users/')).toBe(true);
|
|
109
|
+
expect(pattern.test('/posts')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
it('should create pattern for parameterized paths', () => {
|
|
112
|
+
const createPattern = router.createRoutePattern.bind(router);
|
|
113
|
+
const pattern = createPattern('/users/:id');
|
|
114
|
+
expect(pattern.test('/users/123')).toBe(true);
|
|
115
|
+
expect(pattern.test('/users/abc')).toBe(true);
|
|
116
|
+
expect(pattern.test('/users')).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('addRoute methods', () => {
|
|
120
|
+
it('should add GET route', () => {
|
|
121
|
+
const handler = jest.fn();
|
|
122
|
+
router.get('/test', [handler]);
|
|
123
|
+
const routes = router.routes;
|
|
124
|
+
expect(routes.has('GET /test')).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
it('should add POST route', () => {
|
|
127
|
+
const handler = jest.fn();
|
|
128
|
+
router.post('/test', [handler]);
|
|
129
|
+
const routes = router.routes;
|
|
130
|
+
expect(routes.has('POST /test')).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it('should add PUT route', () => {
|
|
133
|
+
const handler = jest.fn();
|
|
134
|
+
router.put('/test', [handler]);
|
|
135
|
+
const routes = router.routes;
|
|
136
|
+
expect(routes.has('PUT /test')).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('should add DELETE route', () => {
|
|
139
|
+
const handler = jest.fn();
|
|
140
|
+
router.delete('/test', [handler]);
|
|
141
|
+
const routes = router.routes;
|
|
142
|
+
expect(routes.has('DELETE /test')).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('match', () => {
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
const handler = jest.fn();
|
|
148
|
+
router.get('/users/:id', [handler]);
|
|
149
|
+
// Also add to method-specific cache
|
|
150
|
+
const methodRoutes = router.routesByMethod.get('GET');
|
|
151
|
+
if (methodRoutes) {
|
|
152
|
+
methodRoutes.set('/users/:id', [handler]);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
it('should match registered route', async () => {
|
|
156
|
+
const context = {
|
|
157
|
+
params: {},
|
|
158
|
+
query: {},
|
|
159
|
+
body: {},
|
|
160
|
+
headers: {},
|
|
161
|
+
method: 'GET',
|
|
162
|
+
url: '/users/123',
|
|
163
|
+
};
|
|
164
|
+
const match = await router.match('GET', '/users/123', context);
|
|
165
|
+
expect(match).not.toBeNull();
|
|
166
|
+
expect(match?.context.params).toEqual({ id: '123' });
|
|
167
|
+
});
|
|
168
|
+
it('should return null for unmatched route', async () => {
|
|
169
|
+
const context = {
|
|
170
|
+
params: {},
|
|
171
|
+
query: {},
|
|
172
|
+
body: {},
|
|
173
|
+
headers: {},
|
|
174
|
+
method: 'GET',
|
|
175
|
+
url: '/posts/123',
|
|
176
|
+
};
|
|
177
|
+
const match = await router.match('GET', '/posts/123', context);
|
|
178
|
+
expect(match).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
it('should return null for wrong method', async () => {
|
|
181
|
+
const context = {
|
|
182
|
+
params: {},
|
|
183
|
+
query: {},
|
|
184
|
+
body: {},
|
|
185
|
+
headers: {},
|
|
186
|
+
method: 'POST',
|
|
187
|
+
url: '/users/123',
|
|
188
|
+
};
|
|
189
|
+
const match = await router.match('POST', '/users/123', context);
|
|
190
|
+
expect(match).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
it('should handle query strings in URL', async () => {
|
|
193
|
+
const context = {
|
|
194
|
+
params: {},
|
|
195
|
+
query: { filter: 'active' },
|
|
196
|
+
body: {},
|
|
197
|
+
headers: {},
|
|
198
|
+
method: 'GET',
|
|
199
|
+
url: '/users/123?filter=active',
|
|
200
|
+
};
|
|
201
|
+
const match = await router.match('GET', '/users/123?filter=active', context);
|
|
202
|
+
expect(match).not.toBeNull();
|
|
203
|
+
expect(match?.context.params).toEqual({ id: '123' });
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('handleRequest', () => {
|
|
207
|
+
it('should return 404 for unmatched routes', async () => {
|
|
208
|
+
mockReq.method = 'GET';
|
|
209
|
+
mockReq.url = '/nonexistent';
|
|
210
|
+
await router.handleRequest(mockReq, mockRes);
|
|
211
|
+
expect(mockRes.status).toHaveBeenCalledWith(404);
|
|
212
|
+
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Not Found' });
|
|
213
|
+
});
|
|
214
|
+
it('should handle errors gracefully', async () => {
|
|
215
|
+
// Mock match to throw error
|
|
216
|
+
jest.spyOn(router, 'match').mockRejectedValue(new Error('Test error'));
|
|
217
|
+
mockReq.method = 'GET';
|
|
218
|
+
mockReq.url = '/test';
|
|
219
|
+
await router.handleRequest(mockReq, mockRes);
|
|
220
|
+
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
221
|
+
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Test error' });
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('registerController', () => {
|
|
225
|
+
it('should register controller with routes', () => {
|
|
226
|
+
class TestController {
|
|
227
|
+
getTest() {
|
|
228
|
+
return { message: 'test' };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Set up metadata
|
|
232
|
+
Reflect.defineMetadata('hazel:controller', { path: '/api' }, TestController);
|
|
233
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/test', propertyKey: 'getTest' }], TestController);
|
|
234
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getTest');
|
|
235
|
+
router.registerController(TestController);
|
|
236
|
+
const routes = router.routes;
|
|
237
|
+
expect(routes.has('GET /api/test')).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
it('should handle controller without base path', () => {
|
|
240
|
+
class TestController {
|
|
241
|
+
getRoot() {
|
|
242
|
+
return { message: 'root' };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
Reflect.defineMetadata('hazel:controller', {}, TestController);
|
|
246
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/', propertyKey: 'getRoot' }], TestController);
|
|
247
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getRoot');
|
|
248
|
+
router.registerController(TestController);
|
|
249
|
+
const routes = router.routes;
|
|
250
|
+
expect(routes.has('GET /')).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
it('should register multiple routes for same controller', () => {
|
|
253
|
+
class TestController {
|
|
254
|
+
getUsers() {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
createUser() {
|
|
258
|
+
return { id: 1 };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
Reflect.defineMetadata('hazel:controller', { path: '/users' }, TestController);
|
|
262
|
+
Reflect.defineMetadata('hazel:routes', [
|
|
263
|
+
{ method: 'GET', path: '/', propertyKey: 'getUsers' },
|
|
264
|
+
{ method: 'POST', path: '/', propertyKey: 'createUser' },
|
|
265
|
+
], TestController);
|
|
266
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getUsers');
|
|
267
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'createUser');
|
|
268
|
+
router.registerController(TestController);
|
|
269
|
+
const routes = router.routes;
|
|
270
|
+
expect(routes.has('GET /users')).toBe(true);
|
|
271
|
+
expect(routes.has('POST /users')).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe('pipes and interceptors', () => {
|
|
275
|
+
it('should apply pipes to route parameters', async () => {
|
|
276
|
+
class TestPipe {
|
|
277
|
+
async transform(value) {
|
|
278
|
+
return `transformed-${value}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
class TestController {
|
|
282
|
+
getUser() {
|
|
283
|
+
return { id: 1 };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
container.registerProvider({ token: TestPipe, useClass: TestPipe });
|
|
287
|
+
Reflect.defineMetadata('hazel:controller', { path: '/users' }, TestController);
|
|
288
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/:id', propertyKey: 'getUser', pipes: [{ type: TestPipe }] }], TestController);
|
|
289
|
+
Reflect.defineMetadata('hazel:inject', ['param:id'], TestController, 'getUser');
|
|
290
|
+
router.registerController(TestController);
|
|
291
|
+
const context = { params: { id: '123' }, query: {}, body: {}, headers: {}, method: 'GET', url: '/users/123' };
|
|
292
|
+
const route = await router.match('GET', '/users/123', context);
|
|
293
|
+
expect(route).toBeDefined();
|
|
294
|
+
});
|
|
295
|
+
it('should apply interceptors to routes', async () => {
|
|
296
|
+
class TestInterceptor {
|
|
297
|
+
async intercept(context, next) {
|
|
298
|
+
const result = await next();
|
|
299
|
+
return { intercepted: true, result };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
class TestController {
|
|
303
|
+
getUser() {
|
|
304
|
+
return { id: 1 };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
container.registerProvider({ token: TestInterceptor, useClass: TestInterceptor });
|
|
308
|
+
Reflect.defineMetadata('hazel:controller', { path: '/users' }, TestController);
|
|
309
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/:id', propertyKey: 'getUser', interceptors: [{ type: TestInterceptor }] }], TestController);
|
|
310
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getUser');
|
|
311
|
+
router.registerController(TestController);
|
|
312
|
+
const context = { params: { id: '123' }, query: {}, body: {}, headers: {}, method: 'GET', url: '/users/123' };
|
|
313
|
+
const route = await router.match('GET', '/users/123', context);
|
|
314
|
+
expect(route).toBeDefined();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
describe('parameter injection', () => {
|
|
318
|
+
it('should inject body parameter', async () => {
|
|
319
|
+
class TestController {
|
|
320
|
+
createUser(body) {
|
|
321
|
+
return body;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
Reflect.defineMetadata('hazel:controller', { path: '/users' }, TestController);
|
|
325
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'POST', path: '/', propertyKey: 'createUser' }], TestController);
|
|
326
|
+
Reflect.defineMetadata('hazel:inject', ['body'], TestController, 'createUser');
|
|
327
|
+
router.registerController(TestController);
|
|
328
|
+
const routes = router.routes;
|
|
329
|
+
expect(routes.has('POST /users')).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
it('should inject query parameter', async () => {
|
|
332
|
+
class TestController {
|
|
333
|
+
searchUsers(query) {
|
|
334
|
+
return query;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
Reflect.defineMetadata('hazel:controller', { path: '/users' }, TestController);
|
|
338
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/search', propertyKey: 'searchUsers' }], TestController);
|
|
339
|
+
Reflect.defineMetadata('hazel:inject', ['query'], TestController, 'searchUsers');
|
|
340
|
+
router.registerController(TestController);
|
|
341
|
+
const routes = router.routes;
|
|
342
|
+
expect(routes.has('GET /users/search')).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
it('should inject specific param', async () => {
|
|
345
|
+
class TestController {
|
|
346
|
+
getUser(id) {
|
|
347
|
+
return { id };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
Reflect.defineMetadata('hazel:controller', { path: '/users' }, TestController);
|
|
351
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/:id', propertyKey: 'getUser' }], TestController);
|
|
352
|
+
Reflect.defineMetadata('hazel:inject', ['param:id'], TestController, 'getUser');
|
|
353
|
+
router.registerController(TestController);
|
|
354
|
+
const routes = router.routes;
|
|
355
|
+
expect(routes.has('GET /users/:id')).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
it('should inject request and response objects', async () => {
|
|
358
|
+
class TestController {
|
|
359
|
+
handleRequest(req, res) {
|
|
360
|
+
return { req, res };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
364
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/', propertyKey: 'handleRequest' }], TestController);
|
|
365
|
+
Reflect.defineMetadata('hazel:inject', ['request', 'response'], TestController, 'handleRequest');
|
|
366
|
+
router.registerController(TestController);
|
|
367
|
+
const routes = router.routes;
|
|
368
|
+
expect(routes.has('GET /test')).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
describe('error handling', () => {
|
|
372
|
+
it('should handle controller without routes metadata', () => {
|
|
373
|
+
class TestController {
|
|
374
|
+
}
|
|
375
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
376
|
+
router.registerController(TestController);
|
|
377
|
+
const routes = router.routes;
|
|
378
|
+
expect(routes.size).toBeGreaterThanOrEqual(0);
|
|
379
|
+
});
|
|
380
|
+
it('should handle route without injections', () => {
|
|
381
|
+
class TestController {
|
|
382
|
+
getTest() {
|
|
383
|
+
return 'test';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
387
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/', propertyKey: 'getTest' }], TestController);
|
|
388
|
+
router.registerController(TestController);
|
|
389
|
+
const routes = router.routes;
|
|
390
|
+
expect(routes.has('GET /test')).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
describe('route normalization', () => {
|
|
394
|
+
it('should normalize paths with trailing slashes', () => {
|
|
395
|
+
class TestController {
|
|
396
|
+
getTest() {
|
|
397
|
+
return 'test';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test/' }, TestController);
|
|
401
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/', propertyKey: 'getTest' }], TestController);
|
|
402
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getTest');
|
|
403
|
+
router.registerController(TestController);
|
|
404
|
+
const routes = router.routes;
|
|
405
|
+
// Path normalization may keep the trailing slash
|
|
406
|
+
expect(routes.has('GET /test') || routes.has('GET /test/')).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
it('should handle empty controller path', () => {
|
|
409
|
+
class TestController {
|
|
410
|
+
getRoot() {
|
|
411
|
+
return 'root';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
Reflect.defineMetadata('hazel:controller', { path: '' }, TestController);
|
|
415
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/test', propertyKey: 'getRoot' }], TestController);
|
|
416
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getRoot');
|
|
417
|
+
router.registerController(TestController);
|
|
418
|
+
const routes = router.routes;
|
|
419
|
+
expect(routes.has('GET /test')).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
describe('route handler execution', () => {
|
|
423
|
+
it('should execute route handler and return result', async () => {
|
|
424
|
+
class TestController {
|
|
425
|
+
getTest() {
|
|
426
|
+
return { message: 'test' };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
container.register(TestController, new TestController());
|
|
430
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
431
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/', propertyKey: 'getTest' }], TestController);
|
|
432
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getTest');
|
|
433
|
+
router.registerController(TestController);
|
|
434
|
+
const context = {
|
|
435
|
+
params: {},
|
|
436
|
+
query: {},
|
|
437
|
+
body: {},
|
|
438
|
+
headers: {},
|
|
439
|
+
method: 'GET',
|
|
440
|
+
url: '/test',
|
|
441
|
+
};
|
|
442
|
+
const route = await router.match('GET', '/test', context);
|
|
443
|
+
expect(route).toBeDefined();
|
|
444
|
+
if (route) {
|
|
445
|
+
await route.handler(mockReq, mockRes, context);
|
|
446
|
+
expect(mockRes.json).toHaveBeenCalledWith({ message: 'test' });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
it('should handle route with body parameter', async () => {
|
|
450
|
+
class CreateDto {
|
|
451
|
+
}
|
|
452
|
+
class TestController {
|
|
453
|
+
create(body) {
|
|
454
|
+
return { created: true, data: body };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
container.register(TestController, new TestController());
|
|
458
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
459
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'POST', path: '/', propertyKey: 'create' }], TestController);
|
|
460
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'body', dtoType: CreateDto }], TestController, 'create');
|
|
461
|
+
router.registerController(TestController);
|
|
462
|
+
const context = {
|
|
463
|
+
params: {},
|
|
464
|
+
query: {},
|
|
465
|
+
body: { name: 'test' },
|
|
466
|
+
headers: {},
|
|
467
|
+
method: 'POST',
|
|
468
|
+
url: '/test',
|
|
469
|
+
};
|
|
470
|
+
const route = await router.match('POST', '/test', context);
|
|
471
|
+
expect(route).toBeDefined();
|
|
472
|
+
if (route) {
|
|
473
|
+
await route.handler(mockReq, mockRes, context);
|
|
474
|
+
expect(mockRes.json).toHaveBeenCalled();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
it('should handle route with param parameter', async () => {
|
|
478
|
+
class TestController {
|
|
479
|
+
getById(id) {
|
|
480
|
+
return { id };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
container.register(TestController, new TestController());
|
|
484
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
485
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/:id', propertyKey: 'getById' }], TestController);
|
|
486
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'param', name: 'id' }], TestController, 'getById');
|
|
487
|
+
router.registerController(TestController);
|
|
488
|
+
const context = {
|
|
489
|
+
params: { id: '123' },
|
|
490
|
+
query: {},
|
|
491
|
+
body: {},
|
|
492
|
+
headers: {},
|
|
493
|
+
method: 'GET',
|
|
494
|
+
url: '/test/123',
|
|
495
|
+
};
|
|
496
|
+
const route = await router.match('GET', '/test/123', context);
|
|
497
|
+
expect(route).toBeDefined();
|
|
498
|
+
if (route) {
|
|
499
|
+
await route.handler(mockReq, mockRes, context);
|
|
500
|
+
expect(mockRes.json).toHaveBeenCalledWith({ id: '123' });
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
it('should handle route with response parameter', async () => {
|
|
504
|
+
class TestController {
|
|
505
|
+
customResponse(res) {
|
|
506
|
+
res.status(201).json({ custom: true });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
container.register(TestController, new TestController());
|
|
510
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
511
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'POST', path: '/', propertyKey: 'customResponse' }], TestController);
|
|
512
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'response' }], TestController, 'customResponse');
|
|
513
|
+
router.registerController(TestController);
|
|
514
|
+
const context = {
|
|
515
|
+
params: {},
|
|
516
|
+
query: {},
|
|
517
|
+
body: {},
|
|
518
|
+
headers: {},
|
|
519
|
+
method: 'POST',
|
|
520
|
+
url: '/test',
|
|
521
|
+
};
|
|
522
|
+
const route = await router.match('POST', '/test', context);
|
|
523
|
+
expect(route).toBeDefined();
|
|
524
|
+
if (route) {
|
|
525
|
+
await route.handler(mockReq, mockRes, context);
|
|
526
|
+
expect(mockRes.status).toHaveBeenCalledWith(201);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
it('should handle HTML response', async () => {
|
|
530
|
+
class TestController {
|
|
531
|
+
getHtml() {
|
|
532
|
+
return '<!DOCTYPE html><html><body>Test</body></html>';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
container.register(TestController, new TestController());
|
|
536
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
537
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/html', propertyKey: 'getHtml' }], TestController);
|
|
538
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'getHtml');
|
|
539
|
+
router.registerController(TestController);
|
|
540
|
+
const context = {
|
|
541
|
+
params: {},
|
|
542
|
+
query: {},
|
|
543
|
+
body: {},
|
|
544
|
+
headers: {},
|
|
545
|
+
method: 'GET',
|
|
546
|
+
url: '/test/html',
|
|
547
|
+
};
|
|
548
|
+
const route = await router.match('GET', '/test/html', context);
|
|
549
|
+
expect(route).toBeDefined();
|
|
550
|
+
if (route) {
|
|
551
|
+
await route.handler(mockReq, mockRes, context);
|
|
552
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html');
|
|
553
|
+
expect(mockRes.send).toHaveBeenCalled();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
it('should handle route returning undefined', async () => {
|
|
557
|
+
class TestController {
|
|
558
|
+
noReturn() {
|
|
559
|
+
// Returns undefined
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
container.register(TestController, new TestController());
|
|
563
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
564
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/no-return', propertyKey: 'noReturn' }], TestController);
|
|
565
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'noReturn');
|
|
566
|
+
router.registerController(TestController);
|
|
567
|
+
const context = {
|
|
568
|
+
params: {},
|
|
569
|
+
query: {},
|
|
570
|
+
body: {},
|
|
571
|
+
headers: {},
|
|
572
|
+
method: 'GET',
|
|
573
|
+
url: '/test/no-return',
|
|
574
|
+
};
|
|
575
|
+
const route = await router.match('GET', '/test/no-return', context);
|
|
576
|
+
expect(route).toBeDefined();
|
|
577
|
+
if (route) {
|
|
578
|
+
await route.handler(mockReq, mockRes, context);
|
|
579
|
+
// Should not call json or send when result is undefined
|
|
580
|
+
expect(mockRes.json).not.toHaveBeenCalled();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
it('should handle ValidationError', async () => {
|
|
584
|
+
class TestController {
|
|
585
|
+
throwValidation() {
|
|
586
|
+
throw new pipe_1.ValidationError('Validation failed', [
|
|
587
|
+
{
|
|
588
|
+
property: 'name',
|
|
589
|
+
constraints: { required: 'Required' },
|
|
590
|
+
value: undefined
|
|
591
|
+
}
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
container.register(TestController, new TestController());
|
|
596
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
597
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'POST', path: '/validate', propertyKey: 'throwValidation' }], TestController);
|
|
598
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'throwValidation');
|
|
599
|
+
router.registerController(TestController);
|
|
600
|
+
const context = {
|
|
601
|
+
params: {},
|
|
602
|
+
query: {},
|
|
603
|
+
body: {},
|
|
604
|
+
headers: {},
|
|
605
|
+
method: 'POST',
|
|
606
|
+
url: '/test/validate',
|
|
607
|
+
};
|
|
608
|
+
const route = await router.match('POST', '/test/validate', context);
|
|
609
|
+
expect(route).toBeDefined();
|
|
610
|
+
if (route) {
|
|
611
|
+
await route.handler(mockReq, mockRes, context);
|
|
612
|
+
expect(mockRes.status).toHaveBeenCalledWith(400);
|
|
613
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
614
|
+
statusCode: 400,
|
|
615
|
+
message: 'Validation failed',
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
it('should handle HttpError', async () => {
|
|
620
|
+
class TestController {
|
|
621
|
+
throwHttp() {
|
|
622
|
+
throw new http_error_1.HttpError(404, 'Not found');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
container.register(TestController, new TestController());
|
|
626
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
627
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/error', propertyKey: 'throwHttp' }], TestController);
|
|
628
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'throwHttp');
|
|
629
|
+
router.registerController(TestController);
|
|
630
|
+
const context = {
|
|
631
|
+
params: {},
|
|
632
|
+
query: {},
|
|
633
|
+
body: {},
|
|
634
|
+
headers: {},
|
|
635
|
+
method: 'GET',
|
|
636
|
+
url: '/test/error',
|
|
637
|
+
};
|
|
638
|
+
const route = await router.match('GET', '/test/error', context);
|
|
639
|
+
expect(route).toBeDefined();
|
|
640
|
+
if (route) {
|
|
641
|
+
await route.handler(mockReq, mockRes, context);
|
|
642
|
+
expect(mockRes.status).toHaveBeenCalledWith(404);
|
|
643
|
+
expect(mockRes.json).toHaveBeenCalledWith({
|
|
644
|
+
statusCode: 404,
|
|
645
|
+
message: 'Not found',
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
it('should handle generic errors', async () => {
|
|
650
|
+
class TestController {
|
|
651
|
+
throwGeneric() {
|
|
652
|
+
throw new Error('Generic error');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
container.register(TestController, new TestController());
|
|
656
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
657
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/generic', propertyKey: 'throwGeneric' }], TestController);
|
|
658
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'throwGeneric');
|
|
659
|
+
router.registerController(TestController);
|
|
660
|
+
const context = {
|
|
661
|
+
params: {},
|
|
662
|
+
query: {},
|
|
663
|
+
body: {},
|
|
664
|
+
headers: {},
|
|
665
|
+
method: 'GET',
|
|
666
|
+
url: '/test/generic',
|
|
667
|
+
};
|
|
668
|
+
const route = await router.match('GET', '/test/generic', context);
|
|
669
|
+
expect(route).toBeDefined();
|
|
670
|
+
if (route) {
|
|
671
|
+
await route.handler(mockReq, mockRes, context);
|
|
672
|
+
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
673
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
674
|
+
statusCode: 500,
|
|
675
|
+
message: 'Generic error',
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
// -----------------------------------------------------------------------
|
|
680
|
+
// Parameter injection — extended types
|
|
681
|
+
// -----------------------------------------------------------------------
|
|
682
|
+
it('should inject named header via { type: "headers", name }', async () => {
|
|
683
|
+
class TestController {
|
|
684
|
+
getHeader(authHeader) {
|
|
685
|
+
return { auth: authHeader };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
container.register(TestController, new TestController());
|
|
689
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
690
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/header-named', propertyKey: 'getHeader' }], TestController);
|
|
691
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'headers', name: 'authorization' }], TestController, 'getHeader');
|
|
692
|
+
router.registerController(TestController);
|
|
693
|
+
const context = {
|
|
694
|
+
params: {},
|
|
695
|
+
query: {},
|
|
696
|
+
body: {},
|
|
697
|
+
headers: { authorization: 'Bearer token123' },
|
|
698
|
+
method: 'GET',
|
|
699
|
+
url: '/test/header-named',
|
|
700
|
+
};
|
|
701
|
+
const route = await router.match('GET', '/test/header-named', context);
|
|
702
|
+
expect(route).toBeDefined();
|
|
703
|
+
if (route) {
|
|
704
|
+
await route.handler(mockReq, mockRes, context);
|
|
705
|
+
expect(mockRes.json).toHaveBeenCalledWith({ auth: 'Bearer token123' });
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
it('should inject all headers when { type: "headers" } has no name', async () => {
|
|
709
|
+
class TestController {
|
|
710
|
+
getAllHeaders(headers) {
|
|
711
|
+
return headers;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
container.register(TestController, new TestController());
|
|
715
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
716
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/headers-all', propertyKey: 'getAllHeaders' }], TestController);
|
|
717
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'headers' }], TestController, 'getAllHeaders');
|
|
718
|
+
router.registerController(TestController);
|
|
719
|
+
const context = {
|
|
720
|
+
params: {},
|
|
721
|
+
query: {},
|
|
722
|
+
body: {},
|
|
723
|
+
headers: { 'x-custom': 'value' },
|
|
724
|
+
method: 'GET',
|
|
725
|
+
url: '/test/headers-all',
|
|
726
|
+
};
|
|
727
|
+
const route = await router.match('GET', '/test/headers-all', context);
|
|
728
|
+
expect(route).toBeDefined();
|
|
729
|
+
if (route) {
|
|
730
|
+
await route.handler(mockReq, mockRes, context);
|
|
731
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ 'x-custom': 'value' }));
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
it('should inject user object via { type: "user" } without field', async () => {
|
|
735
|
+
class TestController {
|
|
736
|
+
whoAmI(user) {
|
|
737
|
+
return user;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
container.register(TestController, new TestController());
|
|
741
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
742
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/me', propertyKey: 'whoAmI' }], TestController);
|
|
743
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'user' }], TestController, 'whoAmI');
|
|
744
|
+
router.registerController(TestController);
|
|
745
|
+
const context = {
|
|
746
|
+
params: {},
|
|
747
|
+
query: {},
|
|
748
|
+
body: {},
|
|
749
|
+
headers: {},
|
|
750
|
+
method: 'GET',
|
|
751
|
+
url: '/test/me',
|
|
752
|
+
};
|
|
753
|
+
const reqWithUser = { ...mockReq, user: { sub: 'u1', role: 'admin' } };
|
|
754
|
+
const route = await router.match('GET', '/test/me', context);
|
|
755
|
+
expect(route).toBeDefined();
|
|
756
|
+
if (route) {
|
|
757
|
+
await route.handler(reqWithUser, mockRes, context);
|
|
758
|
+
expect(mockRes.json).toHaveBeenCalledWith({ sub: 'u1', role: 'admin' });
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
it('should inject specific user field via { type: "user", field }', async () => {
|
|
762
|
+
class TestController {
|
|
763
|
+
getRole(role) {
|
|
764
|
+
return { role };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
container.register(TestController, new TestController());
|
|
768
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
769
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/role', propertyKey: 'getRole' }], TestController);
|
|
770
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'user', field: 'role' }], TestController, 'getRole');
|
|
771
|
+
router.registerController(TestController);
|
|
772
|
+
const context = {
|
|
773
|
+
params: {},
|
|
774
|
+
query: {},
|
|
775
|
+
body: {},
|
|
776
|
+
headers: {},
|
|
777
|
+
method: 'GET',
|
|
778
|
+
url: '/test/role',
|
|
779
|
+
};
|
|
780
|
+
const reqWithUser = { ...mockReq, user: { sub: 'u1', role: 'manager' } };
|
|
781
|
+
const route = await router.match('GET', '/test/role', context);
|
|
782
|
+
expect(route).toBeDefined();
|
|
783
|
+
if (route) {
|
|
784
|
+
await route.handler(reqWithUser, mockRes, context);
|
|
785
|
+
expect(mockRes.json).toHaveBeenCalledWith({ role: 'manager' });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
it('should invoke a custom resolver via { type: "custom", resolve }', async () => {
|
|
789
|
+
const resolvedValue = { custom: 'injected' };
|
|
790
|
+
const resolveFn = jest.fn().mockReturnValue(resolvedValue);
|
|
791
|
+
class TestController {
|
|
792
|
+
getCustom(val) {
|
|
793
|
+
return val;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
container.register(TestController, new TestController());
|
|
797
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
798
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/custom', propertyKey: 'getCustom' }], TestController);
|
|
799
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'custom', resolve: resolveFn }], TestController, 'getCustom');
|
|
800
|
+
router.registerController(TestController);
|
|
801
|
+
const context = {
|
|
802
|
+
params: {},
|
|
803
|
+
query: {},
|
|
804
|
+
body: {},
|
|
805
|
+
headers: {},
|
|
806
|
+
method: 'GET',
|
|
807
|
+
url: '/test/custom',
|
|
808
|
+
};
|
|
809
|
+
const route = await router.match('GET', '/test/custom', context);
|
|
810
|
+
expect(route).toBeDefined();
|
|
811
|
+
if (route) {
|
|
812
|
+
await route.handler(mockReq, mockRes, context);
|
|
813
|
+
expect(resolveFn).toHaveBeenCalled();
|
|
814
|
+
expect(mockRes.json).toHaveBeenCalledWith(resolvedValue);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
it('should auto-inject RequestContext for undecorated parameters', async () => {
|
|
818
|
+
class TestController {
|
|
819
|
+
contextReceiver(ctx) {
|
|
820
|
+
return { url: ctx.url };
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
container.register(TestController, new TestController());
|
|
824
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
825
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/ctx', propertyKey: 'contextReceiver' }], TestController);
|
|
826
|
+
// No injection metadata — relies on design:paramtypes auto-inject
|
|
827
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'contextReceiver');
|
|
828
|
+
Reflect.defineMetadata('design:paramtypes', [Object], // one undecorated parameter
|
|
829
|
+
TestController.prototype, 'contextReceiver');
|
|
830
|
+
router.registerController(TestController);
|
|
831
|
+
const context = {
|
|
832
|
+
params: {},
|
|
833
|
+
query: {},
|
|
834
|
+
body: {},
|
|
835
|
+
headers: {},
|
|
836
|
+
method: 'GET',
|
|
837
|
+
url: '/test/ctx',
|
|
838
|
+
};
|
|
839
|
+
const route = await router.match('GET', '/test/ctx', context);
|
|
840
|
+
expect(route).toBeDefined();
|
|
841
|
+
if (route) {
|
|
842
|
+
await route.handler(mockReq, mockRes, context);
|
|
843
|
+
expect(mockRes.json).toHaveBeenCalledWith({ url: '/test/ctx' });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
// -----------------------------------------------------------------------
|
|
847
|
+
// @Redirect metadata
|
|
848
|
+
// -----------------------------------------------------------------------
|
|
849
|
+
it('should send a redirect response when @Redirect metadata is set', async () => {
|
|
850
|
+
class TestController {
|
|
851
|
+
goHome() {
|
|
852
|
+
return undefined;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
container.register(TestController, new TestController());
|
|
856
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
857
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/redirect', propertyKey: 'goHome' }], TestController);
|
|
858
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'goHome');
|
|
859
|
+
// Simulate @Redirect('/home', 301) on the prototype
|
|
860
|
+
Reflect.defineMetadata('hazel:redirect', { url: '/home', statusCode: 301 }, TestController.prototype, 'goHome');
|
|
861
|
+
router.registerController(TestController);
|
|
862
|
+
const context = {
|
|
863
|
+
params: {},
|
|
864
|
+
query: {},
|
|
865
|
+
body: {},
|
|
866
|
+
headers: {},
|
|
867
|
+
method: 'GET',
|
|
868
|
+
url: '/test/redirect',
|
|
869
|
+
};
|
|
870
|
+
const route = await router.match('GET', '/test/redirect', context);
|
|
871
|
+
expect(route).toBeDefined();
|
|
872
|
+
if (route) {
|
|
873
|
+
mockRes.setHeader = jest.fn();
|
|
874
|
+
await route.handler(mockReq, mockRes, context);
|
|
875
|
+
expect(mockRes.status).toHaveBeenCalledWith(301);
|
|
876
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('Location', '/home');
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
// -----------------------------------------------------------------------
|
|
880
|
+
// @Header metadata (custom response headers)
|
|
881
|
+
// -----------------------------------------------------------------------
|
|
882
|
+
it('should set custom response headers when @Header metadata is set', async () => {
|
|
883
|
+
class TestController {
|
|
884
|
+
headered() {
|
|
885
|
+
return { ok: true };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
container.register(TestController, new TestController());
|
|
889
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
890
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/headered', propertyKey: 'headered' }], TestController);
|
|
891
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'headered');
|
|
892
|
+
Reflect.defineMetadata('hazel:headers', [{ name: 'X-Custom', value: 'hello' }], TestController.prototype, 'headered');
|
|
893
|
+
router.registerController(TestController);
|
|
894
|
+
const context = {
|
|
895
|
+
params: {},
|
|
896
|
+
query: {},
|
|
897
|
+
body: {},
|
|
898
|
+
headers: {},
|
|
899
|
+
method: 'GET',
|
|
900
|
+
url: '/test/headered',
|
|
901
|
+
};
|
|
902
|
+
const route = await router.match('GET', '/test/headered', context);
|
|
903
|
+
expect(route).toBeDefined();
|
|
904
|
+
if (route) {
|
|
905
|
+
await route.handler(mockReq, mockRes, context);
|
|
906
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Custom', 'hello');
|
|
907
|
+
expect(mockRes.json).toHaveBeenCalledWith({ ok: true });
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
// -----------------------------------------------------------------------
|
|
911
|
+
// @HttpCode metadata
|
|
912
|
+
// -----------------------------------------------------------------------
|
|
913
|
+
it('should use custom HTTP status code from @HttpCode when result is defined', async () => {
|
|
914
|
+
class TestController {
|
|
915
|
+
create() {
|
|
916
|
+
return { created: true };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
container.register(TestController, new TestController());
|
|
920
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
921
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'POST', path: '/http-code', propertyKey: 'create' }], TestController);
|
|
922
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'create');
|
|
923
|
+
Reflect.defineMetadata('hazel:http-code', 201, TestController.prototype, 'create');
|
|
924
|
+
router.registerController(TestController);
|
|
925
|
+
const context = {
|
|
926
|
+
params: {},
|
|
927
|
+
query: {},
|
|
928
|
+
body: {},
|
|
929
|
+
headers: {},
|
|
930
|
+
method: 'POST',
|
|
931
|
+
url: '/test/http-code',
|
|
932
|
+
};
|
|
933
|
+
const route = await router.match('POST', '/test/http-code', context);
|
|
934
|
+
expect(route).toBeDefined();
|
|
935
|
+
if (route) {
|
|
936
|
+
await route.handler(mockReq, mockRes, context);
|
|
937
|
+
expect(mockRes.status).toHaveBeenCalledWith(201);
|
|
938
|
+
expect(mockRes.json).toHaveBeenCalledWith({ created: true });
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
it('should use custom HTTP status code from @HttpCode when result is undefined', async () => {
|
|
942
|
+
class TestController {
|
|
943
|
+
noContent() {
|
|
944
|
+
return undefined;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
container.register(TestController, new TestController());
|
|
948
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
949
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'DELETE', path: '/no-content', propertyKey: 'noContent' }], TestController);
|
|
950
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'noContent');
|
|
951
|
+
Reflect.defineMetadata('hazel:http-code', 204, TestController.prototype, 'noContent');
|
|
952
|
+
router.registerController(TestController);
|
|
953
|
+
const context = {
|
|
954
|
+
params: {},
|
|
955
|
+
query: {},
|
|
956
|
+
body: {},
|
|
957
|
+
headers: {},
|
|
958
|
+
method: 'DELETE',
|
|
959
|
+
url: '/test/no-content',
|
|
960
|
+
};
|
|
961
|
+
const route = await router.match('DELETE', '/test/no-content', context);
|
|
962
|
+
expect(route).toBeDefined();
|
|
963
|
+
if (route) {
|
|
964
|
+
await route.handler(mockReq, mockRes, context);
|
|
965
|
+
expect(mockRes.status).toHaveBeenCalledWith(204);
|
|
966
|
+
expect(mockRes.end).toHaveBeenCalled();
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
// -----------------------------------------------------------------------
|
|
970
|
+
// Guard execution
|
|
971
|
+
// -----------------------------------------------------------------------
|
|
972
|
+
it('should throw UnauthorizedError when guard canActivate returns false', async () => {
|
|
973
|
+
class DenyGuard {
|
|
974
|
+
canActivate() {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
container.register(DenyGuard, new DenyGuard());
|
|
979
|
+
class TestController {
|
|
980
|
+
secret() {
|
|
981
|
+
return { secret: true };
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
container.register(TestController, new TestController());
|
|
985
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
986
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/secret', propertyKey: 'secret' }], TestController);
|
|
987
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'secret');
|
|
988
|
+
Reflect.defineMetadata('hazel:guards', [DenyGuard], TestController);
|
|
989
|
+
router.registerController(TestController);
|
|
990
|
+
const context = {
|
|
991
|
+
params: {},
|
|
992
|
+
query: {},
|
|
993
|
+
body: {},
|
|
994
|
+
headers: {},
|
|
995
|
+
method: 'GET',
|
|
996
|
+
url: '/test/secret',
|
|
997
|
+
};
|
|
998
|
+
const route = await router.match('GET', '/test/secret', context);
|
|
999
|
+
expect(route).toBeDefined();
|
|
1000
|
+
if (route) {
|
|
1001
|
+
await route.handler(mockReq, mockRes, context);
|
|
1002
|
+
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
it('should propagate req.user to context.user after guards pass', async () => {
|
|
1006
|
+
class AllowGuard {
|
|
1007
|
+
canActivate() {
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
container.register(AllowGuard, new AllowGuard());
|
|
1012
|
+
class TestController {
|
|
1013
|
+
whoAmI(user) {
|
|
1014
|
+
return user;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
container.register(TestController, new TestController());
|
|
1018
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
1019
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/user-propagate', propertyKey: 'whoAmI' }], TestController);
|
|
1020
|
+
Reflect.defineMetadata('hazel:inject', [{ type: 'user' }], TestController, 'whoAmI');
|
|
1021
|
+
Reflect.defineMetadata('hazel:guards', [AllowGuard], TestController);
|
|
1022
|
+
router.registerController(TestController);
|
|
1023
|
+
const context = {
|
|
1024
|
+
params: {},
|
|
1025
|
+
query: {},
|
|
1026
|
+
body: {},
|
|
1027
|
+
headers: {},
|
|
1028
|
+
method: 'GET',
|
|
1029
|
+
url: '/test/user-propagate',
|
|
1030
|
+
};
|
|
1031
|
+
const reqWithUser = { ...mockReq, user: { sub: 'u99' } };
|
|
1032
|
+
const route = await router.match('GET', '/test/user-propagate', context);
|
|
1033
|
+
expect(route).toBeDefined();
|
|
1034
|
+
if (route) {
|
|
1035
|
+
await route.handler(reqWithUser, mockRes, context);
|
|
1036
|
+
expect(mockRes.json).toHaveBeenCalledWith({ sub: 'u99' });
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
// -----------------------------------------------------------------------
|
|
1040
|
+
// Legacy string-based injection
|
|
1041
|
+
// -----------------------------------------------------------------------
|
|
1042
|
+
it('should handle legacy string injection "body"', async () => {
|
|
1043
|
+
class TestController {
|
|
1044
|
+
legacyBody(body) {
|
|
1045
|
+
return body;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
container.register(TestController, new TestController());
|
|
1049
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
1050
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'POST', path: '/legacy-body', propertyKey: 'legacyBody' }], TestController);
|
|
1051
|
+
Reflect.defineMetadata('hazel:inject', ['body'], TestController, 'legacyBody');
|
|
1052
|
+
router.registerController(TestController);
|
|
1053
|
+
const context = {
|
|
1054
|
+
params: {},
|
|
1055
|
+
query: {},
|
|
1056
|
+
body: { name: 'legacy' },
|
|
1057
|
+
headers: {},
|
|
1058
|
+
method: 'POST',
|
|
1059
|
+
url: '/test/legacy-body',
|
|
1060
|
+
};
|
|
1061
|
+
const route = await router.match('POST', '/test/legacy-body', context);
|
|
1062
|
+
if (route) {
|
|
1063
|
+
await route.handler(mockReq, mockRes, context);
|
|
1064
|
+
expect(mockRes.json).toHaveBeenCalledWith({ name: 'legacy' });
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
it('should handle legacy string injection "param"', async () => {
|
|
1068
|
+
class TestController {
|
|
1069
|
+
legacyParam(params) {
|
|
1070
|
+
return params;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
container.register(TestController, new TestController());
|
|
1074
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
1075
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/legacy-param/:id', propertyKey: 'legacyParam' }], TestController);
|
|
1076
|
+
Reflect.defineMetadata('hazel:inject', ['param'], TestController, 'legacyParam');
|
|
1077
|
+
router.registerController(TestController);
|
|
1078
|
+
const context = {
|
|
1079
|
+
params: { id: '42' },
|
|
1080
|
+
query: {},
|
|
1081
|
+
body: {},
|
|
1082
|
+
headers: {},
|
|
1083
|
+
method: 'GET',
|
|
1084
|
+
url: '/test/legacy-param/42',
|
|
1085
|
+
};
|
|
1086
|
+
const route = await router.match('GET', '/test/legacy-param/42', context);
|
|
1087
|
+
if (route) {
|
|
1088
|
+
await route.handler(mockReq, mockRes, context);
|
|
1089
|
+
expect(mockRes.json).toHaveBeenCalledWith({ id: '42' });
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
it('should handle legacy string injection "query"', async () => {
|
|
1093
|
+
class TestController {
|
|
1094
|
+
legacyQuery(query) {
|
|
1095
|
+
return query;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
container.register(TestController, new TestController());
|
|
1099
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
1100
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/legacy-query', propertyKey: 'legacyQuery' }], TestController);
|
|
1101
|
+
Reflect.defineMetadata('hazel:inject', ['query'], TestController, 'legacyQuery');
|
|
1102
|
+
router.registerController(TestController);
|
|
1103
|
+
const context = {
|
|
1104
|
+
params: {},
|
|
1105
|
+
query: { filter: 'active' },
|
|
1106
|
+
body: {},
|
|
1107
|
+
headers: {},
|
|
1108
|
+
method: 'GET',
|
|
1109
|
+
url: '/test/legacy-query',
|
|
1110
|
+
};
|
|
1111
|
+
const route = await router.match('GET', '/test/legacy-query', context);
|
|
1112
|
+
if (route) {
|
|
1113
|
+
await route.handler(mockReq, mockRes, context);
|
|
1114
|
+
expect(mockRes.json).toHaveBeenCalledWith({ filter: 'active' });
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
it('should handle legacy string injection "headers"', async () => {
|
|
1118
|
+
class TestController {
|
|
1119
|
+
legacyHeaders(headers) {
|
|
1120
|
+
return headers;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
container.register(TestController, new TestController());
|
|
1124
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
1125
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/legacy-headers', propertyKey: 'legacyHeaders' }], TestController);
|
|
1126
|
+
Reflect.defineMetadata('hazel:inject', ['headers'], TestController, 'legacyHeaders');
|
|
1127
|
+
router.registerController(TestController);
|
|
1128
|
+
const context = {
|
|
1129
|
+
params: {},
|
|
1130
|
+
query: {},
|
|
1131
|
+
body: {},
|
|
1132
|
+
headers: { 'x-trace': 'abc' },
|
|
1133
|
+
method: 'GET',
|
|
1134
|
+
url: '/test/legacy-headers',
|
|
1135
|
+
};
|
|
1136
|
+
const route = await router.match('GET', '/test/legacy-headers', context);
|
|
1137
|
+
if (route) {
|
|
1138
|
+
await route.handler(mockReq, mockRes, context);
|
|
1139
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ 'x-trace': 'abc' }));
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
// -----------------------------------------------------------------------
|
|
1143
|
+
// handleRequest edge cases
|
|
1144
|
+
// -----------------------------------------------------------------------
|
|
1145
|
+
it('should return "Internal Server Error" when a non-Error is thrown in handleRequest', async () => {
|
|
1146
|
+
jest.spyOn(router, 'match').mockRejectedValue('string-error');
|
|
1147
|
+
mockReq.method = 'GET';
|
|
1148
|
+
mockReq.url = '/test';
|
|
1149
|
+
await router.handleRequest(mockReq, mockRes);
|
|
1150
|
+
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
1151
|
+
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Internal Server Error' });
|
|
1152
|
+
});
|
|
1153
|
+
it('should use production error message when NODE_ENV is production', async () => {
|
|
1154
|
+
const originalEnv = process.env.NODE_ENV;
|
|
1155
|
+
process.env.NODE_ENV = 'production';
|
|
1156
|
+
class TestController {
|
|
1157
|
+
throwInProd() {
|
|
1158
|
+
throw new Error('Sensitive internal details');
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
container.register(TestController, new TestController());
|
|
1162
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
1163
|
+
Reflect.defineMetadata('hazel:routes', [{ method: 'GET', path: '/prod-error', propertyKey: 'throwInProd' }], TestController);
|
|
1164
|
+
Reflect.defineMetadata('hazel:inject', [], TestController, 'throwInProd');
|
|
1165
|
+
router.registerController(TestController);
|
|
1166
|
+
const context = {
|
|
1167
|
+
params: {},
|
|
1168
|
+
query: {},
|
|
1169
|
+
body: {},
|
|
1170
|
+
headers: {},
|
|
1171
|
+
method: 'GET',
|
|
1172
|
+
url: '/test/prod-error',
|
|
1173
|
+
};
|
|
1174
|
+
const route = await router.match('GET', '/test/prod-error', context);
|
|
1175
|
+
if (route) {
|
|
1176
|
+
await route.handler(mockReq, mockRes, context);
|
|
1177
|
+
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
1178
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ message: 'Internal server error' }));
|
|
1179
|
+
}
|
|
1180
|
+
process.env.NODE_ENV = originalEnv;
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
});
|