@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.6 → 3.9.0-ultramodern.8
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/package.json +4 -4
- package/tests/buildIdentity.test.ts +0 -19
- package/tests/contextExtensions.test.ts +0 -44
- package/tests/headRuntime.test.tsx +0 -140
- package/tests/rendererHead.test.ts +0 -41
- package/tests/request-policy/browser-policy.test.ts +0 -500
- package/tests/request-policy/client-isolation.test.ts +0 -40
- package/tests/request-policy/node-policy.test.ts +0 -294
- package/tests/request-policy/policyCore.test.ts +0 -121
- package/tests/request-policy/requestContext.test.ts +8 -22
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +109 -510
- package/tests/request-policy/traceparent.test.ts +2 -5
- package/tests/request-policy/transport-retry.test.ts +44 -47
- package/tests/routerState.test.ts +0 -79
- package/tests/routerStatePlugin.test.ts +1 -46
- package/tests/runtimeContextProjection.test.ts +0 -52
- package/tests/safeFailure.test.ts +10 -28
- package/tests/workspaceEvents.test.ts +31 -201
- package/tests/nativeSafeFailureCompatibility.test.ts +0 -117
- package/tests/prefetchPolicy.test.ts +0 -51
- package/tests/request-policy/identityBinding.test.ts +0 -75
|
@@ -5,9 +5,6 @@ import {
|
|
|
5
5
|
BFF_ENVELOPE_HEADER,
|
|
6
6
|
BFF_OPERATION_CONTEXT_DETAIL_HEADER,
|
|
7
7
|
BFF_OPERATION_CONTEXT_HEADER,
|
|
8
|
-
type IdentityBindingOptions,
|
|
9
|
-
type OperationContext,
|
|
10
|
-
type OperationContractOptions,
|
|
11
8
|
type TransportTarget,
|
|
12
9
|
} from '../../src/request-policy/types';
|
|
13
10
|
|
|
@@ -23,37 +20,6 @@ const OPERATION_TRACEPARENT =
|
|
|
23
20
|
'00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01';
|
|
24
21
|
|
|
25
22
|
type HeaderMap = Record<string, any>;
|
|
26
|
-
type Method = 'GET' | 'POST';
|
|
27
|
-
|
|
28
|
-
type OutboundScenario = {
|
|
29
|
-
name: string;
|
|
30
|
-
target: TransportTarget;
|
|
31
|
-
method: Method;
|
|
32
|
-
requestId?: string;
|
|
33
|
-
domain?: string;
|
|
34
|
-
incomingHeaders?: HeaderMap;
|
|
35
|
-
payload: {
|
|
36
|
-
body?: string;
|
|
37
|
-
headers?: HeaderMap;
|
|
38
|
-
};
|
|
39
|
-
identityBinding?: IdentityBindingOptions;
|
|
40
|
-
requireEnvelope?: boolean;
|
|
41
|
-
allowCrossOriginEnvelope?: boolean;
|
|
42
|
-
operationContract?: OperationContractOptions;
|
|
43
|
-
operationContext?: OperationContext;
|
|
44
|
-
expectedUrl: string;
|
|
45
|
-
expectedBody?: string;
|
|
46
|
-
expectedStaticHeaders: HeaderMap;
|
|
47
|
-
assertDynamicHeaders?: (headers: HeaderMap) => void;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const restoreStrictDefaultRequestId = (value: string | undefined) => {
|
|
51
|
-
if (typeof value === 'undefined') {
|
|
52
|
-
delete process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID = value;
|
|
56
|
-
};
|
|
57
23
|
|
|
58
24
|
const createHarness = (
|
|
59
25
|
target: TransportTarget,
|
|
@@ -68,254 +34,118 @@ const createHarness = (
|
|
|
68
34
|
const requestFactory =
|
|
69
35
|
target === 'server' ? createServerClient() : createBrowserClient();
|
|
70
36
|
|
|
71
|
-
return {
|
|
72
|
-
request,
|
|
73
|
-
requestFactory,
|
|
74
|
-
};
|
|
37
|
+
return { request, requestFactory };
|
|
75
38
|
};
|
|
76
39
|
|
|
77
|
-
|
|
78
|
-
|
|
40
|
+
describe('requestFactory outbound request contract', () => {
|
|
41
|
+
test('browser GET drops the caller body, canonicalizes accept and withholds the incoming traceparent', async () => {
|
|
42
|
+
const { request, requestFactory } = createHarness('browser', {
|
|
43
|
+
traceparent: INCOMING_TRACEPARENT,
|
|
44
|
+
});
|
|
45
|
+
requestFactory.configure({ request: request as unknown as typeof fetch });
|
|
79
46
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
47
|
+
const send = requestFactory.createRequest({
|
|
48
|
+
path: REQUEST_PATH,
|
|
49
|
+
method: 'GET',
|
|
50
|
+
port: PORT,
|
|
51
|
+
});
|
|
52
|
+
await send({
|
|
53
|
+
body: 'drop-me',
|
|
54
|
+
headers: { Accept: 'application/problem+json' },
|
|
55
|
+
});
|
|
88
56
|
|
|
89
|
-
|
|
90
|
-
|
|
57
|
+
const [url, init] = request.mock.calls[0];
|
|
58
|
+
expect(String(url)).toBe(REQUEST_PATH);
|
|
59
|
+
expect(init.method).toBe('GET');
|
|
60
|
+
expect(init.body).toBeUndefined();
|
|
61
|
+
const headers = init.headers as HeaderMap;
|
|
62
|
+
expect(headers.accept).toBe(ACCEPT_HEADER);
|
|
63
|
+
expect(
|
|
64
|
+
Object.keys(headers).filter(key => key.toLowerCase() === 'accept'),
|
|
65
|
+
).toEqual(['accept']);
|
|
66
|
+
expect(headers.traceparent).toBeUndefined();
|
|
67
|
+
});
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
name: 'browser GET strips caller body and canonicalizes accept',
|
|
95
|
-
target: 'browser',
|
|
96
|
-
method: 'GET',
|
|
97
|
-
payload: {
|
|
98
|
-
body: 'drop-me',
|
|
99
|
-
headers: {
|
|
100
|
-
Accept: 'application/problem+json',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
expectedUrl: REQUEST_PATH,
|
|
104
|
-
expectedStaticHeaders: {
|
|
105
|
-
'Content-Type': 'text/plain',
|
|
106
|
-
accept: ACCEPT_HEADER,
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
name: 'browser POST keeps caller body without propagating incoming traceparent',
|
|
111
|
-
target: 'browser',
|
|
112
|
-
method: 'POST',
|
|
113
|
-
incomingHeaders: {
|
|
114
|
-
traceparent: INCOMING_TRACEPARENT,
|
|
115
|
-
},
|
|
116
|
-
payload: {
|
|
117
|
-
body: 'keep-me',
|
|
118
|
-
headers: {
|
|
119
|
-
Accept: 'application/problem+json',
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
expectedUrl: REQUEST_PATH,
|
|
123
|
-
expectedBody: 'keep-me',
|
|
124
|
-
expectedStaticHeaders: {
|
|
125
|
-
'Content-Type': 'text/plain',
|
|
126
|
-
accept: ACCEPT_HEADER,
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
name: 'server GET strips caller body and propagates incoming traceparent',
|
|
131
|
-
target: 'server',
|
|
132
|
-
method: 'GET',
|
|
133
|
-
incomingHeaders: {
|
|
134
|
-
traceparent: INCOMING_TRACEPARENT,
|
|
135
|
-
},
|
|
136
|
-
payload: {
|
|
137
|
-
body: 'drop-me',
|
|
138
|
-
headers: {
|
|
139
|
-
Accept: 'application/problem+json',
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
expectedUrl: SERVER_URL,
|
|
143
|
-
expectedStaticHeaders: {
|
|
144
|
-
'Content-Type': 'text/plain',
|
|
145
|
-
accept: ACCEPT_HEADER,
|
|
146
|
-
traceparent: INCOMING_TRACEPARENT,
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: 'server POST keeps caller body and propagates incoming traceparent',
|
|
151
|
-
target: 'server',
|
|
152
|
-
method: 'POST',
|
|
153
|
-
incomingHeaders: {
|
|
154
|
-
traceparent: INCOMING_TRACEPARENT,
|
|
155
|
-
},
|
|
156
|
-
payload: {
|
|
157
|
-
body: 'keep-me',
|
|
158
|
-
headers: {
|
|
159
|
-
Accept: 'application/problem+json',
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
expectedUrl: SERVER_URL,
|
|
163
|
-
expectedBody: 'keep-me',
|
|
164
|
-
expectedStaticHeaders: {
|
|
165
|
-
'Content-Type': 'text/plain',
|
|
166
|
-
accept: ACCEPT_HEADER,
|
|
69
|
+
test('server POST keeps the caller body and propagates the incoming traceparent', async () => {
|
|
70
|
+
const { request, requestFactory } = createHarness('server', {
|
|
167
71
|
traceparent: INCOMING_TRACEPARENT,
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
},
|
|
189
|
-
expectedUrl: SERVER_URL,
|
|
190
|
-
expectedBody: 'keep-me',
|
|
191
|
-
expectedStaticHeaders: {
|
|
192
|
-
'Content-Type': 'text/plain',
|
|
193
|
-
accept: ACCEPT_HEADER,
|
|
194
|
-
'x-tenant-id': 'bound-tenant',
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
name: 'configured secured request emits envelope and operation context',
|
|
199
|
-
target: 'server',
|
|
200
|
-
method: 'POST',
|
|
201
|
-
requestId: 'producer-checkout',
|
|
202
|
-
domain: 'https://producer.example',
|
|
203
|
-
incomingHeaders: {
|
|
72
|
+
});
|
|
73
|
+
requestFactory.configure({ request: request as unknown as typeof fetch });
|
|
74
|
+
|
|
75
|
+
const send = requestFactory.createRequest({
|
|
76
|
+
path: REQUEST_PATH,
|
|
77
|
+
method: 'POST',
|
|
78
|
+
port: PORT,
|
|
79
|
+
});
|
|
80
|
+
await send({ body: 'keep-me' });
|
|
81
|
+
|
|
82
|
+
const [url, init] = request.mock.calls[0];
|
|
83
|
+
expect(String(url)).toBe(SERVER_URL);
|
|
84
|
+
expect(init.method).toBe('POST');
|
|
85
|
+
expect(init.body).toBe('keep-me');
|
|
86
|
+
expect((init.headers as HeaderMap).traceparent).toBe(INCOMING_TRACEPARENT);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('secured producer request emits the cross-origin envelope and operation context', async () => {
|
|
90
|
+
const requestId = 'producer-checkout';
|
|
91
|
+
const { request, requestFactory } = createHarness('server', {
|
|
204
92
|
origin: CONSUMER_ORIGIN,
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
requestFactory.configure({
|
|
96
|
+
request: request as unknown as typeof fetch,
|
|
97
|
+
requestId,
|
|
98
|
+
setDomain: () => 'https://producer.example',
|
|
99
|
+
requireEnvelope: true,
|
|
100
|
+
allowCrossOriginEnvelope: true,
|
|
101
|
+
operationContract: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
requireSchemaHash: true,
|
|
104
|
+
requireOperationVersion: true,
|
|
210
105
|
},
|
|
211
|
-
}
|
|
212
|
-
requireEnvelope: true,
|
|
213
|
-
allowCrossOriginEnvelope: true,
|
|
214
|
-
operationContract: {
|
|
215
|
-
enabled: true,
|
|
216
|
-
requireSchemaHash: true,
|
|
217
|
-
requireOperationVersion: true,
|
|
218
|
-
},
|
|
219
|
-
operationContext: {
|
|
220
|
-
operationId: 'create-widget',
|
|
221
|
-
routePath: REQUEST_PATH,
|
|
222
|
-
method: 'POST',
|
|
223
|
-
schemaHash: 'sha256:create-widget',
|
|
224
|
-
operationVersion: 3,
|
|
225
|
-
traceparent: OPERATION_TRACEPARENT,
|
|
226
|
-
},
|
|
227
|
-
expectedUrl: PRODUCER_URL,
|
|
228
|
-
expectedBody: 'keep-me',
|
|
229
|
-
expectedStaticHeaders: {
|
|
230
|
-
'Content-Type': 'text/plain',
|
|
231
|
-
accept: ACCEPT_HEADER,
|
|
232
|
-
traceparent: OPERATION_TRACEPARENT,
|
|
233
|
-
[BFF_OPERATION_CONTEXT_HEADER]: 'producer-checkout:create-widget',
|
|
234
|
-
},
|
|
235
|
-
assertDynamicHeaders: headers => {
|
|
236
|
-
const envelope = JSON.parse(headers[BFF_ENVELOPE_HEADER]);
|
|
237
|
-
expect(envelope).toMatchObject({
|
|
238
|
-
requestId: 'producer-checkout',
|
|
239
|
-
target: 'server',
|
|
240
|
-
sourceOrigin: CONSUMER_ORIGIN,
|
|
241
|
-
targetOrigin: 'https://producer.example',
|
|
242
|
-
traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
243
|
-
spanId: 'bbbbbbbbbbbbbbbb',
|
|
244
|
-
});
|
|
245
|
-
expect(typeof envelope.timestamp).toBe('number');
|
|
106
|
+
});
|
|
246
107
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
108
|
+
const send = requestFactory.createRequest({
|
|
109
|
+
path: REQUEST_PATH,
|
|
110
|
+
method: 'POST',
|
|
111
|
+
port: PORT,
|
|
112
|
+
requestId,
|
|
113
|
+
operationContext: {
|
|
114
|
+
operationId: 'create-widget',
|
|
253
115
|
routePath: REQUEST_PATH,
|
|
254
116
|
method: 'POST',
|
|
255
117
|
schemaHash: 'sha256:create-widget',
|
|
256
118
|
operationVersion: 3,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
];
|
|
263
|
-
|
|
264
|
-
describe('requestFactory outbound request contract', () => {
|
|
265
|
-
for (const scenario of outboundScenarios) {
|
|
266
|
-
test(scenario.name, async () => {
|
|
267
|
-
const strictDefaultRequestId =
|
|
268
|
-
process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
|
|
269
|
-
delete process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const { request, requestFactory } = createHarness(
|
|
273
|
-
scenario.target,
|
|
274
|
-
scenario.incomingHeaders,
|
|
275
|
-
);
|
|
276
|
-
const requestId = scenario.requestId || 'default';
|
|
277
|
-
|
|
278
|
-
requestFactory.configure({
|
|
279
|
-
request: request as unknown as typeof fetch,
|
|
280
|
-
requestId,
|
|
281
|
-
...(requestId !== 'default'
|
|
282
|
-
? { setDomain: () => scenario.domain || '' }
|
|
283
|
-
: {}),
|
|
284
|
-
...(scenario.identityBinding
|
|
285
|
-
? { identityBinding: scenario.identityBinding }
|
|
286
|
-
: {}),
|
|
287
|
-
...(typeof scenario.requireEnvelope === 'boolean'
|
|
288
|
-
? { requireEnvelope: scenario.requireEnvelope }
|
|
289
|
-
: {}),
|
|
290
|
-
...(typeof scenario.allowCrossOriginEnvelope === 'boolean'
|
|
291
|
-
? { allowCrossOriginEnvelope: scenario.allowCrossOriginEnvelope }
|
|
292
|
-
: {}),
|
|
293
|
-
...(scenario.operationContract
|
|
294
|
-
? { operationContract: scenario.operationContract }
|
|
295
|
-
: {}),
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const send = requestFactory.createRequest({
|
|
299
|
-
path: REQUEST_PATH,
|
|
300
|
-
method: scenario.method,
|
|
301
|
-
port: PORT,
|
|
302
|
-
requestId,
|
|
303
|
-
operationContext: scenario.operationContext,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
await send(scenario.payload);
|
|
119
|
+
traceparent: OPERATION_TRACEPARENT,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
await send({ body: 'keep-me' });
|
|
307
123
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
124
|
+
const [url, init] = request.mock.calls[0];
|
|
125
|
+
expect(String(url)).toBe(PRODUCER_URL);
|
|
126
|
+
const headers = init.headers as HeaderMap;
|
|
127
|
+
expect(headers.traceparent).toBe(OPERATION_TRACEPARENT);
|
|
128
|
+
expect(headers[BFF_OPERATION_CONTEXT_HEADER]).toBe(
|
|
129
|
+
'producer-checkout:create-widget',
|
|
130
|
+
);
|
|
131
|
+
expect(JSON.parse(headers[BFF_ENVELOPE_HEADER])).toMatchObject({
|
|
132
|
+
requestId,
|
|
133
|
+
target: 'server',
|
|
134
|
+
sourceOrigin: CONSUMER_ORIGIN,
|
|
135
|
+
targetOrigin: 'https://producer.example',
|
|
136
|
+
traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
137
|
+
spanId: 'bbbbbbbbbbbbbbbb',
|
|
315
138
|
});
|
|
316
|
-
|
|
139
|
+
expect(
|
|
140
|
+
JSON.parse(headers[BFF_OPERATION_CONTEXT_DETAIL_HEADER]),
|
|
141
|
+
).toMatchObject({
|
|
142
|
+
operationId: 'producer-checkout:create-widget',
|
|
143
|
+
schemaHash: 'sha256:create-widget',
|
|
144
|
+
operationVersion: 3,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
317
147
|
|
|
318
|
-
test('
|
|
148
|
+
test('forwards only allowlisted and server-derived identity headers', async () => {
|
|
319
149
|
const requestId = 'producer-input-params-identity';
|
|
320
150
|
const { request, requestFactory } = createHarness('server', {
|
|
321
151
|
'x-tenant-id': 'tenant-server',
|
|
@@ -340,10 +170,8 @@ describe('requestFactory outbound request contract', () => {
|
|
|
340
170
|
httpMethodDecider: 'inputParams',
|
|
341
171
|
requestId,
|
|
342
172
|
});
|
|
343
|
-
|
|
344
173
|
await send('widget');
|
|
345
174
|
|
|
346
|
-
expect(request).toHaveBeenCalledTimes(1);
|
|
347
175
|
const headers = request.mock.calls[0][1]?.headers as HeaderMap;
|
|
348
176
|
expect(headers).toMatchObject({
|
|
349
177
|
'x-tenant-id': 'tenant-server',
|
|
@@ -353,94 +181,44 @@ describe('requestFactory outbound request contract', () => {
|
|
|
353
181
|
expect(headers['x-private-incoming']).toBeUndefined();
|
|
354
182
|
});
|
|
355
183
|
|
|
356
|
-
test('uploader
|
|
184
|
+
test('uploader sends the file as form data under the same header policy', async () => {
|
|
357
185
|
const requestId = 'producer-uploader-identity';
|
|
358
186
|
const { request, requestFactory } = createHarness('server', {
|
|
359
187
|
origin: CONSUMER_ORIGIN,
|
|
360
188
|
'x-tenant-id': 'tenant-server',
|
|
361
|
-
'x-subject-id': 'subject-server',
|
|
362
|
-
'x-forwarded-feature': 'feature-server',
|
|
363
189
|
'x-private-incoming': 'must-not-forward',
|
|
364
190
|
});
|
|
365
191
|
|
|
366
192
|
requestFactory.configure({
|
|
367
193
|
request: request as unknown as typeof fetch,
|
|
368
194
|
requestId,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
enabled: true,
|
|
372
|
-
requireSchemaHash: true,
|
|
373
|
-
requireOperationVersion: true,
|
|
374
|
-
},
|
|
375
|
-
requireEnvelope: true,
|
|
376
|
-
allowCrossOriginEnvelope: true,
|
|
195
|
+
operationContract: { enabled: false },
|
|
196
|
+
requireEnvelope: false,
|
|
377
197
|
setDomain: () => 'https://producer.example',
|
|
378
198
|
});
|
|
379
199
|
|
|
380
200
|
const upload = requestFactory.createUploader({
|
|
381
201
|
path: REQUEST_PATH,
|
|
382
202
|
requestId,
|
|
383
|
-
operationContext: {
|
|
384
|
-
operationId: 'upload-widget',
|
|
385
|
-
routePath: REQUEST_PATH,
|
|
386
|
-
method: 'POST',
|
|
387
|
-
schemaHash: 'sha256:upload-widget',
|
|
388
|
-
operationVersion: 1,
|
|
389
|
-
traceparent: OPERATION_TRACEPARENT,
|
|
390
|
-
},
|
|
391
203
|
});
|
|
392
|
-
|
|
393
204
|
await upload({
|
|
394
205
|
files: {
|
|
395
206
|
file: new File(['widget'], 'widget.txt', { type: 'text/plain' }),
|
|
396
207
|
},
|
|
397
208
|
});
|
|
398
209
|
|
|
399
|
-
expect(request).toHaveBeenCalledTimes(1);
|
|
400
210
|
const [, init] = request.mock.calls[0];
|
|
401
211
|
expect(init.method).toBe('POST');
|
|
402
212
|
expect(init.body).toBeInstanceOf(FormData);
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
expect(uploaded).
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
type: 'text/plain',
|
|
409
|
-
});
|
|
410
|
-
expect(await (uploaded as File).text()).toBe('widget');
|
|
411
|
-
const headers = request.mock.calls[0][1]?.headers as HeaderMap;
|
|
412
|
-
expect(headers).toMatchObject({
|
|
413
|
-
'x-tenant-id': 'tenant-server',
|
|
414
|
-
'x-subject-id': 'subject-server',
|
|
415
|
-
'x-forwarded-feature': 'feature-server',
|
|
416
|
-
});
|
|
213
|
+
const uploaded = (init.body as FormData).get('file') as File;
|
|
214
|
+
expect(uploaded.name).toBe('widget.txt');
|
|
215
|
+
expect(await uploaded.text()).toBe('widget');
|
|
216
|
+
const headers = init.headers as HeaderMap;
|
|
217
|
+
expect(headers['x-tenant-id']).toBe('tenant-server');
|
|
417
218
|
expect(headers['x-private-incoming']).toBeUndefined();
|
|
418
|
-
const envelope = JSON.parse(headers[BFF_ENVELOPE_HEADER]);
|
|
419
|
-
expect(envelope).toMatchObject({
|
|
420
|
-
requestId,
|
|
421
|
-
target: 'server',
|
|
422
|
-
sourceOrigin: CONSUMER_ORIGIN,
|
|
423
|
-
targetOrigin: 'https://producer.example',
|
|
424
|
-
});
|
|
425
|
-
expect(headers[BFF_OPERATION_CONTEXT_HEADER]).toBe(
|
|
426
|
-
`${requestId}:upload-widget`,
|
|
427
|
-
);
|
|
428
|
-
const operationContext = JSON.parse(
|
|
429
|
-
headers[BFF_OPERATION_CONTEXT_DETAIL_HEADER],
|
|
430
|
-
);
|
|
431
|
-
expect(operationContext).toMatchObject({
|
|
432
|
-
requestId,
|
|
433
|
-
operationId: `${requestId}:upload-widget`,
|
|
434
|
-
routePath: REQUEST_PATH,
|
|
435
|
-
method: 'POST',
|
|
436
|
-
schemaHash: 'sha256:upload-widget',
|
|
437
|
-
operationVersion: 1,
|
|
438
|
-
traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
439
|
-
spanId: 'bbbbbbbbbbbbbbbb',
|
|
440
|
-
});
|
|
441
219
|
});
|
|
442
220
|
|
|
443
|
-
test('
|
|
221
|
+
test('resolved headers replace caller and incoming values without injecting unlisted ones', async () => {
|
|
444
222
|
const resolveHeaders = rs.fn(() => ({
|
|
445
223
|
AUTHORIZATION: 'Bearer resolved',
|
|
446
224
|
'x-injected': 'must-not-forward',
|
|
@@ -460,9 +238,7 @@ describe('requestFactory outbound request contract', () => {
|
|
|
460
238
|
method: 'GET',
|
|
461
239
|
port: PORT,
|
|
462
240
|
});
|
|
463
|
-
await send({
|
|
464
|
-
headers: { Authorization: 'Bearer caller' },
|
|
465
|
-
});
|
|
241
|
+
await send({ headers: { Authorization: 'Bearer caller' } });
|
|
466
242
|
|
|
467
243
|
expect(resolveHeaders).toHaveBeenCalledWith(
|
|
468
244
|
expect.objectContaining({
|
|
@@ -470,19 +246,15 @@ describe('requestFactory outbound request contract', () => {
|
|
|
470
246
|
}),
|
|
471
247
|
);
|
|
472
248
|
const headers = request.mock.calls[0][1]?.headers as HeaderMap;
|
|
473
|
-
expect(headers).
|
|
474
|
-
authorization: 'Bearer resolved',
|
|
475
|
-
});
|
|
249
|
+
expect(headers.authorization).toBe('Bearer resolved');
|
|
476
250
|
expect(headers['x-injected']).toBeUndefined();
|
|
477
251
|
expect(
|
|
478
252
|
Object.keys(headers).filter(key => key.toLowerCase() === 'authorization'),
|
|
479
253
|
).toEqual(['authorization']);
|
|
480
254
|
});
|
|
481
255
|
|
|
482
|
-
test('reconfiguration
|
|
483
|
-
const resolveHeaders = rs.fn(() => ({
|
|
484
|
-
authorization: 'Bearer stale',
|
|
485
|
-
}));
|
|
256
|
+
test('reconfiguration drops an omitted header and envelope policy', async () => {
|
|
257
|
+
const resolveHeaders = rs.fn(() => ({ authorization: 'Bearer stale' }));
|
|
486
258
|
const { request, requestFactory } = createHarness('server', {
|
|
487
259
|
authorization: 'Bearer incoming',
|
|
488
260
|
});
|
|
@@ -515,177 +287,4 @@ describe('requestFactory outbound request contract', () => {
|
|
|
515
287
|
accept: ACCEPT_HEADER,
|
|
516
288
|
});
|
|
517
289
|
});
|
|
518
|
-
|
|
519
|
-
test('reconfiguration removes omitted retry policy', async () => {
|
|
520
|
-
const failure = Object.assign(new Error('unavailable'), { status: 503 });
|
|
521
|
-
const request = rs.fn(async () => {
|
|
522
|
-
throw failure;
|
|
523
|
-
});
|
|
524
|
-
const { requestFactory } = createHarness('server');
|
|
525
|
-
|
|
526
|
-
requestFactory.configure({
|
|
527
|
-
request: request as unknown as typeof fetch,
|
|
528
|
-
transport: {
|
|
529
|
-
retry: {
|
|
530
|
-
retries: 1,
|
|
531
|
-
baseDelayMs: 1,
|
|
532
|
-
maxDelayMs: 1,
|
|
533
|
-
jitterRatio: 0,
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
});
|
|
537
|
-
requestFactory.configure({
|
|
538
|
-
request: request as unknown as typeof fetch,
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
const send = requestFactory.createRequest({
|
|
542
|
-
path: REQUEST_PATH,
|
|
543
|
-
method: 'GET',
|
|
544
|
-
port: PORT,
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
await expect(send()).rejects.toBe(failure);
|
|
548
|
-
expect(request).toHaveBeenCalledTimes(1);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test('reconfiguration restores omitted secured-producer defaults', () => {
|
|
552
|
-
const requestId = 'producer-policy-reset';
|
|
553
|
-
const { request, requestFactory } = createHarness('server');
|
|
554
|
-
|
|
555
|
-
requestFactory.configure({
|
|
556
|
-
request: request as unknown as typeof fetch,
|
|
557
|
-
requestId,
|
|
558
|
-
requireEnvelope: false,
|
|
559
|
-
identityBinding: { enabled: false },
|
|
560
|
-
operationContract: { enabled: false },
|
|
561
|
-
setDomain: () => 'https://producer.example',
|
|
562
|
-
});
|
|
563
|
-
requestFactory.configure({
|
|
564
|
-
request: request as unknown as typeof fetch,
|
|
565
|
-
requestId,
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const send = requestFactory.createRequest({
|
|
569
|
-
path: REQUEST_PATH,
|
|
570
|
-
method: 'GET',
|
|
571
|
-
port: PORT,
|
|
572
|
-
requestId,
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
expect(() => send()).toThrow('missing_schema_hash');
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
test('uploader uses an explicitly configured POST retry policy', async () => {
|
|
579
|
-
rs.useFakeTimers();
|
|
580
|
-
const retryableError = Object.assign(new Error('upload unavailable'), {
|
|
581
|
-
status: 503,
|
|
582
|
-
});
|
|
583
|
-
const response = new Response('{}', { status: 200 });
|
|
584
|
-
const onDegraded = rs.fn();
|
|
585
|
-
let attempts = 0;
|
|
586
|
-
const request = rs.fn(async () => {
|
|
587
|
-
attempts += 1;
|
|
588
|
-
if (attempts === 1) {
|
|
589
|
-
throw retryableError;
|
|
590
|
-
}
|
|
591
|
-
return response;
|
|
592
|
-
});
|
|
593
|
-
const { requestFactory } = createHarness('server');
|
|
594
|
-
|
|
595
|
-
requestFactory.configure({
|
|
596
|
-
request: request as unknown as typeof fetch,
|
|
597
|
-
transport: {
|
|
598
|
-
retry: {
|
|
599
|
-
retries: 1,
|
|
600
|
-
baseDelayMs: 5,
|
|
601
|
-
maxDelayMs: 5,
|
|
602
|
-
jitterRatio: 0,
|
|
603
|
-
shouldRetry: ({ method }) => method === 'POST',
|
|
604
|
-
},
|
|
605
|
-
onDegraded,
|
|
606
|
-
},
|
|
607
|
-
});
|
|
608
|
-
const upload = requestFactory.createUploader({ path: REQUEST_PATH });
|
|
609
|
-
const pending = upload({
|
|
610
|
-
files: { file: new File(['widget'], 'widget.txt') },
|
|
611
|
-
});
|
|
612
|
-
const observed = pending.catch(error => error);
|
|
613
|
-
|
|
614
|
-
try {
|
|
615
|
-
await Promise.resolve();
|
|
616
|
-
expect(request).toHaveBeenCalledTimes(1);
|
|
617
|
-
await rs.advanceTimersByTimeAsync(5);
|
|
618
|
-
|
|
619
|
-
await expect(pending).resolves.toBe(response);
|
|
620
|
-
expect(request).toHaveBeenCalledTimes(2);
|
|
621
|
-
expect(onDegraded).toHaveBeenCalledWith(
|
|
622
|
-
expect.objectContaining({
|
|
623
|
-
reason: 'retry',
|
|
624
|
-
method: 'POST',
|
|
625
|
-
attempt: 1,
|
|
626
|
-
maxAttempts: 2,
|
|
627
|
-
}),
|
|
628
|
-
);
|
|
629
|
-
} finally {
|
|
630
|
-
await rs.advanceTimersByTimeAsync(1000);
|
|
631
|
-
await observed;
|
|
632
|
-
rs.useRealTimers();
|
|
633
|
-
}
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
test('uploader timeout aborts transport and emits degraded telemetry', async () => {
|
|
637
|
-
rs.useFakeTimers();
|
|
638
|
-
const onDegraded = rs.fn();
|
|
639
|
-
const cleanupError = new Error('test cleanup');
|
|
640
|
-
let rejectRequest: ((error: Error) => void) | undefined;
|
|
641
|
-
let inFlightSignal: AbortSignal | undefined;
|
|
642
|
-
const request = rs.fn((_url: RequestInfo | URL, init?: RequestInit) => {
|
|
643
|
-
inFlightSignal = init?.signal || undefined;
|
|
644
|
-
return new Promise((_, reject) => {
|
|
645
|
-
rejectRequest = reject;
|
|
646
|
-
init?.signal?.addEventListener('abort', () => {
|
|
647
|
-
const error = new Error('aborted');
|
|
648
|
-
error.name = 'AbortError';
|
|
649
|
-
reject(error);
|
|
650
|
-
});
|
|
651
|
-
});
|
|
652
|
-
});
|
|
653
|
-
const { requestFactory } = createHarness('server');
|
|
654
|
-
|
|
655
|
-
requestFactory.configure({
|
|
656
|
-
request: request as unknown as typeof fetch,
|
|
657
|
-
transport: {
|
|
658
|
-
timeoutMs: 20,
|
|
659
|
-
onDegraded,
|
|
660
|
-
},
|
|
661
|
-
});
|
|
662
|
-
const upload = requestFactory.createUploader({ path: REQUEST_PATH });
|
|
663
|
-
const pending = upload({
|
|
664
|
-
files: { file: new File(['widget'], 'widget.txt') },
|
|
665
|
-
});
|
|
666
|
-
const observed = pending.catch(error => error);
|
|
667
|
-
|
|
668
|
-
try {
|
|
669
|
-
await Promise.resolve();
|
|
670
|
-
expect(inFlightSignal).toBeDefined();
|
|
671
|
-
await rs.advanceTimersByTimeAsync(20);
|
|
672
|
-
|
|
673
|
-
await expect(observed).resolves.toMatchObject({ name: 'TimeoutError' });
|
|
674
|
-
expect(inFlightSignal?.aborted).toBe(true);
|
|
675
|
-
expect(onDegraded).toHaveBeenCalledWith(
|
|
676
|
-
expect.objectContaining({
|
|
677
|
-
reason: 'timeout',
|
|
678
|
-
method: 'POST',
|
|
679
|
-
timeoutMs: 20,
|
|
680
|
-
}),
|
|
681
|
-
);
|
|
682
|
-
} finally {
|
|
683
|
-
if (!inFlightSignal?.aborted) {
|
|
684
|
-
rejectRequest?.(cleanupError);
|
|
685
|
-
}
|
|
686
|
-
await observed;
|
|
687
|
-
await rs.advanceTimersByTimeAsync(1000);
|
|
688
|
-
rs.useRealTimers();
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
290
|
});
|