@dangao/bun-server 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/config/config-module.d.ts +17 -0
- package/dist/config/config-module.d.ts.map +1 -1
- package/dist/config/service.d.ts +18 -1
- package/dist/config/service.d.ts.map +1 -1
- package/dist/config/types.d.ts +25 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controller/controller.d.ts +5 -0
- package/dist/controller/controller.d.ts.map +1 -1
- package/dist/controller/decorators.d.ts +30 -1
- package/dist/controller/decorators.d.ts.map +1 -1
- package/dist/controller/index.d.ts +2 -2
- package/dist/controller/index.d.ts.map +1 -1
- package/dist/controller/param-binder.d.ts +12 -0
- package/dist/controller/param-binder.d.ts.map +1 -1
- package/dist/core/application.d.ts +15 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/context.d.ts +1 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/server.d.ts +8 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6487 -4178
- package/dist/microservice/config-center/config-center-module.d.ts +43 -0
- package/dist/microservice/config-center/config-center-module.d.ts.map +1 -0
- package/dist/microservice/config-center/decorators.d.ts +58 -0
- package/dist/microservice/config-center/decorators.d.ts.map +1 -0
- package/dist/microservice/config-center/index.d.ts +9 -0
- package/dist/microservice/config-center/index.d.ts.map +1 -0
- package/dist/microservice/config-center/nacos-config-center.d.ts +37 -0
- package/dist/microservice/config-center/nacos-config-center.d.ts.map +1 -0
- package/dist/microservice/config-center/nacos-decorators.d.ts +24 -0
- package/dist/microservice/config-center/nacos-decorators.d.ts.map +1 -0
- package/dist/microservice/config-center/types.d.ts +63 -0
- package/dist/microservice/config-center/types.d.ts.map +1 -0
- package/dist/microservice/governance/circuit-breaker.d.ts +54 -0
- package/dist/microservice/governance/circuit-breaker.d.ts.map +1 -0
- package/dist/microservice/governance/decorators.d.ts +51 -0
- package/dist/microservice/governance/decorators.d.ts.map +1 -0
- package/dist/microservice/governance/index.d.ts +9 -0
- package/dist/microservice/governance/index.d.ts.map +1 -0
- package/dist/microservice/governance/rate-limiter.d.ts +26 -0
- package/dist/microservice/governance/rate-limiter.d.ts.map +1 -0
- package/dist/microservice/governance/redis-rate-limiter.d.ts +76 -0
- package/dist/microservice/governance/redis-rate-limiter.d.ts.map +1 -0
- package/dist/microservice/governance/retry-strategy.d.ts +21 -0
- package/dist/microservice/governance/retry-strategy.d.ts.map +1 -0
- package/dist/microservice/governance/types.d.ts +212 -0
- package/dist/microservice/governance/types.d.ts.map +1 -0
- package/dist/microservice/index.d.ts +10 -0
- package/dist/microservice/index.d.ts.map +1 -0
- package/dist/microservice/monitoring/index.d.ts +4 -0
- package/dist/microservice/monitoring/index.d.ts.map +1 -0
- package/dist/microservice/monitoring/metrics-collector.d.ts +54 -0
- package/dist/microservice/monitoring/metrics-collector.d.ts.map +1 -0
- package/dist/microservice/monitoring/metrics-integration.d.ts +24 -0
- package/dist/microservice/monitoring/metrics-integration.d.ts.map +1 -0
- package/dist/microservice/monitoring/types.d.ts +99 -0
- package/dist/microservice/monitoring/types.d.ts.map +1 -0
- package/dist/microservice/service-client/call-decorators.d.ts +52 -0
- package/dist/microservice/service-client/call-decorators.d.ts.map +1 -0
- package/dist/microservice/service-client/decorators.d.ts +35 -0
- package/dist/microservice/service-client/decorators.d.ts.map +1 -0
- package/dist/microservice/service-client/index.d.ts +7 -0
- package/dist/microservice/service-client/index.d.ts.map +1 -0
- package/dist/microservice/service-client/interceptors.d.ts +96 -0
- package/dist/microservice/service-client/interceptors.d.ts.map +1 -0
- package/dist/microservice/service-client/load-balancer.d.ts +59 -0
- package/dist/microservice/service-client/load-balancer.d.ts.map +1 -0
- package/dist/microservice/service-client/service-client.d.ts +74 -0
- package/dist/microservice/service-client/service-client.d.ts.map +1 -0
- package/dist/microservice/service-client/types.d.ts +155 -0
- package/dist/microservice/service-client/types.d.ts.map +1 -0
- package/dist/microservice/service-registry/decorators.d.ts +84 -0
- package/dist/microservice/service-registry/decorators.d.ts.map +1 -0
- package/dist/microservice/service-registry/discovery-decorators.d.ts +58 -0
- package/dist/microservice/service-registry/discovery-decorators.d.ts.map +1 -0
- package/dist/microservice/service-registry/health-integration.d.ts +32 -0
- package/dist/microservice/service-registry/health-integration.d.ts.map +1 -0
- package/dist/microservice/service-registry/index.d.ts +10 -0
- package/dist/microservice/service-registry/index.d.ts.map +1 -0
- package/dist/microservice/service-registry/nacos-service-registry.d.ts +68 -0
- package/dist/microservice/service-registry/nacos-service-registry.d.ts.map +1 -0
- package/dist/microservice/service-registry/service-registry-module.d.ts +48 -0
- package/dist/microservice/service-registry/service-registry-module.d.ts.map +1 -0
- package/dist/microservice/service-registry/types.d.ts +121 -0
- package/dist/microservice/service-registry/types.d.ts.map +1 -0
- package/dist/microservice/tracing/collectors.d.ts +27 -0
- package/dist/microservice/tracing/collectors.d.ts.map +1 -0
- package/dist/microservice/tracing/index.d.ts +4 -0
- package/dist/microservice/tracing/index.d.ts.map +1 -0
- package/dist/microservice/tracing/tracer.d.ts +59 -0
- package/dist/microservice/tracing/tracer.d.ts.map +1 -0
- package/dist/microservice/tracing/types.d.ts +179 -0
- package/dist/microservice/tracing/types.d.ts.map +1 -0
- package/dist/request/request.d.ts +1 -0
- package/dist/request/request.d.ts.map +1 -1
- package/docs/microservice-config-center.md +258 -0
- package/docs/microservice-nacos.md +346 -0
- package/docs/microservice-service-registry.md +306 -0
- package/docs/microservice.md +680 -0
- package/docs/troubleshooting.md +41 -0
- package/docs/zh/troubleshooting.md +41 -0
- package/package.json +5 -4
- package/src/config/config-module.ts +210 -0
- package/src/config/service.ts +52 -1
- package/src/config/types.ts +31 -0
- package/src/controller/controller.ts +8 -0
- package/src/controller/decorators.ts +55 -0
- package/src/controller/index.ts +16 -2
- package/src/controller/param-binder.ts +87 -1
- package/src/core/application.ts +100 -2
- package/src/core/context.ts +1 -0
- package/src/core/server.ts +14 -0
- package/src/index.ts +98 -2
- package/src/microservice/config-center/config-center-module.ts +98 -0
- package/src/microservice/config-center/decorators.ts +159 -0
- package/src/microservice/config-center/index.ts +13 -0
- package/src/microservice/config-center/nacos-config-center.ts +126 -0
- package/src/microservice/config-center/nacos-decorators.ts +34 -0
- package/src/microservice/config-center/types.ts +80 -0
- package/src/microservice/governance/circuit-breaker.ts +229 -0
- package/src/microservice/governance/decorators.ts +113 -0
- package/src/microservice/governance/index.ts +18 -0
- package/src/microservice/governance/rate-limiter.ts +72 -0
- package/src/microservice/governance/redis-rate-limiter.ts +154 -0
- package/src/microservice/governance/retry-strategy.ts +74 -0
- package/src/microservice/governance/types.ts +247 -0
- package/src/microservice/index.ts +12 -0
- package/src/microservice/monitoring/index.ts +8 -0
- package/src/microservice/monitoring/metrics-collector.ts +223 -0
- package/src/microservice/monitoring/metrics-integration.ts +154 -0
- package/src/microservice/monitoring/types.ts +118 -0
- package/src/microservice/service-client/call-decorators.ts +107 -0
- package/src/microservice/service-client/decorators.ts +87 -0
- package/src/microservice/service-client/index.ts +37 -0
- package/src/microservice/service-client/interceptors.ts +182 -0
- package/src/microservice/service-client/load-balancer.ts +205 -0
- package/src/microservice/service-client/service-client.ts +488 -0
- package/src/microservice/service-client/types.ts +186 -0
- package/src/microservice/service-registry/decorators.ts +238 -0
- package/src/microservice/service-registry/discovery-decorators.ts +156 -0
- package/src/microservice/service-registry/health-integration.ts +146 -0
- package/src/microservice/service-registry/index.ts +20 -0
- package/src/microservice/service-registry/nacos-service-registry.ts +259 -0
- package/src/microservice/service-registry/service-registry-module.ts +105 -0
- package/src/microservice/service-registry/types.ts +149 -0
- package/src/microservice/tracing/collectors.ts +50 -0
- package/src/microservice/tracing/index.ts +15 -0
- package/src/microservice/tracing/tracer.ts +293 -0
- package/src/microservice/tracing/types.ts +213 -0
- package/src/request/request.ts +1 -0
- package/tests/config/set-value-by-path.test.ts +53 -0
- package/tests/controller/param-map.test.ts +237 -0
- package/tests/microservice/config-center.test.ts +77 -0
- package/tests/microservice/governance.test.ts +157 -0
- package/tests/microservice/monitoring.test.ts +75 -0
- package/tests/microservice/service-client.test.ts +136 -0
- package/tests/microservice/service-registry.test.ts +80 -0
- package/tests/microservice/tracing.test.ts +143 -0
- package/tests/utils/test-port.ts +29 -19
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { ParamBinder } from '../../src/controller/param-binder';
|
|
4
|
+
import {
|
|
5
|
+
QueryMap,
|
|
6
|
+
HeaderMap,
|
|
7
|
+
type QueryMapOptions,
|
|
8
|
+
type HeaderMapOptions,
|
|
9
|
+
} from '../../src/controller/decorators';
|
|
10
|
+
import { Context } from '../../src/core/context';
|
|
11
|
+
|
|
12
|
+
class QueryMapController {
|
|
13
|
+
public constructor(public readonly store: unknown[] = []) {}
|
|
14
|
+
|
|
15
|
+
public async handle(@QueryMap() query: Record<string, string | string[]>) {
|
|
16
|
+
this.store.push(query);
|
|
17
|
+
return query;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async handleTransformed(
|
|
21
|
+
@QueryMap<{ name: string; age: number }>((input) => ({
|
|
22
|
+
name: (input.name as string) ?? '',
|
|
23
|
+
age: Number((input.age as string) ?? '0'),
|
|
24
|
+
}))
|
|
25
|
+
query: { name: string; age: number },
|
|
26
|
+
) {
|
|
27
|
+
this.store.push(query);
|
|
28
|
+
return query;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async handleValidated(
|
|
32
|
+
@QueryMap<{ foo: string }>(
|
|
33
|
+
((input) => ({ foo: input.foo as string })) as QueryMapOptions<{ foo: string }>['transform'],
|
|
34
|
+
)
|
|
35
|
+
query: { foo: string },
|
|
36
|
+
) {
|
|
37
|
+
if (!query.foo) {
|
|
38
|
+
throw new Error('validation failed');
|
|
39
|
+
}
|
|
40
|
+
this.store.push(query);
|
|
41
|
+
return query;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class HeaderMapController {
|
|
46
|
+
public constructor(public readonly store: unknown[] = []) {}
|
|
47
|
+
|
|
48
|
+
public async handle(
|
|
49
|
+
@HeaderMap() headers: Record<string, string | string[]>,
|
|
50
|
+
) {
|
|
51
|
+
this.store.push(headers);
|
|
52
|
+
return headers;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async handlePicked(
|
|
56
|
+
@HeaderMap({
|
|
57
|
+
normalize: true,
|
|
58
|
+
pick: ['x-custom', 'x-list'],
|
|
59
|
+
})
|
|
60
|
+
headers: Record<string, string | string[]>,
|
|
61
|
+
) {
|
|
62
|
+
this.store.push(headers);
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async handlePickedWithNormalizeFalse(
|
|
67
|
+
@HeaderMap({
|
|
68
|
+
normalize: false,
|
|
69
|
+
pick: ['X-Custom', 'X-List'], // 混合大小写
|
|
70
|
+
})
|
|
71
|
+
headers: Record<string, string | string[]>,
|
|
72
|
+
) {
|
|
73
|
+
this.store.push(headers);
|
|
74
|
+
return headers;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async handleTransformed(
|
|
78
|
+
@HeaderMap<{ token: string }>((input) => ({
|
|
79
|
+
token: (input.authorization as string) ?? '',
|
|
80
|
+
}))
|
|
81
|
+
headers: { token: string },
|
|
82
|
+
) {
|
|
83
|
+
this.store.push(headers);
|
|
84
|
+
return headers;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createContext(url: string, init?: RequestInit) {
|
|
89
|
+
return new Context(new Request(url, init));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('ParamBinder QueryMap', () => {
|
|
93
|
+
test('should aggregate query params into map with arrays', async () => {
|
|
94
|
+
const controller = new QueryMapController();
|
|
95
|
+
const ctx = createContext('http://localhost/api?q=1&q=2&name=alice');
|
|
96
|
+
const params = await ParamBinder.bind(
|
|
97
|
+
controller,
|
|
98
|
+
'handle',
|
|
99
|
+
ctx,
|
|
100
|
+
);
|
|
101
|
+
expect(params[0]).toEqual({ q: ['1', '2'], name: 'alice' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should transform query map', async () => {
|
|
105
|
+
const controller = new QueryMapController();
|
|
106
|
+
const ctx = createContext('http://localhost/api?name=alice&age=20');
|
|
107
|
+
const params = await ParamBinder.bind(
|
|
108
|
+
controller,
|
|
109
|
+
'handleTransformed',
|
|
110
|
+
ctx,
|
|
111
|
+
);
|
|
112
|
+
expect(params[0]).toEqual({ name: 'alice', age: 20 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should validate query map via user code', async () => {
|
|
116
|
+
const controller = new QueryMapController();
|
|
117
|
+
const ctx = createContext('http://localhost/api?foo=bar');
|
|
118
|
+
const params = await ParamBinder.bind(
|
|
119
|
+
controller,
|
|
120
|
+
'handleValidated',
|
|
121
|
+
ctx,
|
|
122
|
+
);
|
|
123
|
+
expect(params[0]).toEqual({ foo: 'bar' });
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('ParamBinder HeaderMap', () => {
|
|
128
|
+
test('should aggregate headers into map with optional array', async () => {
|
|
129
|
+
const controller = new HeaderMapController();
|
|
130
|
+
const ctx = createContext('http://localhost/api', {
|
|
131
|
+
headers: {
|
|
132
|
+
'X-Token': 'abc',
|
|
133
|
+
'X-List': 'a, b',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const params = await ParamBinder.bind(
|
|
137
|
+
controller,
|
|
138
|
+
'handle',
|
|
139
|
+
ctx,
|
|
140
|
+
);
|
|
141
|
+
expect(params[0]).toMatchObject({
|
|
142
|
+
'x-token': 'abc',
|
|
143
|
+
'x-list': ['a', 'b'],
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('should pick and normalize headers', async () => {
|
|
148
|
+
const controller = new HeaderMapController();
|
|
149
|
+
const ctx = createContext('http://localhost/api', {
|
|
150
|
+
headers: {
|
|
151
|
+
'X-Custom': 'val',
|
|
152
|
+
'X-List': 'a, b',
|
|
153
|
+
'Other': 'ignore',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const params = await ParamBinder.bind(
|
|
157
|
+
controller,
|
|
158
|
+
'handlePicked',
|
|
159
|
+
ctx,
|
|
160
|
+
);
|
|
161
|
+
expect(params[0]).toEqual({
|
|
162
|
+
'x-custom': 'val',
|
|
163
|
+
'x-list': ['a', 'b'],
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should pick headers with case-insensitive pick when normalize=true (default)', async () => {
|
|
168
|
+
const controller = new HeaderMapController();
|
|
169
|
+
const ctx = createContext('http://localhost/api', {
|
|
170
|
+
headers: {
|
|
171
|
+
'X-CUSTOM': 'val',
|
|
172
|
+
'X-LIST': 'a, b',
|
|
173
|
+
'Other': 'ignore',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const params = await ParamBinder.bind(
|
|
177
|
+
controller,
|
|
178
|
+
'handlePicked',
|
|
179
|
+
ctx,
|
|
180
|
+
);
|
|
181
|
+
expect(params[0]).toEqual({
|
|
182
|
+
'x-custom': 'val',
|
|
183
|
+
'x-list': ['a', 'b'],
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('should transform headers', async () => {
|
|
188
|
+
const controller = new HeaderMapController();
|
|
189
|
+
const ctx = createContext('http://localhost/api', {
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: ' Bearer token123 ',
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const params = await ParamBinder.bind(
|
|
195
|
+
controller,
|
|
196
|
+
'handleTransformed',
|
|
197
|
+
ctx,
|
|
198
|
+
);
|
|
199
|
+
expect(params[0]).toEqual({ token: 'Bearer token123' });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('should trim single header value when no comma', async () => {
|
|
203
|
+
const controller = new HeaderMapController();
|
|
204
|
+
const ctx = createContext('http://localhost/api', {
|
|
205
|
+
headers: {
|
|
206
|
+
'X-Single': ' abc ',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const params = await ParamBinder.bind(controller, 'handle', ctx);
|
|
210
|
+
expect(params[0]).toMatchObject({
|
|
211
|
+
'x-single': 'abc',
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('should pick headers with mixed-case pick keys when normalize=false', async () => {
|
|
216
|
+
const controller = new HeaderMapController();
|
|
217
|
+
const ctx = createContext('http://localhost/api', {
|
|
218
|
+
headers: {
|
|
219
|
+
'X-Custom': 'val',
|
|
220
|
+
'X-List': 'a, b',
|
|
221
|
+
'Other': 'ignore',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const params = await ParamBinder.bind(
|
|
225
|
+
controller,
|
|
226
|
+
'handlePickedWithNormalizeFalse',
|
|
227
|
+
ctx,
|
|
228
|
+
);
|
|
229
|
+
// Headers API 总是返回小写的 key,所以即使 normalize=false,结果中的 key 也是小写
|
|
230
|
+
expect(params[0]).toEqual({
|
|
231
|
+
'x-custom': 'val',
|
|
232
|
+
'x-list': ['a', 'b'],
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { ConfigCenterModule, CONFIG_CENTER_TOKEN, type ConfigCenter } from '../../src/microservice/config-center';
|
|
4
|
+
import { Container } from '../../src/di/container';
|
|
5
|
+
import { ModuleRegistry } from '../../src/di/module-registry';
|
|
6
|
+
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
7
|
+
import { ControllerRegistry } from '../../src/controller/controller';
|
|
8
|
+
|
|
9
|
+
describe('ConfigCenterModule', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// 清除模块元数据
|
|
12
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, ConfigCenterModule);
|
|
13
|
+
ControllerRegistry.getInstance().clear();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should register config center provider', () => {
|
|
17
|
+
ConfigCenterModule.forRoot({
|
|
18
|
+
provider: 'nacos',
|
|
19
|
+
nacos: {
|
|
20
|
+
client: {
|
|
21
|
+
serverList: ['http://localhost:8848'],
|
|
22
|
+
namespaceId: 'public',
|
|
23
|
+
},
|
|
24
|
+
watchInterval: 3000,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, ConfigCenterModule);
|
|
29
|
+
expect(metadata).toBeDefined();
|
|
30
|
+
expect(metadata.providers).toBeDefined();
|
|
31
|
+
|
|
32
|
+
const configCenterProvider = metadata.providers.find(
|
|
33
|
+
(provider: any) => provider.provide === CONFIG_CENTER_TOKEN,
|
|
34
|
+
);
|
|
35
|
+
expect(configCenterProvider).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should throw error when provider is not supported', () => {
|
|
39
|
+
expect(() => {
|
|
40
|
+
ConfigCenterModule.forRoot({
|
|
41
|
+
provider: 'unsupported' as any,
|
|
42
|
+
} as any);
|
|
43
|
+
}).toThrow('Unsupported config center provider');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should throw error when nacos config is missing', () => {
|
|
47
|
+
expect(() => {
|
|
48
|
+
ConfigCenterModule.forRoot({
|
|
49
|
+
provider: 'nacos',
|
|
50
|
+
} as any);
|
|
51
|
+
}).toThrow('Nacos configuration is required');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('ConfigCenter Interface (Mock)', () => {
|
|
56
|
+
test('should implement ConfigCenter interface', async () => {
|
|
57
|
+
const mockConfigCenter: ConfigCenter = {
|
|
58
|
+
async getConfig(dataId: string, groupName: string, namespaceId?: string) {
|
|
59
|
+
return {
|
|
60
|
+
content: JSON.stringify({ key: 'value' }),
|
|
61
|
+
md5: 'abc123',
|
|
62
|
+
lastModified: Date.now(),
|
|
63
|
+
contentType: 'application/json',
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
watchConfig(dataId: string, groupName: string, listener: any, namespaceId?: string) {
|
|
67
|
+
return () => {}; // 返回取消监听的函数
|
|
68
|
+
},
|
|
69
|
+
async close() {},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const result = await mockConfigCenter.getConfig('test-config', 'DEFAULT_GROUP');
|
|
73
|
+
expect(result.content).toBeDefined();
|
|
74
|
+
expect(result.md5).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { CircuitBreaker, CircuitBreakerState } from '../../src/microservice/governance/circuit-breaker';
|
|
3
|
+
import { RateLimiter } from '../../src/microservice/governance/rate-limiter';
|
|
4
|
+
import { RetryStrategyImpl } from '../../src/microservice/governance';
|
|
5
|
+
|
|
6
|
+
describe('CircuitBreaker', () => {
|
|
7
|
+
let circuitBreaker: CircuitBreaker;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
circuitBreaker = new CircuitBreaker({
|
|
11
|
+
failureThreshold: 0.5,
|
|
12
|
+
timeWindow: 60000,
|
|
13
|
+
minimumRequests: 5,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should start in CLOSED state', () => {
|
|
18
|
+
expect(circuitBreaker.getState()).toBe(CircuitBreakerState.CLOSED);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should execute successful request', async () => {
|
|
22
|
+
const result = await circuitBreaker.execute(async () => {
|
|
23
|
+
return { success: true };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result).toEqual({ success: true });
|
|
27
|
+
expect(circuitBreaker.getState()).toBe(CircuitBreakerState.CLOSED);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should use fallback when circuit is open', async () => {
|
|
31
|
+
// 模拟多次失败以打开熔断器
|
|
32
|
+
for (let i = 0; i < 10; i++) {
|
|
33
|
+
try {
|
|
34
|
+
await circuitBreaker.execute(async () => {
|
|
35
|
+
throw new Error('Service error');
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
// 忽略错误
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 熔断器应该打开
|
|
43
|
+
const fallback = async () => ({ fallback: true });
|
|
44
|
+
const result = await circuitBreaker.execute(async () => {
|
|
45
|
+
throw new Error('Service error');
|
|
46
|
+
}, fallback);
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual({ fallback: true });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('RateLimiter', () => {
|
|
53
|
+
let rateLimiter: RateLimiter;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
rateLimiter = new RateLimiter({
|
|
57
|
+
requestsPerSecond: 10,
|
|
58
|
+
timeWindow: 1000,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should allow requests within limit', async () => {
|
|
63
|
+
for (let i = 0; i < 10; i++) {
|
|
64
|
+
const allowed = await rateLimiter.allow('test-key');
|
|
65
|
+
expect(allowed).toBe(true);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should reject requests exceeding limit', async () => {
|
|
70
|
+
// 先允许 10 个请求
|
|
71
|
+
for (let i = 0; i < 10; i++) {
|
|
72
|
+
await rateLimiter.allow('test-key');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 第 11 个请求应该被拒绝
|
|
76
|
+
const allowed = await rateLimiter.allow('test-key');
|
|
77
|
+
expect(allowed).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should get remaining requests', () => {
|
|
81
|
+
rateLimiter.allow('test-key');
|
|
82
|
+
const remaining = rateLimiter.getRemaining('test-key');
|
|
83
|
+
expect(remaining).toBeLessThan(10);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should reset rate limiter', async () => {
|
|
87
|
+
// 使用完所有请求
|
|
88
|
+
for (let i = 0; i < 10; i++) {
|
|
89
|
+
await rateLimiter.allow('test-key');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rateLimiter.reset('test-key');
|
|
93
|
+
const allowed = await rateLimiter.allow('test-key');
|
|
94
|
+
expect(allowed).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('RetryStrategy', () => {
|
|
99
|
+
test('RetryStrategyImpl should retry with fixed delay', async () => {
|
|
100
|
+
const strategy = new RetryStrategyImpl({
|
|
101
|
+
maxRetries: 3,
|
|
102
|
+
retryDelay: 10, // 使用较小的延迟以便测试快速完成
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let attempts = 0;
|
|
106
|
+
const result = await strategy.execute(async () => {
|
|
107
|
+
attempts++;
|
|
108
|
+
if (attempts < 3) {
|
|
109
|
+
throw new Error('Retry needed');
|
|
110
|
+
}
|
|
111
|
+
return { success: true };
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result).toEqual({ success: true });
|
|
115
|
+
expect(attempts).toBe(3);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('RetryStrategyImpl should retry with exponential delay', async () => {
|
|
119
|
+
const strategy = new RetryStrategyImpl({
|
|
120
|
+
maxRetries: 3,
|
|
121
|
+
retryDelay: 10,
|
|
122
|
+
exponentialBackoff: true,
|
|
123
|
+
baseDelay: 10,
|
|
124
|
+
maxDelay: 1000,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
let attempts = 0;
|
|
128
|
+
const result = await strategy.execute(async () => {
|
|
129
|
+
attempts++;
|
|
130
|
+
if (attempts < 3) {
|
|
131
|
+
throw new Error('Retry needed');
|
|
132
|
+
}
|
|
133
|
+
return { success: true };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual({ success: true });
|
|
137
|
+
expect(attempts).toBe(3);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('should respect maxRetries limit', async () => {
|
|
141
|
+
const strategy = new RetryStrategyImpl({
|
|
142
|
+
maxRetries: 2,
|
|
143
|
+
retryDelay: 10,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
let attempts = 0;
|
|
147
|
+
await expect(
|
|
148
|
+
strategy.execute(async () => {
|
|
149
|
+
attempts++;
|
|
150
|
+
throw new Error('Always fail');
|
|
151
|
+
}),
|
|
152
|
+
).rejects.toThrow('Always fail');
|
|
153
|
+
|
|
154
|
+
expect(attempts).toBe(3); // 1 initial + 2 retries
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { ServiceMetricsCollector } from '../../src/microservice/monitoring/metrics-collector';
|
|
3
|
+
|
|
4
|
+
describe('ServiceMetricsCollector', () => {
|
|
5
|
+
let collector: ServiceMetricsCollector;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
collector = new ServiceMetricsCollector({
|
|
9
|
+
enabled: true,
|
|
10
|
+
autoReportToMetrics: false, // 测试时不自动上报
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should create metrics collector', () => {
|
|
15
|
+
expect(collector).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should record service call', () => {
|
|
19
|
+
collector.recordCall('test-service', '127.0.0.1:3000', true, 100);
|
|
20
|
+
|
|
21
|
+
const metrics = collector.getMetrics('test-service');
|
|
22
|
+
expect(metrics.length).toBeGreaterThan(0);
|
|
23
|
+
expect(metrics[0]?.totalRequests).toBe(1);
|
|
24
|
+
expect(metrics[0]?.successRequests).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should calculate error rate', () => {
|
|
28
|
+
// 记录 10 次调用,5 次成功,5 次失败
|
|
29
|
+
for (let i = 0; i < 5; i++) {
|
|
30
|
+
collector.recordCall('test-service', '127.0.0.1:3000', true, 100);
|
|
31
|
+
}
|
|
32
|
+
for (let i = 0; i < 5; i++) {
|
|
33
|
+
collector.recordCall('test-service', '127.0.0.1:3000', false, 200);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const metrics = collector.getMetrics('test-service');
|
|
37
|
+
expect(metrics[0]?.errorRate).toBe(0.5);
|
|
38
|
+
expect(metrics[0]?.totalRequests).toBe(10);
|
|
39
|
+
expect(metrics[0]?.successRequests).toBe(5);
|
|
40
|
+
expect(metrics[0]?.failureRequests).toBe(5);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should track health status', () => {
|
|
44
|
+
// 记录多次失败
|
|
45
|
+
for (let i = 0; i < 3; i++) {
|
|
46
|
+
collector.recordCall('test-service', '127.0.0.1:3000', false, 500);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const healthStatus = collector.getHealthStatus('test-service');
|
|
50
|
+
expect(healthStatus.length).toBeGreaterThan(0);
|
|
51
|
+
expect(healthStatus[0]?.healthy).toBe(false);
|
|
52
|
+
expect(healthStatus[0]?.consecutiveFailures).toBeGreaterThanOrEqual(3);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should reset metrics', () => {
|
|
56
|
+
collector.recordCall('test-service', '127.0.0.1:3000', true, 100);
|
|
57
|
+
collector.reset('test-service', '127.0.0.1:3000');
|
|
58
|
+
|
|
59
|
+
const metrics = collector.getMetrics('test-service');
|
|
60
|
+
expect(metrics.length).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should calculate latency statistics', () => {
|
|
64
|
+
const latencies = [50, 100, 150, 200, 250];
|
|
65
|
+
for (const latency of latencies) {
|
|
66
|
+
collector.recordCall('test-service', '127.0.0.1:3000', true, latency);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const metrics = collector.getMetrics('test-service');
|
|
70
|
+
expect(metrics[0]?.averageLatency).toBe(150);
|
|
71
|
+
expect(metrics[0]?.minLatency).toBe(50);
|
|
72
|
+
expect(metrics[0]?.maxLatency).toBe(250);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { ServiceClient } from '../../src/microservice/service-client/service-client';
|
|
3
|
+
import { LoadBalancerFactory } from '../../src/microservice/service-client/load-balancer';
|
|
4
|
+
import type { ServiceRegistry, ServiceInstance } from '../../src/microservice/service-registry/types';
|
|
5
|
+
|
|
6
|
+
describe('ServiceClient', () => {
|
|
7
|
+
let mockServiceRegistry: ServiceRegistry;
|
|
8
|
+
let serviceClient: ServiceClient;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// 创建 Mock ServiceRegistry
|
|
12
|
+
mockServiceRegistry = {
|
|
13
|
+
async register(instance: ServiceInstance) {},
|
|
14
|
+
async deregister(instance: ServiceInstance) {},
|
|
15
|
+
async renew(instance: ServiceInstance) {},
|
|
16
|
+
async getInstances(serviceName: string) {
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
serviceName,
|
|
20
|
+
ip: '127.0.0.1',
|
|
21
|
+
port: 3000,
|
|
22
|
+
healthy: true,
|
|
23
|
+
weight: 100,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
serviceName,
|
|
27
|
+
ip: '127.0.0.1',
|
|
28
|
+
port: 3001,
|
|
29
|
+
healthy: true,
|
|
30
|
+
weight: 200,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
},
|
|
34
|
+
watchInstances() {
|
|
35
|
+
return () => {};
|
|
36
|
+
},
|
|
37
|
+
async close() {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
serviceClient = new ServiceClient(mockServiceRegistry);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should create ServiceClient instance', () => {
|
|
44
|
+
expect(serviceClient).toBeInstanceOf(ServiceClient);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should throw error when no instances found', async () => {
|
|
48
|
+
const emptyRegistry: ServiceRegistry = {
|
|
49
|
+
...mockServiceRegistry,
|
|
50
|
+
async getInstances() {
|
|
51
|
+
return [];
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const client = new ServiceClient(emptyRegistry);
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
client.call({
|
|
59
|
+
serviceName: 'test-service',
|
|
60
|
+
method: 'GET',
|
|
61
|
+
path: '/api/test',
|
|
62
|
+
}),
|
|
63
|
+
).rejects.toThrow('No instances found');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should add request interceptor', () => {
|
|
67
|
+
const interceptor = {
|
|
68
|
+
intercept: async (options: any) => {
|
|
69
|
+
options.headers = { ...options.headers, 'X-Custom': 'value' };
|
|
70
|
+
return options;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
serviceClient.addRequestInterceptor(interceptor);
|
|
75
|
+
// 验证拦截器已添加(通过调用时验证)
|
|
76
|
+
expect(serviceClient).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should add response interceptor', () => {
|
|
80
|
+
const interceptor = {
|
|
81
|
+
intercept: async (response: any) => {
|
|
82
|
+
return response;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
serviceClient.addResponseInterceptor(interceptor);
|
|
87
|
+
// 验证拦截器已添加
|
|
88
|
+
expect(serviceClient).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('LoadBalancerFactory', () => {
|
|
93
|
+
const instances: ServiceInstance[] = [
|
|
94
|
+
{ serviceName: 'test', ip: '127.0.0.1', port: 3000, weight: 100 },
|
|
95
|
+
{ serviceName: 'test', ip: '127.0.0.1', port: 3001, weight: 200 },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
test('should create RandomLoadBalancer', () => {
|
|
99
|
+
const balancer = LoadBalancerFactory.create('random');
|
|
100
|
+
expect(balancer).toBeDefined();
|
|
101
|
+
const instance = balancer.select(instances);
|
|
102
|
+
expect(instance).toBeDefined();
|
|
103
|
+
expect(instances).toContain(instance);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should create RoundRobinLoadBalancer', () => {
|
|
107
|
+
const balancer = LoadBalancerFactory.create('roundRobin');
|
|
108
|
+
expect(balancer).toBeDefined();
|
|
109
|
+
const instance1 = balancer.select(instances);
|
|
110
|
+
const instance2 = balancer.select(instances);
|
|
111
|
+
expect(instance1).toBeDefined();
|
|
112
|
+
expect(instance2).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should create WeightedRoundRobinLoadBalancer', () => {
|
|
116
|
+
const balancer = LoadBalancerFactory.create('weightedRoundRobin');
|
|
117
|
+
expect(balancer).toBeDefined();
|
|
118
|
+
const instance = balancer.select(instances);
|
|
119
|
+
expect(instance).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should create ConsistentHashLoadBalancer', () => {
|
|
123
|
+
const balancer = LoadBalancerFactory.create('consistentHash');
|
|
124
|
+
expect(balancer).toBeDefined();
|
|
125
|
+
const instance = balancer.select(instances, 'test-key');
|
|
126
|
+
expect(instance).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should create LeastActiveLoadBalancer', () => {
|
|
130
|
+
const balancer = LoadBalancerFactory.create('leastActive');
|
|
131
|
+
expect(balancer).toBeDefined();
|
|
132
|
+
const instance = balancer.select(instances);
|
|
133
|
+
expect(instance).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|