@hazeljs/gateway 0.2.0-beta.41
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 +255 -0
- package/dist/__tests__/canary-engine.test.d.ts +2 -0
- package/dist/__tests__/canary-engine.test.d.ts.map +1 -0
- package/dist/__tests__/canary-engine.test.js +133 -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 +174 -0
- package/dist/__tests__/from-config.test.d.ts +2 -0
- package/dist/__tests__/from-config.test.d.ts.map +1 -0
- package/dist/__tests__/from-config.test.js +67 -0
- package/dist/__tests__/gateway-metrics.test.d.ts +2 -0
- package/dist/__tests__/gateway-metrics.test.d.ts.map +1 -0
- package/dist/__tests__/gateway-metrics.test.js +82 -0
- package/dist/__tests__/gateway-module.test.d.ts +2 -0
- package/dist/__tests__/gateway-module.test.d.ts.map +1 -0
- package/dist/__tests__/gateway-module.test.js +91 -0
- package/dist/__tests__/gateway.test.d.ts +2 -0
- package/dist/__tests__/gateway.test.d.ts.map +1 -0
- package/dist/__tests__/gateway.test.js +257 -0
- package/dist/__tests__/hazel-integration.test.d.ts +2 -0
- package/dist/__tests__/hazel-integration.test.d.ts.map +1 -0
- package/dist/__tests__/hazel-integration.test.js +92 -0
- package/dist/__tests__/route-matcher.test.d.ts +2 -0
- package/dist/__tests__/route-matcher.test.d.ts.map +1 -0
- package/dist/__tests__/route-matcher.test.js +67 -0
- package/dist/__tests__/service-proxy.test.d.ts +2 -0
- package/dist/__tests__/service-proxy.test.d.ts.map +1 -0
- package/dist/__tests__/service-proxy.test.js +110 -0
- package/dist/__tests__/traffic-mirror.test.d.ts +2 -0
- package/dist/__tests__/traffic-mirror.test.d.ts.map +1 -0
- package/dist/__tests__/traffic-mirror.test.js +70 -0
- package/dist/__tests__/version-router.test.d.ts +2 -0
- package/dist/__tests__/version-router.test.d.ts.map +1 -0
- package/dist/__tests__/version-router.test.js +136 -0
- package/dist/canary/canary-engine.d.ts +107 -0
- package/dist/canary/canary-engine.d.ts.map +1 -0
- package/dist/canary/canary-engine.js +334 -0
- package/dist/decorators/index.d.ts +74 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +170 -0
- package/dist/gateway.d.ts +67 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +310 -0
- package/dist/gateway.module.d.ts +67 -0
- package/dist/gateway.module.d.ts.map +1 -0
- package/dist/gateway.module.js +82 -0
- package/dist/hazel-integration.d.ts +24 -0
- package/dist/hazel-integration.d.ts.map +1 -0
- package/dist/hazel-integration.js +70 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/metrics/gateway-metrics.d.ts +64 -0
- package/dist/metrics/gateway-metrics.d.ts.map +1 -0
- package/dist/metrics/gateway-metrics.js +159 -0
- package/dist/middleware/traffic-mirror.d.ts +19 -0
- package/dist/middleware/traffic-mirror.d.ts.map +1 -0
- package/dist/middleware/traffic-mirror.js +60 -0
- package/dist/proxy/service-proxy.d.ts +68 -0
- package/dist/proxy/service-proxy.d.ts.map +1 -0
- package/dist/proxy/service-proxy.js +211 -0
- package/dist/routing/route-matcher.d.ts +31 -0
- package/dist/routing/route-matcher.d.ts.map +1 -0
- package/dist/routing/route-matcher.js +112 -0
- package/dist/routing/version-router.d.ts +36 -0
- package/dist/routing/version-router.d.ts.map +1 -0
- package/dist/routing/version-router.js +136 -0
- package/dist/types/index.d.ts +217 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +17 -0
- package/package.json +74 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const gateway_1 = require("../gateway");
|
|
16
|
+
const decorators_1 = require("../decorators");
|
|
17
|
+
const discovery_1 = require("@hazeljs/discovery");
|
|
18
|
+
const axios_1 = __importDefault(require("axios"));
|
|
19
|
+
jest.mock('axios');
|
|
20
|
+
const mockedAxios = axios_1.default;
|
|
21
|
+
describe('GatewayServer', () => {
|
|
22
|
+
let backend;
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
backend = new discovery_1.MemoryRegistryBackend();
|
|
25
|
+
await backend.register({
|
|
26
|
+
id: 'user-1',
|
|
27
|
+
name: 'user-service',
|
|
28
|
+
host: 'localhost',
|
|
29
|
+
port: 3001,
|
|
30
|
+
protocol: 'http',
|
|
31
|
+
status: discovery_1.ServiceStatus.UP,
|
|
32
|
+
lastHeartbeat: new Date(),
|
|
33
|
+
registeredAt: new Date(),
|
|
34
|
+
});
|
|
35
|
+
mockedAxios.create.mockReturnValue({
|
|
36
|
+
request: jest.fn().mockResolvedValue({
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: {},
|
|
39
|
+
data: { id: 1, name: 'Alice' },
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
describe('handleRequest', () => {
|
|
47
|
+
it('should return 404 when no route matches', async () => {
|
|
48
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
49
|
+
gateway.addRoute({
|
|
50
|
+
path: '/api/users/**',
|
|
51
|
+
serviceName: 'user-service',
|
|
52
|
+
});
|
|
53
|
+
const response = await gateway.handleRequest({
|
|
54
|
+
method: 'GET',
|
|
55
|
+
path: '/unknown/path',
|
|
56
|
+
headers: {},
|
|
57
|
+
});
|
|
58
|
+
expect(response.status).toBe(404);
|
|
59
|
+
expect(response.body).toMatchObject({ error: 'No matching gateway route' });
|
|
60
|
+
});
|
|
61
|
+
it('should return 405 when method not allowed', async () => {
|
|
62
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
63
|
+
gateway.addRoute({
|
|
64
|
+
path: '/api/users/**',
|
|
65
|
+
serviceName: 'user-service',
|
|
66
|
+
methods: ['POST'],
|
|
67
|
+
});
|
|
68
|
+
const response = await gateway.handleRequest({
|
|
69
|
+
method: 'GET',
|
|
70
|
+
path: '/api/users/1',
|
|
71
|
+
headers: {},
|
|
72
|
+
});
|
|
73
|
+
expect(response.status).toBe(405);
|
|
74
|
+
expect(response.body).toMatchObject({ error: 'Method not allowed' });
|
|
75
|
+
});
|
|
76
|
+
it('should proxy request to service and return response', async () => {
|
|
77
|
+
const mockRequest = jest.fn().mockResolvedValue({
|
|
78
|
+
status: 200,
|
|
79
|
+
headers: { 'content-type': 'application/json' },
|
|
80
|
+
data: { id: 1, name: 'Alice' },
|
|
81
|
+
});
|
|
82
|
+
mockedAxios.create.mockReturnValue({ request: mockRequest });
|
|
83
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
84
|
+
gateway.addRoute({
|
|
85
|
+
path: '/api/users/**',
|
|
86
|
+
serviceName: 'user-service',
|
|
87
|
+
});
|
|
88
|
+
const response = await gateway.handleRequest({
|
|
89
|
+
method: 'GET',
|
|
90
|
+
path: '/api/users/1',
|
|
91
|
+
headers: {},
|
|
92
|
+
});
|
|
93
|
+
expect(response.status).toBe(200);
|
|
94
|
+
expect(response.body).toEqual({ id: 1, name: 'Alice' });
|
|
95
|
+
expect(mockRequest).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
it('should return 429 with Retry-After when rate limit exceeded', async () => {
|
|
98
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
99
|
+
gateway.addRoute({
|
|
100
|
+
path: '/api/users/**',
|
|
101
|
+
serviceName: 'user-service',
|
|
102
|
+
rateLimit: { strategy: 'sliding-window', max: 1, window: 60000 },
|
|
103
|
+
});
|
|
104
|
+
await gateway.handleRequest({
|
|
105
|
+
method: 'GET',
|
|
106
|
+
path: '/api/users/1',
|
|
107
|
+
headers: {},
|
|
108
|
+
});
|
|
109
|
+
const response = await gateway.handleRequest({
|
|
110
|
+
method: 'GET',
|
|
111
|
+
path: '/api/users/1',
|
|
112
|
+
headers: {},
|
|
113
|
+
});
|
|
114
|
+
expect(response.status).toBe(429);
|
|
115
|
+
expect(response.headers['Retry-After']).toBeDefined();
|
|
116
|
+
expect(response.body).toMatchObject({ error: 'Too Many Requests' });
|
|
117
|
+
});
|
|
118
|
+
it('should return 502 when service proxy throws', async () => {
|
|
119
|
+
await backend.deregister('user-1');
|
|
120
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
121
|
+
gateway.addRoute({
|
|
122
|
+
path: '/api/users/**',
|
|
123
|
+
serviceName: 'user-service',
|
|
124
|
+
});
|
|
125
|
+
const response = await gateway.handleRequest({
|
|
126
|
+
method: 'GET',
|
|
127
|
+
path: '/api/users/1',
|
|
128
|
+
headers: {},
|
|
129
|
+
});
|
|
130
|
+
expect(response.status).toBe(502);
|
|
131
|
+
expect(response.body).toMatchObject({ error: 'Bad Gateway' });
|
|
132
|
+
});
|
|
133
|
+
it('should emit route:error on proxy failure', async () => {
|
|
134
|
+
await backend.deregister('user-1');
|
|
135
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
136
|
+
gateway.addRoute({
|
|
137
|
+
path: '/api/users/**',
|
|
138
|
+
serviceName: 'user-service',
|
|
139
|
+
});
|
|
140
|
+
const errorHandler = jest.fn();
|
|
141
|
+
gateway.on('route:error', errorHandler);
|
|
142
|
+
await gateway.handleRequest({
|
|
143
|
+
method: 'GET',
|
|
144
|
+
path: '/api/users/1',
|
|
145
|
+
headers: {},
|
|
146
|
+
});
|
|
147
|
+
expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
148
|
+
route: '/api/users/**',
|
|
149
|
+
service: 'user-service',
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('fromConfig', () => {
|
|
154
|
+
it('should create gateway with routes from config', () => {
|
|
155
|
+
const gateway = gateway_1.GatewayServer.fromConfig({
|
|
156
|
+
discovery: { cacheEnabled: false },
|
|
157
|
+
routes: [
|
|
158
|
+
{ path: '/a', serviceName: 'svc-a' },
|
|
159
|
+
{ path: '/b', serviceName: 'svc-b' },
|
|
160
|
+
],
|
|
161
|
+
}, backend);
|
|
162
|
+
expect(gateway.getRoutes()).toEqual(['/a', '/b']);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('fromClass', () => {
|
|
166
|
+
it('should create gateway from decorated class', () => {
|
|
167
|
+
let TestGateway = class TestGateway {
|
|
168
|
+
};
|
|
169
|
+
__decorate([
|
|
170
|
+
(0, decorators_1.Route)('/api/test/**'),
|
|
171
|
+
(0, decorators_1.ServiceRoute)('test-service'),
|
|
172
|
+
__metadata("design:type", Object)
|
|
173
|
+
], TestGateway.prototype, "testRoute", void 0);
|
|
174
|
+
TestGateway = __decorate([
|
|
175
|
+
(0, decorators_1.Gateway)({})
|
|
176
|
+
], TestGateway);
|
|
177
|
+
const gateway = gateway_1.GatewayServer.fromClass(TestGateway, backend);
|
|
178
|
+
expect(gateway.getRoutes()).toContain('/api/test/**');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('lifecycle', () => {
|
|
182
|
+
it('should start and stop canaries', () => {
|
|
183
|
+
const gateway = gateway_1.GatewayServer.fromConfig({
|
|
184
|
+
discovery: { cacheEnabled: false },
|
|
185
|
+
routes: [
|
|
186
|
+
{
|
|
187
|
+
path: '/api/orders/**',
|
|
188
|
+
serviceName: 'order-service',
|
|
189
|
+
canary: {
|
|
190
|
+
stable: { version: 'v1', weight: 90 },
|
|
191
|
+
canary: { version: 'v2', weight: 10 },
|
|
192
|
+
promotion: {
|
|
193
|
+
strategy: 'error-rate',
|
|
194
|
+
errorThreshold: 5,
|
|
195
|
+
evaluationWindow: '5m',
|
|
196
|
+
autoPromote: true,
|
|
197
|
+
autoRollback: true,
|
|
198
|
+
steps: [10, 25, 50, 75, 100],
|
|
199
|
+
stepInterval: '10m',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
}, backend);
|
|
205
|
+
expect(() => gateway.startCanaries()).not.toThrow();
|
|
206
|
+
expect(() => gateway.stop()).not.toThrow();
|
|
207
|
+
});
|
|
208
|
+
it('stop should close discovery client', async () => {
|
|
209
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
210
|
+
gateway.addRoute({ path: '/x', serviceName: 'user-service' });
|
|
211
|
+
gateway.stop();
|
|
212
|
+
expect(backend).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('getters', () => {
|
|
216
|
+
it('getMetrics should return GatewayMetrics', () => {
|
|
217
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
218
|
+
gateway.addRoute({ path: '/x', serviceName: 'user-service' });
|
|
219
|
+
const metrics = gateway.getMetrics();
|
|
220
|
+
expect(metrics).toBeDefined();
|
|
221
|
+
expect(metrics.getSnapshot).toBeDefined();
|
|
222
|
+
});
|
|
223
|
+
it('getCanaryEngine should return engine for canary route', () => {
|
|
224
|
+
const gateway = gateway_1.GatewayServer.fromConfig({
|
|
225
|
+
discovery: { cacheEnabled: false },
|
|
226
|
+
routes: [
|
|
227
|
+
{
|
|
228
|
+
path: '/api/orders/**',
|
|
229
|
+
serviceName: 'order-service',
|
|
230
|
+
canary: {
|
|
231
|
+
stable: { version: 'v1', weight: 90 },
|
|
232
|
+
canary: { version: 'v2', weight: 10 },
|
|
233
|
+
promotion: {
|
|
234
|
+
strategy: 'error-rate',
|
|
235
|
+
errorThreshold: 5,
|
|
236
|
+
evaluationWindow: '5m',
|
|
237
|
+
autoPromote: true,
|
|
238
|
+
autoRollback: true,
|
|
239
|
+
steps: [10, 25, 50, 75, 100],
|
|
240
|
+
stepInterval: '10m',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
}, backend);
|
|
246
|
+
const engine = gateway.getCanaryEngine('/api/orders/**');
|
|
247
|
+
expect(engine).toBeDefined();
|
|
248
|
+
expect(gateway.getCanaryEngine('/nonexistent')).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
it('getDiscoveryClient should return the client', () => {
|
|
251
|
+
const gateway = new gateway_1.GatewayServer({ discovery: { cacheEnabled: false } }, backend);
|
|
252
|
+
const client = gateway.getDiscoveryClient();
|
|
253
|
+
expect(client).toBeDefined();
|
|
254
|
+
expect(client.getInstances).toBeDefined();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hazel-integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hazel-integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const hazel_integration_1 = require("../hazel-integration");
|
|
7
|
+
const gateway_1 = require("../gateway");
|
|
8
|
+
const discovery_1 = require("@hazeljs/discovery");
|
|
9
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
jest.mock('axios');
|
|
11
|
+
const mockedAxios = axios_1.default;
|
|
12
|
+
describe('createGatewayHandler', () => {
|
|
13
|
+
let backend;
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
backend = new discovery_1.MemoryRegistryBackend();
|
|
16
|
+
await backend.register({
|
|
17
|
+
id: 'svc-1',
|
|
18
|
+
name: 'user-service',
|
|
19
|
+
host: 'localhost',
|
|
20
|
+
port: 3001,
|
|
21
|
+
protocol: 'http',
|
|
22
|
+
status: discovery_1.ServiceStatus.UP,
|
|
23
|
+
lastHeartbeat: new Date(),
|
|
24
|
+
registeredAt: new Date(),
|
|
25
|
+
});
|
|
26
|
+
mockedAxios.create.mockReturnValue({
|
|
27
|
+
request: jest.fn().mockResolvedValue({
|
|
28
|
+
status: 200,
|
|
29
|
+
headers: { 'content-type': 'application/json' },
|
|
30
|
+
data: { id: 1, name: 'Alice' },
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('should create handler that forwards to gateway and returns true', async () => {
|
|
35
|
+
const gateway = gateway_1.GatewayServer.fromConfig({
|
|
36
|
+
discovery: { cacheEnabled: false },
|
|
37
|
+
routes: [{ path: '/api/users/**', serviceName: 'user-service' }],
|
|
38
|
+
}, backend);
|
|
39
|
+
const handler = (0, hazel_integration_1.createGatewayHandler)(gateway);
|
|
40
|
+
const req = {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
url: '/api/users/1',
|
|
43
|
+
headers: { 'content-type': 'application/json' },
|
|
44
|
+
};
|
|
45
|
+
const res = {
|
|
46
|
+
writeHead: jest.fn(),
|
|
47
|
+
end: jest.fn(),
|
|
48
|
+
writableEnded: false,
|
|
49
|
+
};
|
|
50
|
+
const context = {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
url: '/api/users/1',
|
|
53
|
+
headers: {},
|
|
54
|
+
params: {},
|
|
55
|
+
query: {},
|
|
56
|
+
body: undefined,
|
|
57
|
+
};
|
|
58
|
+
const handled = await handler(req, res, context);
|
|
59
|
+
expect(handled).toBe(true);
|
|
60
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
61
|
+
expect(res.end).toHaveBeenCalledWith(JSON.stringify({ id: 1, name: 'Alice' }));
|
|
62
|
+
});
|
|
63
|
+
it('should return 404 response when no route matches', async () => {
|
|
64
|
+
const gateway = gateway_1.GatewayServer.fromConfig({
|
|
65
|
+
discovery: { cacheEnabled: false },
|
|
66
|
+
routes: [{ path: '/api/users/**', serviceName: 'user-service' }],
|
|
67
|
+
}, backend);
|
|
68
|
+
const handler = (0, hazel_integration_1.createGatewayHandler)(gateway);
|
|
69
|
+
const req = {
|
|
70
|
+
method: 'GET',
|
|
71
|
+
url: '/unknown/path',
|
|
72
|
+
headers: {},
|
|
73
|
+
};
|
|
74
|
+
const res = {
|
|
75
|
+
writeHead: jest.fn(),
|
|
76
|
+
end: jest.fn(),
|
|
77
|
+
writableEnded: false,
|
|
78
|
+
};
|
|
79
|
+
const context = {
|
|
80
|
+
method: 'GET',
|
|
81
|
+
url: '/unknown/path',
|
|
82
|
+
headers: {},
|
|
83
|
+
params: {},
|
|
84
|
+
query: {},
|
|
85
|
+
body: undefined,
|
|
86
|
+
};
|
|
87
|
+
const handled = await handler(req, res, context);
|
|
88
|
+
expect(handled).toBe(true);
|
|
89
|
+
expect(res.writeHead).toHaveBeenCalledWith(404, expect.any(Object));
|
|
90
|
+
expect(res.end).toHaveBeenCalledWith(JSON.stringify({ error: 'No matching gateway route', path: '/unknown/path' }));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-matcher.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/route-matcher.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const route_matcher_1 = require("../routing/route-matcher");
|
|
4
|
+
describe('matchRoute', () => {
|
|
5
|
+
describe('exact matches', () => {
|
|
6
|
+
it('should match exact paths', () => {
|
|
7
|
+
const result = (0, route_matcher_1.matchRoute)('/api/users', '/api/users');
|
|
8
|
+
expect(result.matched).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
it('should not match different paths', () => {
|
|
11
|
+
const result = (0, route_matcher_1.matchRoute)('/api/users', '/api/orders');
|
|
12
|
+
expect(result.matched).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
it('should normalize paths', () => {
|
|
15
|
+
const result = (0, route_matcher_1.matchRoute)('api/users', '/api/users');
|
|
16
|
+
expect(result.matched).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('parameter matches', () => {
|
|
20
|
+
it('should match path parameters', () => {
|
|
21
|
+
const result = (0, route_matcher_1.matchRoute)('/api/users/:id', '/api/users/123');
|
|
22
|
+
expect(result.matched).toBe(true);
|
|
23
|
+
expect(result.params).toEqual({ id: '123' });
|
|
24
|
+
});
|
|
25
|
+
it('should match multiple parameters', () => {
|
|
26
|
+
const result = (0, route_matcher_1.matchRoute)('/api/:service/:id', '/api/users/456');
|
|
27
|
+
expect(result.matched).toBe(true);
|
|
28
|
+
expect(result.params).toEqual({ service: 'users', id: '456' });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('wildcard matches', () => {
|
|
32
|
+
it('should match single-segment wildcard', () => {
|
|
33
|
+
const result = (0, route_matcher_1.matchRoute)('/api/*/users', '/api/v2/users');
|
|
34
|
+
expect(result.matched).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('should not match multi-segment with single wildcard', () => {
|
|
37
|
+
const result = (0, route_matcher_1.matchRoute)('/api/*/users', '/api/v2/v3/users');
|
|
38
|
+
expect(result.matched).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('catch-all (**)', () => {
|
|
42
|
+
it('should match everything under prefix', () => {
|
|
43
|
+
expect((0, route_matcher_1.matchRoute)('/api/users/**', '/api/users').matched).toBe(true);
|
|
44
|
+
expect((0, route_matcher_1.matchRoute)('/api/users/**', '/api/users/123').matched).toBe(true);
|
|
45
|
+
expect((0, route_matcher_1.matchRoute)('/api/users/**', '/api/users/123/orders').matched).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('should not match different prefix', () => {
|
|
48
|
+
expect((0, route_matcher_1.matchRoute)('/api/users/**', '/api/orders').matched).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it('should capture remaining path', () => {
|
|
51
|
+
const result = (0, route_matcher_1.matchRoute)('/api/users/**', '/api/users/123/orders');
|
|
52
|
+
expect(result.remainingPath).toBe('/123/orders');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('sortRoutesBySpecificity', () => {
|
|
57
|
+
it('should sort most specific first', () => {
|
|
58
|
+
const routes = ['/api/**', '/api/users/:id', '/api/users/me', '/api/users/*'];
|
|
59
|
+
const sorted = (0, route_matcher_1.sortRoutesBySpecificity)(routes);
|
|
60
|
+
// /api/users/me (most specific - all literal segments)
|
|
61
|
+
expect(sorted[0]).toBe('/api/users/me');
|
|
62
|
+
// /api/users/:id (param is less specific than literal)
|
|
63
|
+
// /api/users/* (wildcard is less specific than param)
|
|
64
|
+
// /api/** (catch-all is least specific)
|
|
65
|
+
expect(sorted[sorted.length - 1]).toBe('/api/**');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-proxy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service-proxy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const service_proxy_1 = require("../proxy/service-proxy");
|
|
7
|
+
const discovery_1 = require("@hazeljs/discovery");
|
|
8
|
+
const resilience_1 = require("@hazeljs/resilience");
|
|
9
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
jest.mock('axios');
|
|
11
|
+
const mockedAxios = axios_1.default;
|
|
12
|
+
describe('ServiceProxy', () => {
|
|
13
|
+
let backend;
|
|
14
|
+
let discoveryClient;
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
backend = new discovery_1.MemoryRegistryBackend();
|
|
17
|
+
await backend.register({
|
|
18
|
+
id: 'svc-1',
|
|
19
|
+
name: 'test-service',
|
|
20
|
+
host: 'localhost',
|
|
21
|
+
port: 4000,
|
|
22
|
+
protocol: 'http',
|
|
23
|
+
status: discovery_1.ServiceStatus.UP,
|
|
24
|
+
lastHeartbeat: new Date(),
|
|
25
|
+
registeredAt: new Date(),
|
|
26
|
+
});
|
|
27
|
+
discoveryClient = new discovery_1.DiscoveryClient({ cacheEnabled: false }, backend);
|
|
28
|
+
const mockRequest = jest.fn().mockResolvedValue({
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: {},
|
|
31
|
+
data: { ok: true },
|
|
32
|
+
});
|
|
33
|
+
mockedAxios.create.mockReturnValue({ request: mockRequest });
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
discoveryClient.close();
|
|
37
|
+
});
|
|
38
|
+
it('should forward request to discovered instance', async () => {
|
|
39
|
+
const proxy = new service_proxy_1.ServiceProxy(discoveryClient, { serviceName: 'test-service' });
|
|
40
|
+
const request = {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
path: '/items',
|
|
43
|
+
headers: {},
|
|
44
|
+
};
|
|
45
|
+
const response = await proxy.forward(request);
|
|
46
|
+
expect(response.status).toBe(200);
|
|
47
|
+
expect(response.body).toEqual({ ok: true });
|
|
48
|
+
});
|
|
49
|
+
it('should throw when no instances available', async () => {
|
|
50
|
+
await backend.deregister('svc-1');
|
|
51
|
+
const proxy = new service_proxy_1.ServiceProxy(discoveryClient, { serviceName: 'test-service' });
|
|
52
|
+
await expect(proxy.forward({ method: 'GET', path: '/x', headers: {} })).rejects.toThrow('No instances available');
|
|
53
|
+
});
|
|
54
|
+
it('should apply stripPrefix and addPrefix', async () => {
|
|
55
|
+
const mockRequest = jest.fn().mockResolvedValue({
|
|
56
|
+
status: 200,
|
|
57
|
+
headers: {},
|
|
58
|
+
data: {},
|
|
59
|
+
});
|
|
60
|
+
mockedAxios.create.mockReturnValue({ request: mockRequest });
|
|
61
|
+
const proxy = new service_proxy_1.ServiceProxy(discoveryClient, {
|
|
62
|
+
serviceName: 'test-service',
|
|
63
|
+
stripPrefix: '/api',
|
|
64
|
+
addPrefix: '/v1',
|
|
65
|
+
});
|
|
66
|
+
await proxy.forward({
|
|
67
|
+
method: 'GET',
|
|
68
|
+
path: '/api/items',
|
|
69
|
+
headers: {},
|
|
70
|
+
});
|
|
71
|
+
expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
72
|
+
url: '/v1/items',
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
it('getMetrics should return MetricsCollector', () => {
|
|
76
|
+
const proxy = new service_proxy_1.ServiceProxy(discoveryClient, { serviceName: 'test-service' });
|
|
77
|
+
const m = proxy.getMetrics();
|
|
78
|
+
expect(m).toBeDefined();
|
|
79
|
+
expect(m.getSnapshot).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
it('getServiceName should return service name', () => {
|
|
82
|
+
const proxy = new service_proxy_1.ServiceProxy(discoveryClient, { serviceName: 'my-service' });
|
|
83
|
+
expect(proxy.getServiceName()).toBe('my-service');
|
|
84
|
+
});
|
|
85
|
+
it('should throw RateLimitError when rate limit exceeded', async () => {
|
|
86
|
+
const proxy = new service_proxy_1.ServiceProxy(discoveryClient, {
|
|
87
|
+
serviceName: 'test-service',
|
|
88
|
+
rateLimit: { strategy: 'sliding-window', max: 1, window: 60000 },
|
|
89
|
+
});
|
|
90
|
+
await proxy.forward({ method: 'GET', path: '/x', headers: {} });
|
|
91
|
+
await expect(proxy.forward({ method: 'GET', path: '/x', headers: {} })).rejects.toThrow(resilience_1.RateLimitError);
|
|
92
|
+
await expect(proxy.forward({ method: 'GET', path: '/x', headers: {} })).rejects.toThrow('Rate limit exceeded');
|
|
93
|
+
});
|
|
94
|
+
it('forwardToVersion should merge filter with version', async () => {
|
|
95
|
+
await backend.register({
|
|
96
|
+
id: 'svc-2',
|
|
97
|
+
name: 'test-service',
|
|
98
|
+
host: 'localhost',
|
|
99
|
+
port: 4001,
|
|
100
|
+
protocol: 'http',
|
|
101
|
+
status: discovery_1.ServiceStatus.UP,
|
|
102
|
+
lastHeartbeat: new Date(),
|
|
103
|
+
registeredAt: new Date(),
|
|
104
|
+
metadata: { version: 'v2' },
|
|
105
|
+
});
|
|
106
|
+
const proxy = new service_proxy_1.ServiceProxy(discoveryClient, { serviceName: 'test-service' });
|
|
107
|
+
const response = await proxy.forwardToVersion({ method: 'GET', path: '/x', headers: {} }, 'v2');
|
|
108
|
+
expect(response.status).toBe(200);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traffic-mirror.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/traffic-mirror.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const traffic_mirror_1 = require("../middleware/traffic-mirror");
|
|
7
|
+
const discovery_1 = require("@hazeljs/discovery");
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
jest.mock('axios');
|
|
10
|
+
const mockedAxios = axios_1.default;
|
|
11
|
+
describe('TrafficMirror', () => {
|
|
12
|
+
let backend;
|
|
13
|
+
let discoveryClient;
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
backend = new discovery_1.MemoryRegistryBackend();
|
|
16
|
+
await backend.register({
|
|
17
|
+
id: 'mirror-1',
|
|
18
|
+
name: 'shadow-service',
|
|
19
|
+
host: 'localhost',
|
|
20
|
+
port: 5000,
|
|
21
|
+
protocol: 'http',
|
|
22
|
+
status: discovery_1.ServiceStatus.UP,
|
|
23
|
+
lastHeartbeat: new Date(),
|
|
24
|
+
registeredAt: new Date(),
|
|
25
|
+
});
|
|
26
|
+
discoveryClient = new discovery_1.DiscoveryClient({ cacheEnabled: false }, backend);
|
|
27
|
+
mockedAxios.create.mockReturnValue({
|
|
28
|
+
request: jest.fn().mockResolvedValue({}),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
discoveryClient.close();
|
|
33
|
+
});
|
|
34
|
+
it('should mirror request when percentage allows', async () => {
|
|
35
|
+
const mockRequest = jest.fn().mockResolvedValue({});
|
|
36
|
+
mockedAxios.create.mockReturnValue({ request: mockRequest });
|
|
37
|
+
const mirror = new traffic_mirror_1.TrafficMirror({ service: 'shadow-service', percentage: 100, waitForResponse: true }, discoveryClient);
|
|
38
|
+
await mirror.mirror({
|
|
39
|
+
method: 'GET',
|
|
40
|
+
path: '/api/test',
|
|
41
|
+
headers: {},
|
|
42
|
+
});
|
|
43
|
+
expect(mockRequest).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
it('should not fail when mirror service has no instances', async () => {
|
|
46
|
+
const mirror = new traffic_mirror_1.TrafficMirror({ service: 'non-existent-service', percentage: 100 }, discoveryClient);
|
|
47
|
+
await expect(mirror.mirror({
|
|
48
|
+
method: 'GET',
|
|
49
|
+
path: '/api/test',
|
|
50
|
+
headers: {},
|
|
51
|
+
})).resolves.not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
it('should include X-Mirror headers when mirroring', async () => {
|
|
54
|
+
const mockRequest = jest.fn().mockResolvedValue({});
|
|
55
|
+
mockedAxios.create.mockReturnValue({ request: mockRequest });
|
|
56
|
+
const mirror = new traffic_mirror_1.TrafficMirror({ service: 'shadow-service', percentage: 100, waitForResponse: true }, discoveryClient);
|
|
57
|
+
await mirror.mirror({
|
|
58
|
+
method: 'POST',
|
|
59
|
+
path: '/api/test',
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
body: { foo: 'bar' },
|
|
62
|
+
});
|
|
63
|
+
expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
64
|
+
headers: expect.objectContaining({
|
|
65
|
+
'X-Mirror': 'true',
|
|
66
|
+
'X-Mirror-Source': 'hazeljs-gateway',
|
|
67
|
+
}),
|
|
68
|
+
}));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-router.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/version-router.test.ts"],"names":[],"mappings":""}
|