cabloy 5.1.240 → 5.1.242
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/.cabloy-version +1 -1
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/repo-docs/backend/i18n-guide.md +109 -11
- package/repo-docs/backend/openapi-guide.md +18 -0
- package/repo-docs/frontend/environment-config-guide.md +43 -0
- package/repo-docs/frontend/module-scope.md +88 -0
- package/repo-docs/frontend/system-startup-guide.md +31 -0
- package/repo-e2e/specs/home-user-account.spec.ts +53 -8
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +17 -142
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/dto/operatorContext.ts +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/service/operator.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/operatorAccess.test.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/accountCurrent.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/accountProfileUpdate.ts +9 -2
- package/vona/src/suite/a-home/modules/home-user/src/dto/passportUser.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/service/account.ts +7 -3
- package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/account.test.ts +133 -8
- package/vona/src/suite/a-home/modules/home-user/test/accountOpenapi.test.ts +32 -5
- package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +159 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/bean.captcha.ts +271 -137
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/cacheRedis.captcha.ts +199 -2
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/lib/captchaLua.ts +90 -0
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captcha.ts +15 -1
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captchaProvider.ts +38 -1
- package/vona/src/suite-vendor/a-captcha/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +54 -8
- package/vona/src/suite-vendor/a-vona/modules/a-permission/test/permission.test.ts +12 -15
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +1 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/schemas.ts +68 -52
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/types.ts +530 -344
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/page/dashboard/controller.tsx +7 -1
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +6 -1
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +1 -1
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx +3 -1
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +4 -0
- package/zova/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-home/modules/home-user/src/model/account.ts +2 -1
- package/zova/src/suite/a-home/modules/home-user/src/page/account/controller.tsx +17 -3
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/test/aggregate.test.ts +0 -247
package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/page/dashboard/controller.tsx
CHANGED
|
@@ -19,12 +19,18 @@ export class ControllerPageDashboard extends BeanControllerPageBase {
|
|
|
19
19
|
|
|
20
20
|
protected render() {
|
|
21
21
|
const query = this.queryOperatorContext;
|
|
22
|
+
const operator = query.data!;
|
|
23
|
+
const user = this.$passport.user;
|
|
24
|
+
const displayName =
|
|
25
|
+
user && String(user.id) === String(operator.userId)
|
|
26
|
+
? this.$passport.displayName
|
|
27
|
+
: operator.displayName || operator.userName;
|
|
22
28
|
return (
|
|
23
29
|
<div class="p-6">
|
|
24
30
|
<h1 class="text-2xl font-semibold">{this.scope.locale.CommerceAdministration()}</h1>
|
|
25
31
|
<dl class="mt-4 grid max-w-xl grid-cols-[auto_1fr] gap-x-4 gap-y-2">
|
|
26
32
|
<dt>{this.scope.locale.Operator()}</dt>
|
|
27
|
-
<dd>{
|
|
33
|
+
<dd>{displayName}</dd>
|
|
28
34
|
<dt>{this.scope.locale.Instance()}</dt>
|
|
29
35
|
<dd>{query.data!.instanceName || 'default'}</dd>
|
|
30
36
|
</dl>
|
|
@@ -2596,6 +2596,7 @@ export interface components {
|
|
|
2596
2596
|
| undefined;
|
|
2597
2597
|
'home-user.dto.accountCurrent': {
|
|
2598
2598
|
name: string;
|
|
2599
|
+
displayName?: string | undefined;
|
|
2599
2600
|
avatar?: string | undefined;
|
|
2600
2601
|
locale?: unknown;
|
|
2601
2602
|
tz?: string | undefined;
|
|
@@ -2605,7 +2606,7 @@ export interface components {
|
|
|
2605
2606
|
};
|
|
2606
2607
|
'home-user.dto.accountProfileUpdate': {
|
|
2607
2608
|
/** @description Display name */
|
|
2608
|
-
|
|
2609
|
+
displayName?: string | undefined;
|
|
2609
2610
|
/**
|
|
2610
2611
|
* Avatar
|
|
2611
2612
|
* @description Absolute HTTPS avatar URL or API delivery URL
|
|
@@ -2673,6 +2674,7 @@ export interface components {
|
|
|
2673
2674
|
'home-user.dto.passportUser': {
|
|
2674
2675
|
id: number | string;
|
|
2675
2676
|
name: string;
|
|
2677
|
+
displayName?: string | undefined;
|
|
2676
2678
|
avatar?: string | undefined;
|
|
2677
2679
|
email?: string | undefined;
|
|
2678
2680
|
mobile?: string | undefined;
|
|
@@ -2783,6 +2785,7 @@ export interface components {
|
|
|
2783
2785
|
instanceName: string;
|
|
2784
2786
|
userId: string;
|
|
2785
2787
|
userName: string;
|
|
2788
|
+
displayName?: string | undefined;
|
|
2786
2789
|
};
|
|
2787
2790
|
'commerce-promotion.dto.couponMineItem': {
|
|
2788
2791
|
id: number | string;
|
|
@@ -3215,7 +3218,9 @@ export interface components {
|
|
|
3215
3218
|
shippedAt: Date;
|
|
3216
3219
|
};
|
|
3217
3220
|
'commerce-trade.dto.orderShip': {
|
|
3221
|
+
/** @description Carrier */
|
|
3218
3222
|
carrier: string;
|
|
3223
|
+
/** @description Tracking number */
|
|
3219
3224
|
trackingNumber: string;
|
|
3220
3225
|
};
|
|
3221
3226
|
'commerce-trade.dto.orderSelectRes': {
|
|
@@ -5,6 +5,7 @@ export default {
|
|
|
5
5
|
LanguageEnglish: 'English',
|
|
6
6
|
LanguageChinese: 'Chinese',
|
|
7
7
|
LanguageJapanese: 'Japanese',
|
|
8
|
+
LanguageTraditionalChinese: 'Traditional Chinese',
|
|
8
9
|
VonaIntegratedSsr: 'Vona integrated SSR',
|
|
9
10
|
ZovaStandaloneSsr: 'Zova standalone SSR',
|
|
10
11
|
SsrSite: 'SSR Site',
|
package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx
CHANGED
|
@@ -21,7 +21,9 @@ export class RenderUser extends BeanRenderBase {
|
|
|
21
21
|
) || this.$scopeBase.config.avatar.empty
|
|
22
22
|
}
|
|
23
23
|
/>
|
|
24
|
-
{this.$passport.
|
|
24
|
+
{this.$passport.isAuthenticated
|
|
25
|
+
? this.$passport.displayName
|
|
26
|
+
: this.scope.locale.NotLoggedIn()}
|
|
25
27
|
</summary>
|
|
26
28
|
<ul class="bg-base-100 rounded-t-none p-2 w-40">
|
|
27
29
|
{this.$passport.isAuthenticated ? (
|
|
@@ -187,6 +187,10 @@ export class ModelPassport extends BeanModelBase {
|
|
|
187
187
|
return this.passport?.user;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
get displayName(): string | undefined {
|
|
191
|
+
return this.user?.displayName || this.user?.name;
|
|
192
|
+
}
|
|
193
|
+
|
|
190
194
|
get roles() {
|
|
191
195
|
return this.passport?.roles;
|
|
192
196
|
}
|
|
@@ -21,8 +21,9 @@ export interface IModelOptionsAccount extends IDecoratorModelOptions {}
|
|
|
21
21
|
|
|
22
22
|
export type AccountProfileUpdateCommand = Omit<
|
|
23
23
|
ApiApiHomeUserAccountupdateProfileRequestBody,
|
|
24
|
-
'avatar' | 'locale' | 'tz'
|
|
24
|
+
'displayName' | 'avatar' | 'locale' | 'tz'
|
|
25
25
|
> & {
|
|
26
|
+
displayName?: string | null;
|
|
26
27
|
avatar?: string | null;
|
|
27
28
|
locale?: string | null;
|
|
28
29
|
tz?: string | null;
|
|
@@ -81,7 +81,11 @@ export class ControllerPageAccount extends BeanControllerPageBase {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async submitProfile(data: TypeFormOnSubmitData<ProfileDraft>) {
|
|
84
|
-
const
|
|
84
|
+
const displayName = data.value.displayName;
|
|
85
|
+
const current = await this.$$modelAccount.updateProfile().mutateAsync({
|
|
86
|
+
...data.value,
|
|
87
|
+
displayName: displayName === undefined ? undefined : displayName?.trim() || null,
|
|
88
|
+
});
|
|
85
89
|
this._resetProfileDraft(current);
|
|
86
90
|
this.profileMessage = this.scope.locale.AccountProfileSaved();
|
|
87
91
|
}
|
|
@@ -119,7 +123,7 @@ export class ControllerPageAccount extends BeanControllerPageBase {
|
|
|
119
123
|
private _resetProfileDraft(current = this.queryCurrent?.data) {
|
|
120
124
|
if (!current) return;
|
|
121
125
|
this.profileDraft = {
|
|
122
|
-
|
|
126
|
+
displayName: current.displayName ?? '',
|
|
123
127
|
avatar: current.avatar ?? undefined,
|
|
124
128
|
locale: typeof current.locale === 'string' ? current.locale : undefined,
|
|
125
129
|
tz: current.tz ?? undefined,
|
|
@@ -254,8 +258,18 @@ export class ControllerPageAccount extends BeanControllerPageBase {
|
|
|
254
258
|
);
|
|
255
259
|
}}
|
|
256
260
|
/>
|
|
261
|
+
<label class="form-control gap-2">
|
|
262
|
+
<span class="label-text">{this.scope.locale.AccountUserName()}</span>
|
|
263
|
+
<input
|
|
264
|
+
class="input"
|
|
265
|
+
type="text"
|
|
266
|
+
readonly
|
|
267
|
+
autocomplete="username"
|
|
268
|
+
value={this.queryCurrent?.data?.name}
|
|
269
|
+
/>
|
|
270
|
+
</label>
|
|
257
271
|
<ZFormFieldPreset
|
|
258
|
-
name="
|
|
272
|
+
name="displayName"
|
|
259
273
|
render="basic-input:formFieldInput"
|
|
260
274
|
options={{ type: 'text' }}
|
|
261
275
|
/>
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import type { IMiddlewareSystemOptionsAggregate, RateLimitMode } from 'vona-module-a-ratelimit';
|
|
2
|
-
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { once } from 'node:events';
|
|
6
|
-
import { createServer } from 'node:http';
|
|
7
|
-
import { describe, it } from 'node:test';
|
|
8
|
-
import { setTimeout } from 'node:timers/promises';
|
|
9
|
-
import { cast, compose } from 'vona';
|
|
10
|
-
import { app } from 'vona-mock';
|
|
11
|
-
import { MiddlewareSystemAggregate, ServiceRateLimit } from 'vona-module-a-ratelimit';
|
|
12
|
-
|
|
13
|
-
const exemptPath = '/api/test/vona/performAction/rateLimitExempt';
|
|
14
|
-
const limitedPath = '/api/test/vona/performAction/rateLimit';
|
|
15
|
-
|
|
16
|
-
type AdmitCommand = (key: string, windowMs: string) => Promise<[number, number]>;
|
|
17
|
-
|
|
18
|
-
function createService(command: AdmitCommand) {
|
|
19
|
-
const service = app.bean._newBean(ServiceRateLimit);
|
|
20
|
-
Object.defineProperty(service, 'bean', {
|
|
21
|
-
value: { redis: { get: () => ({ rateLimitAdmit: command }) } },
|
|
22
|
-
});
|
|
23
|
-
return service;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type Request = (path: string, options?: RequestInit) => Promise<Response>;
|
|
27
|
-
|
|
28
|
-
async function withGate(
|
|
29
|
-
mode: RateLimitMode,
|
|
30
|
-
limit: number,
|
|
31
|
-
command: AdmitCommand,
|
|
32
|
-
fn: (request: Request) => Promise<void>,
|
|
33
|
-
) {
|
|
34
|
-
await app.bean.executor.mockCtx(async () => {
|
|
35
|
-
const middleware = app.bean._newBean(MiddlewareSystemAggregate);
|
|
36
|
-
Object.defineProperty(middleware, 'scope', {
|
|
37
|
-
value: { service: { rateLimit: createService(command) } },
|
|
38
|
-
});
|
|
39
|
-
const policyName = `test-aggregate-${randomUUID()}`;
|
|
40
|
-
// Keep admission and its listener local; never patch the shared application chain.
|
|
41
|
-
const chain = compose([
|
|
42
|
-
(_ctx, next) => middleware.execute({ rateLimit: { mode, limit } }, next),
|
|
43
|
-
...app.middleware,
|
|
44
|
-
]);
|
|
45
|
-
const server = createServer((req, res) => {
|
|
46
|
-
void app.bean.executor
|
|
47
|
-
.newCtx(
|
|
48
|
-
() => {
|
|
49
|
-
app.ctx.onionsDynamic = {
|
|
50
|
-
interceptor: { 'a-ratelimit:rateLimit': { rateLimit: { name: policyName } } },
|
|
51
|
-
};
|
|
52
|
-
return cast(app).handleRequest(app.ctx, chain);
|
|
53
|
-
},
|
|
54
|
-
{ dbInfo: { level: 0 }, innerAccess: false, req, res },
|
|
55
|
-
)
|
|
56
|
-
.catch(error => res.destroy(error));
|
|
57
|
-
});
|
|
58
|
-
try {
|
|
59
|
-
server.listen(0, '127.0.0.1');
|
|
60
|
-
await once(server, 'listening');
|
|
61
|
-
const address = server.address();
|
|
62
|
-
assert.ok(address && typeof address !== 'string');
|
|
63
|
-
await fn((path, options) =>
|
|
64
|
-
fetch(`http://127.0.0.1:${address.port}${path}`, {
|
|
65
|
-
...options,
|
|
66
|
-
headers: { accept: 'application/json', ...options?.headers },
|
|
67
|
-
signal: AbortSignal.timeout(10_000),
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
} finally {
|
|
71
|
-
await new Promise<void>((resolve, reject) => {
|
|
72
|
-
server.close(error => (error ? reject(error) : resolve()));
|
|
73
|
-
server.closeAllConnections();
|
|
74
|
-
});
|
|
75
|
-
const redis = app.bean.redis.get('limiter');
|
|
76
|
-
const prefix = redis.options.keyPrefix ?? '';
|
|
77
|
-
const keys = await redis.keys(`${prefix}rate-limit:v1:*:${encodeURIComponent(policyName)}:*`);
|
|
78
|
-
if (keys.length) await redis.del(...keys.map(key => key.slice(prefix.length)));
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function counter(): AdmitCommand {
|
|
84
|
-
let current = 0;
|
|
85
|
-
return async () => [++current, 1_000];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
describe('aggregate.test.ts', { concurrency: false }, () => {
|
|
89
|
-
it('shares an atomic Redis budget across request contexts and expires its counter', async () => {
|
|
90
|
-
const key = `test:aggregate:${randomUUID()}`;
|
|
91
|
-
const redis = cast(app.bean.redis.get('limiter'));
|
|
92
|
-
const keys: string[] = [];
|
|
93
|
-
// Isolate this test's bucket without changing the production key dimensions or TTL.
|
|
94
|
-
const service = createService((actualKey, windowMs) => {
|
|
95
|
-
keys.push(actualKey);
|
|
96
|
-
return redis.rateLimitAdmit(key, windowMs);
|
|
97
|
-
});
|
|
98
|
-
try {
|
|
99
|
-
const results = await Promise.all(
|
|
100
|
-
[0, 1, 2, 3].map(index =>
|
|
101
|
-
app.bean.executor.mockCtx(async () => {
|
|
102
|
-
app.ctx.req.url = `/aggregate-${index}`;
|
|
103
|
-
return service.admitAggregate({
|
|
104
|
-
mode: 'enforce',
|
|
105
|
-
limit: 2,
|
|
106
|
-
});
|
|
107
|
-
}),
|
|
108
|
-
),
|
|
109
|
-
);
|
|
110
|
-
assert.equal(results.filter(result => result.allowed).length, 2);
|
|
111
|
-
assert.ok(keys.every(actualKey => /^rate-limit:aggregate:v1:\d+$/.test(actualKey)));
|
|
112
|
-
assert.equal(await redis.get(key), '4');
|
|
113
|
-
const ttl = await redis.pttl(key);
|
|
114
|
-
assert.ok(ttl > 0 && ttl <= 1_000);
|
|
115
|
-
await setTimeout(ttl + 20);
|
|
116
|
-
assert.equal(await redis.get(key), null);
|
|
117
|
-
await app.bean.executor.mockCtx(async () => {
|
|
118
|
-
const result = await service.admitAggregate({
|
|
119
|
-
mode: 'enforce',
|
|
120
|
-
limit: 2,
|
|
121
|
-
});
|
|
122
|
-
assert.equal(result.allowed, true);
|
|
123
|
-
assert.equal(result.remaining, 1);
|
|
124
|
-
});
|
|
125
|
-
} finally {
|
|
126
|
-
await redis.del(key);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('counts health, preflight, unknown paths and identity-exempt actions in one HTTP budget', async () => {
|
|
131
|
-
await withGate('enforce', 3, counter(), async request => {
|
|
132
|
-
const health = await request('/health/live');
|
|
133
|
-
assert.equal(health.status, 200);
|
|
134
|
-
await health.text();
|
|
135
|
-
const preflight = await request('/aggregate-unknown', {
|
|
136
|
-
method: 'OPTIONS',
|
|
137
|
-
headers: { 'origin': 'https://example.test', 'access-control-request-method': 'GET' },
|
|
138
|
-
});
|
|
139
|
-
assert.notEqual(preflight.status, 429);
|
|
140
|
-
await preflight.text();
|
|
141
|
-
const unknown = await request(`/aggregate-unknown-${randomUUID()}`);
|
|
142
|
-
assert.equal(unknown.status, 404);
|
|
143
|
-
await unknown.text();
|
|
144
|
-
const rejected = await request(exemptPath);
|
|
145
|
-
assert.equal(rejected.status, 429);
|
|
146
|
-
assert.equal(rejected.headers.get('retry-after'), '1');
|
|
147
|
-
assert.equal(rejected.headers.get('ratelimit-limit'), null);
|
|
148
|
-
assert.deepEqual(await rejected.json(), { code: 429, message: 'Too Many Requests' });
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('observes excess requests without rejecting or replacing identity headers', async () => {
|
|
153
|
-
await withGate('observe', 1, counter(), async request => {
|
|
154
|
-
for (let index = 0; index < 2; index++) {
|
|
155
|
-
const response = await request(exemptPath);
|
|
156
|
-
assert.equal(response.status, 200);
|
|
157
|
-
assert.equal(response.headers.get('ratelimit-limit'), null);
|
|
158
|
-
assert.equal(response.headers.get('retry-after'), null);
|
|
159
|
-
await response.text();
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
await withGate('enforce', 100, counter(), async request => {
|
|
163
|
-
for (let index = 0; index < 2; index++) {
|
|
164
|
-
const response = await request(limitedPath);
|
|
165
|
-
assert.equal(response.status, 200);
|
|
166
|
-
assert.equal(response.headers.get('ratelimit-limit'), '2');
|
|
167
|
-
await response.text();
|
|
168
|
-
}
|
|
169
|
-
const response = await request(limitedPath);
|
|
170
|
-
assert.equal(response.status, 429);
|
|
171
|
-
assert.equal(response.headers.get('ratelimit-limit'), '2');
|
|
172
|
-
await response.text();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('fails open only in observe mode and preserves HTTP 503 for HTML callers', async () => {
|
|
177
|
-
const unavailable: AdmitCommand = async () => {
|
|
178
|
-
throw new Error('redis unavailable');
|
|
179
|
-
};
|
|
180
|
-
await withGate('observe', 1, unavailable, async request => {
|
|
181
|
-
const response = await request('/health/live');
|
|
182
|
-
assert.equal(response.status, 200);
|
|
183
|
-
await response.text();
|
|
184
|
-
});
|
|
185
|
-
await withGate('enforce', 1, unavailable, async request => {
|
|
186
|
-
const response = await request(exemptPath, { headers: { accept: 'text/html' } });
|
|
187
|
-
assert.equal(response.status, 503);
|
|
188
|
-
assert.match(response.headers.get('content-type')!, /application\/json/);
|
|
189
|
-
assert.equal(response.headers.get('retry-after'), null);
|
|
190
|
-
assert.deepEqual(await response.json(), { code: 503, message: 'Service Unavailable' });
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('keeps competing gates and the shared application listener independent', async () => {
|
|
195
|
-
await Promise.all([
|
|
196
|
-
withGate(
|
|
197
|
-
'enforce',
|
|
198
|
-
1,
|
|
199
|
-
async () => [2, 1_000],
|
|
200
|
-
async request => {
|
|
201
|
-
const response = await request('/health/live');
|
|
202
|
-
assert.equal(response.status, 429);
|
|
203
|
-
await response.text();
|
|
204
|
-
const address = app.server.address();
|
|
205
|
-
assert.ok(address && typeof address !== 'string');
|
|
206
|
-
const shared = await fetch(`http://127.0.0.1:${address.port}/health/live`, {
|
|
207
|
-
signal: AbortSignal.timeout(10_000),
|
|
208
|
-
});
|
|
209
|
-
assert.equal(shared.status, 200);
|
|
210
|
-
await shared.text();
|
|
211
|
-
},
|
|
212
|
-
),
|
|
213
|
-
withGate(
|
|
214
|
-
'enforce',
|
|
215
|
-
1,
|
|
216
|
-
async () => [1, 1_000],
|
|
217
|
-
async request => {
|
|
218
|
-
const response = await request('/health/live');
|
|
219
|
-
assert.equal(response.status, 200);
|
|
220
|
-
await response.text();
|
|
221
|
-
},
|
|
222
|
-
),
|
|
223
|
-
]);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('requires valid explicit configuration even in observe mode', async () => {
|
|
227
|
-
await app.bean.executor.mockCtx(async () => {
|
|
228
|
-
const middleware = app.bean._newBean(MiddlewareSystemAggregate);
|
|
229
|
-
let downstream = false;
|
|
230
|
-
const next = async () => {
|
|
231
|
-
downstream = true;
|
|
232
|
-
};
|
|
233
|
-
for (const options of [
|
|
234
|
-
{},
|
|
235
|
-
{ rateLimit: { mode: 'observe', limit: 0 } },
|
|
236
|
-
{ rateLimit: { mode: 'observe', limit: 1.5 } },
|
|
237
|
-
{ rateLimit: { mode: 'invalid', limit: 1 } },
|
|
238
|
-
]) {
|
|
239
|
-
await assert.rejects(
|
|
240
|
-
middleware.execute(options as IMiddlewareSystemOptionsAggregate, next),
|
|
241
|
-
/rate-limit aggregate/,
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
assert.equal(downstream, false);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
});
|