@fluojs/testing 1.0.6 → 3.0.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.ko.md +98 -16
- package/README.md +100 -16
- package/dist/babel-decorators-plugin.d.ts +1 -0
- package/dist/babel-decorators-plugin.d.ts.map +1 -1
- package/dist/babel-decorators-plugin.js +1 -0
- package/dist/byte-range-portability-consumer.test-fixture.d.ts +2 -0
- package/dist/byte-range-portability-consumer.test-fixture.d.ts.map +1 -0
- package/dist/byte-range-portability-consumer.test-fixture.js +11 -0
- package/dist/conformance/fetch-style-websocket-conformance.d.ts +1 -0
- package/dist/conformance/fetch-style-websocket-conformance.d.ts.map +1 -1
- package/dist/conformance/fetch-style-websocket-conformance.js +1 -1
- package/dist/conformance/platform-shell-lifecycle-conformance.d.ts +30 -0
- package/dist/conformance/platform-shell-lifecycle-conformance.d.ts.map +1 -0
- package/dist/conformance/platform-shell-lifecycle-conformance.js +259 -0
- package/dist/http.d.ts +2 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/mock-types.d.ts +2 -0
- package/dist/mock-types.d.ts.map +1 -0
- package/dist/mock-types.js +1 -0
- package/dist/mock.d.ts +3 -2
- package/dist/mock.d.ts.map +1 -1
- package/dist/mock.js +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +103 -113
- package/dist/portability/error-representation-abort-portability.d.ts +36 -0
- package/dist/portability/error-representation-abort-portability.d.ts.map +1 -0
- package/dist/portability/error-representation-abort-portability.js +190 -0
- package/dist/portability/error-representation-portability-fixture.d.ts +58 -0
- package/dist/portability/error-representation-portability-fixture.d.ts.map +1 -0
- package/dist/portability/error-representation-portability-fixture.js +202 -0
- package/dist/portability/error-representation-portability.d.ts +50 -0
- package/dist/portability/error-representation-portability.d.ts.map +1 -0
- package/dist/portability/error-representation-portability.js +153 -0
- package/dist/portability/http-adapter-portability.d.ts +25 -8
- package/dist/portability/http-adapter-portability.d.ts.map +1 -1
- package/dist/portability/http-adapter-portability.js +446 -69
- package/dist/portability/response-cookie-portability.d.ts +16 -0
- package/dist/portability/response-cookie-portability.d.ts.map +1 -0
- package/dist/portability/response-cookie-portability.js +81 -0
- package/dist/portability/web-runtime-adapter-portability.d.ts +24 -8
- package/dist/portability/web-runtime-adapter-portability.d.ts.map +1 -1
- package/dist/portability/web-runtime-adapter-portability.js +356 -31
- package/dist/types.d.ts +68 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -15
package/dist/module.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getModuleMetadata } from '@fluojs/core';
|
|
2
2
|
import { isForwardRef, isOptionalToken, Scope } from '@fluojs/di';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveMultiContribution } from '@fluojs/di/internal';
|
|
4
4
|
import { createDispatcher, createHandlerMapping } from '@fluojs/http';
|
|
5
|
+
import { bootstrapModule } from '@fluojs/runtime';
|
|
5
6
|
import { createTestRequestContextMiddleware, makeRequest } from './http.js';
|
|
6
7
|
/**
|
|
7
8
|
* Returns providers declared on a module metadata definition.
|
|
@@ -45,9 +46,10 @@ export function extractModuleImports(moduleType) {
|
|
|
45
46
|
return metadata.imports;
|
|
46
47
|
}
|
|
47
48
|
function createHandlerSources(bootstrappedModules) {
|
|
49
|
+
const globalModules = bootstrappedModules.filter(compiledModule => compiledModule.definition.global);
|
|
48
50
|
return bootstrappedModules.flatMap(compiledModule => (compiledModule.definition.controllers ?? []).map(controllerToken => ({
|
|
49
51
|
controllerToken,
|
|
50
|
-
moduleMiddleware: compiledModule.definition.middleware ?? [],
|
|
52
|
+
moduleMiddleware: [...globalModules.filter(globalModule => globalModule !== compiledModule).flatMap(globalModule => globalModule.definition.middleware ?? []), ...(compiledModule.definition.middleware ?? [])],
|
|
51
53
|
moduleType: compiledModule.type
|
|
52
54
|
})));
|
|
53
55
|
}
|
|
@@ -117,33 +119,7 @@ function isSingletonLifecycleProvider(provider) {
|
|
|
117
119
|
if (provider.scope !== Scope.DEFAULT) {
|
|
118
120
|
return false;
|
|
119
121
|
}
|
|
120
|
-
return provider.type === 'class' || provider.type === 'value' && hasAnyBootstrapLifecycleHook(provider.useValue);
|
|
121
|
-
}
|
|
122
|
-
async function resolveLifecycleDependency(entry, bootstrapped, introspection) {
|
|
123
|
-
if (isOptionalToken(entry) && !hasTokenInContainer(introspection, entry.token)) {
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
return bootstrapped.container.resolve(dependencyToken(entry));
|
|
127
|
-
}
|
|
128
|
-
async function instantiateLifecycleProvider(provider, bootstrapped, introspection) {
|
|
129
|
-
if (provider.type !== 'class' || !provider.useClass) {
|
|
130
|
-
throw new Error(`Lifecycle provider ${String(provider.provide)} must use a class provider.`);
|
|
131
|
-
}
|
|
132
|
-
const dependencies = await Promise.all(provider.inject.map(entry => resolveLifecycleDependency(entry, bootstrapped, introspection)));
|
|
133
|
-
return new provider.useClass(...dependencies);
|
|
134
|
-
}
|
|
135
|
-
async function resolveMultiLifecycleProvider(provider, bootstrapped, introspection) {
|
|
136
|
-
const cache = rootContainerIntrospection(introspection).multiSingletonCache;
|
|
137
|
-
const cached = cache.get(provider);
|
|
138
|
-
if (cached) {
|
|
139
|
-
return cached;
|
|
140
|
-
}
|
|
141
|
-
const instance = instantiateLifecycleProvider(provider, bootstrapped, introspection).catch(error => {
|
|
142
|
-
cache.delete(provider);
|
|
143
|
-
throw error;
|
|
144
|
-
});
|
|
145
|
-
cache.set(provider, instance);
|
|
146
|
-
return instance;
|
|
122
|
+
return provider.type === 'class' || provider.type === 'factory' || provider.type === 'value' && hasAnyBootstrapLifecycleHook(provider.useValue);
|
|
147
123
|
}
|
|
148
124
|
async function resolveTestingLifecycleInstances(bootstrapped, overrides = []) {
|
|
149
125
|
const lifecycleProviders = [...bootstrapped.effectiveProviders.runtimeProviders, ...bootstrapped.effectiveProviders.moduleProviders, ...overrides];
|
|
@@ -153,35 +129,27 @@ async function resolveTestingLifecycleInstances(bootstrapped, overrides = []) {
|
|
|
153
129
|
for (const provider of lifecycleProviders) {
|
|
154
130
|
const token = providerToken(provider);
|
|
155
131
|
const effectiveProviders = effectiveProvidersForToken(introspection, token);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
if (effectiveProvider.type === 'value') {
|
|
169
|
-
instances.push(effectiveProvider.useValue);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
try {
|
|
173
|
-
if (effectiveProvider.multi === true) {
|
|
174
|
-
instances.push(await resolveMultiLifecycleProvider(effectiveProvider, bootstrapped, introspection));
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
instances.push(await bootstrapped.container.resolve(token));
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (error instanceof Error && error.message.includes('Request-scoped provider')) {
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
132
|
+
const nextUnseenProviderIndex = effectiveProviders.findIndex(effectiveProvider => !seenProviders.has(effectiveProvider));
|
|
133
|
+
if (nextUnseenProviderIndex === -1) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const effectiveProvider = effectiveProviders[nextUnseenProviderIndex];
|
|
137
|
+
if (!effectiveProvider) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
seenProviders.add(effectiveProvider);
|
|
141
|
+
if (!isSingletonLifecycleProvider(effectiveProvider)) {
|
|
142
|
+
continue;
|
|
184
143
|
}
|
|
144
|
+
if (effectiveProvider.type === 'value') {
|
|
145
|
+
instances.push(effectiveProvider.useValue);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (effectiveProvider.multi === true) {
|
|
149
|
+
instances.push(await resolveMultiContribution(bootstrapped.container, token, nextUnseenProviderIndex));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
instances.push(await bootstrapped.container.resolve(token));
|
|
185
153
|
}
|
|
186
154
|
return instances;
|
|
187
155
|
}
|
|
@@ -228,30 +196,6 @@ function dependencyToken(entry) {
|
|
|
228
196
|
}
|
|
229
197
|
return entry;
|
|
230
198
|
}
|
|
231
|
-
function trackFactoryResolutionKind(provider, factoryResolutionKinds) {
|
|
232
|
-
if (provider.type !== 'factory' || !provider.useFactory) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const originalFactory = provider.useFactory;
|
|
236
|
-
provider.useFactory = (...deps) => {
|
|
237
|
-
const value = originalFactory(...deps);
|
|
238
|
-
factoryResolutionKinds.set(provider, isPromiseLike(value) ? 'async' : 'sync');
|
|
239
|
-
return value;
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
function installFactoryResolutionTracking(target, factoryResolutionKinds) {
|
|
243
|
-
if (target.parent) {
|
|
244
|
-
installFactoryResolutionTracking(target.parent, factoryResolutionKinds);
|
|
245
|
-
}
|
|
246
|
-
for (const provider of target.registrations.values()) {
|
|
247
|
-
trackFactoryResolutionKind(provider, factoryResolutionKinds);
|
|
248
|
-
}
|
|
249
|
-
for (const providers of target.multiRegistrations.values()) {
|
|
250
|
-
for (const provider of providers) {
|
|
251
|
-
trackFactoryResolutionKind(provider, factoryResolutionKinds);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
199
|
function providerGraphIsSyncResolvable(state, token, visited = new Set()) {
|
|
256
200
|
if (visited.has(token)) {
|
|
257
201
|
return true;
|
|
@@ -332,6 +276,7 @@ function instantiateSyncProvider(provider, state) {
|
|
|
332
276
|
if (isPromiseLike(value)) {
|
|
333
277
|
throw new Error(`Token ${String(provider.provide)} requires async resolution. Use resolve() instead of get() for async providers.`);
|
|
334
278
|
}
|
|
279
|
+
state.cacheOwner.recordFactoryResolution(provider, 'sync');
|
|
335
280
|
return value;
|
|
336
281
|
}
|
|
337
282
|
case 'class':
|
|
@@ -355,15 +300,20 @@ function resolveSyncProvider(provider, state) {
|
|
|
355
300
|
if (provider.scope === 'transient') {
|
|
356
301
|
return instantiateSyncProvider(provider, state);
|
|
357
302
|
}
|
|
358
|
-
|
|
359
|
-
|
|
303
|
+
const memo = state.syncSingletonValues.get(provider.provide);
|
|
304
|
+
if (memo) {
|
|
305
|
+
return memo.value;
|
|
360
306
|
}
|
|
361
307
|
if (state.singletonCache.has(provider.provide)) {
|
|
362
308
|
throw new Error(`Token ${String(provider.provide)} was already resolved asynchronously. Use resolve() instead of get() for this provider.`);
|
|
363
309
|
}
|
|
364
310
|
const instance = instantiateSyncProvider(provider, state);
|
|
365
|
-
|
|
366
|
-
state.
|
|
311
|
+
const cacheEntry = Promise.resolve(instance);
|
|
312
|
+
state.syncSingletonValues.set(provider.provide, {
|
|
313
|
+
cacheEntry,
|
|
314
|
+
value: instance
|
|
315
|
+
});
|
|
316
|
+
state.cacheOwner.setSingleton(provider.provide, cacheEntry);
|
|
367
317
|
return instance;
|
|
368
318
|
}
|
|
369
319
|
function resolveSyncMultiProvider(provider, state) {
|
|
@@ -373,15 +323,20 @@ function resolveSyncMultiProvider(provider, state) {
|
|
|
373
323
|
if (provider.scope === 'transient') {
|
|
374
324
|
return instantiateSyncProvider(provider, state);
|
|
375
325
|
}
|
|
376
|
-
|
|
377
|
-
|
|
326
|
+
const memo = state.syncMultiSingletonValues.get(provider);
|
|
327
|
+
if (memo) {
|
|
328
|
+
return memo.value;
|
|
378
329
|
}
|
|
379
330
|
if (state.multiSingletonCache.has(provider)) {
|
|
380
331
|
throw new Error(`Token ${String(provider.provide)} was already resolved asynchronously. Use resolve() instead of get() for this provider.`);
|
|
381
332
|
}
|
|
382
333
|
const instance = instantiateSyncProvider(provider, state);
|
|
383
|
-
|
|
384
|
-
state.
|
|
334
|
+
const cacheEntry = Promise.resolve(instance);
|
|
335
|
+
state.syncMultiSingletonValues.set(provider, {
|
|
336
|
+
cacheEntry,
|
|
337
|
+
value: instance
|
|
338
|
+
});
|
|
339
|
+
state.cacheOwner.setMultiSingleton(provider, cacheEntry);
|
|
385
340
|
return instance;
|
|
386
341
|
}
|
|
387
342
|
function resolveSyncToken(token, state) {
|
|
@@ -405,31 +360,60 @@ function resolveSyncToken(token, state) {
|
|
|
405
360
|
}
|
|
406
361
|
function createSyncResolver(container) {
|
|
407
362
|
const introspection = toContainerIntrospection(container);
|
|
408
|
-
const
|
|
409
|
-
installFactoryResolutionTracking(introspection, factoryResolutionKinds);
|
|
363
|
+
const rootIntrospection = rootContainerIntrospection(introspection);
|
|
410
364
|
const state = {
|
|
411
|
-
|
|
365
|
+
cacheOwner: rootIntrospection.cacheOwner,
|
|
366
|
+
factoryResolutionKinds: rootIntrospection.factoryResolutionKinds,
|
|
412
367
|
introspection,
|
|
413
|
-
multiSingletonCache:
|
|
368
|
+
multiSingletonCache: rootIntrospection.multiSingletonCache,
|
|
414
369
|
resolutionChain: new Set(),
|
|
415
|
-
singletonCache:
|
|
370
|
+
singletonCache: rootIntrospection.singletonCache,
|
|
416
371
|
syncMultiSingletonValues: new Map(),
|
|
417
372
|
syncSingletonValues: new Map()
|
|
418
373
|
};
|
|
374
|
+
const refreshState = () => {
|
|
375
|
+
const freshIntrospection = toContainerIntrospection(container);
|
|
376
|
+
const freshRootIntrospection = rootContainerIntrospection(freshIntrospection);
|
|
377
|
+
state.cacheOwner = freshRootIntrospection.cacheOwner;
|
|
378
|
+
state.factoryResolutionKinds = freshRootIntrospection.factoryResolutionKinds;
|
|
379
|
+
state.introspection = freshIntrospection;
|
|
380
|
+
state.multiSingletonCache = freshRootIntrospection.multiSingletonCache;
|
|
381
|
+
state.singletonCache = freshRootIntrospection.singletonCache;
|
|
382
|
+
for (const [token, memo] of state.syncSingletonValues) {
|
|
383
|
+
if (state.singletonCache.get(token) !== memo.cacheEntry) {
|
|
384
|
+
state.syncSingletonValues.delete(token);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const [provider, memo] of state.syncMultiSingletonValues) {
|
|
388
|
+
if (state.multiSingletonCache.get(provider) !== memo.cacheEntry) {
|
|
389
|
+
state.syncMultiSingletonValues.delete(provider);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
419
393
|
return {
|
|
420
|
-
get: token =>
|
|
394
|
+
get: token => {
|
|
395
|
+
refreshState();
|
|
396
|
+
return resolveSyncToken(token, state);
|
|
397
|
+
},
|
|
421
398
|
syncFromContainer: async () => {
|
|
399
|
+
refreshState();
|
|
422
400
|
for (const [token, promise] of state.singletonCache) {
|
|
423
401
|
if (!canPromoteCachedSingleton(state, token)) {
|
|
424
402
|
continue;
|
|
425
403
|
}
|
|
426
|
-
state.syncSingletonValues.set(token,
|
|
404
|
+
state.syncSingletonValues.set(token, {
|
|
405
|
+
cacheEntry: promise,
|
|
406
|
+
value: await promise
|
|
407
|
+
});
|
|
427
408
|
}
|
|
428
409
|
for (const [provider, promise] of state.multiSingletonCache) {
|
|
429
410
|
if (!providerGraphForProviderIsSyncResolvable(state, provider)) {
|
|
430
411
|
continue;
|
|
431
412
|
}
|
|
432
|
-
state.syncMultiSingletonValues.set(provider,
|
|
413
|
+
state.syncMultiSingletonValues.set(provider, {
|
|
414
|
+
cacheEntry: promise,
|
|
415
|
+
value: await promise
|
|
416
|
+
});
|
|
433
417
|
}
|
|
434
418
|
}
|
|
435
419
|
};
|
|
@@ -523,18 +507,29 @@ class DefaultTestingModuleBuilder {
|
|
|
523
507
|
return this;
|
|
524
508
|
}
|
|
525
509
|
async compile() {
|
|
526
|
-
const bootstrapped = this.bootstrapTestingModule();
|
|
527
|
-
const syncResolver = createSyncResolver(bootstrapped.container);
|
|
528
|
-
await runTestingBootstrapLifecycle(bootstrapped, this.overrides);
|
|
529
|
-
await syncResolver.syncFromContainer();
|
|
530
|
-
return this.createTestingModuleRef(bootstrapped, syncResolver);
|
|
531
|
-
}
|
|
532
|
-
bootstrapTestingModule() {
|
|
533
510
|
const bootstrapped = this.bootstrapWithModuleReplacements();
|
|
534
|
-
|
|
535
|
-
|
|
511
|
+
try {
|
|
512
|
+
if (this.overrides.length > 0) {
|
|
513
|
+
bootstrapped.container.override(...this.overrides);
|
|
514
|
+
}
|
|
515
|
+
const syncResolver = createSyncResolver(bootstrapped.container);
|
|
516
|
+
await runTestingBootstrapLifecycle(bootstrapped, this.overrides);
|
|
517
|
+
await syncResolver.syncFromContainer();
|
|
518
|
+
const resolve = bootstrapped.container.resolve.bind(bootstrapped.container);
|
|
519
|
+
bootstrapped.container.resolve = async token => {
|
|
520
|
+
const value = await resolve(token);
|
|
521
|
+
await syncResolver.syncFromContainer();
|
|
522
|
+
return value;
|
|
523
|
+
};
|
|
524
|
+
return this.createTestingModuleRef(bootstrapped, syncResolver);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
try {
|
|
527
|
+
await bootstrapped.container.dispose();
|
|
528
|
+
} catch (cleanupError) {
|
|
529
|
+
throw new AggregateError([error, cleanupError], 'Testing module compilation and container cleanup both failed.');
|
|
530
|
+
}
|
|
531
|
+
throw error;
|
|
536
532
|
}
|
|
537
|
-
return bootstrapped;
|
|
538
533
|
}
|
|
539
534
|
bootstrapWithModuleReplacements() {
|
|
540
535
|
return bootstrapModule(this.options.rootModule, this.createBootstrapModuleOptions());
|
|
@@ -545,11 +540,7 @@ class DefaultTestingModuleBuilder {
|
|
|
545
540
|
...bootstrapped,
|
|
546
541
|
has: token => bootstrapped.container.has(token),
|
|
547
542
|
get: token => syncResolver.get(token),
|
|
548
|
-
resolve:
|
|
549
|
-
const value = await bootstrapped.container.resolve(token);
|
|
550
|
-
await syncResolver.syncFromContainer();
|
|
551
|
-
return value;
|
|
552
|
-
},
|
|
543
|
+
resolve: token => bootstrapped.container.resolve(token),
|
|
553
544
|
resolveAll: async tokens => {
|
|
554
545
|
const results = [];
|
|
555
546
|
const errors = [];
|
|
@@ -570,7 +561,6 @@ class DefaultTestingModuleBuilder {
|
|
|
570
561
|
}) => ` - ${String(token)}: ${error instanceof Error ? error.message : String(error)}`).join('\n');
|
|
571
562
|
throw new Error(`Failed to resolve ${errors.length} of ${tokens.length} tokens:\n${summary}`);
|
|
572
563
|
}
|
|
573
|
-
await syncResolver.syncFromContainer();
|
|
574
564
|
return results;
|
|
575
565
|
},
|
|
576
566
|
dispatch: async request => {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ModuleType } from '@fluojs/runtime';
|
|
2
|
+
import type { NetworkHttpErrorRepresentationBootstrapOptions, WebHttpErrorRepresentationBootstrapOptions } from './error-representation-portability.js';
|
|
3
|
+
type NetworkApp = {
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
listen(): Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
type WebApp = {
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
dispatch(request: Request): Promise<Response>;
|
|
10
|
+
};
|
|
11
|
+
type NetworkHarnessOptions<TBootstrapOptions extends object, TApp extends NetworkApp> = {
|
|
12
|
+
readonly bootstrap: (rootModule: ModuleType, options: TBootstrapOptions) => Promise<TApp>;
|
|
13
|
+
readonly createBootstrapOptions: (options: NetworkHttpErrorRepresentationBootstrapOptions) => TBootstrapOptions;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
};
|
|
16
|
+
type WebHarnessOptions<TBootstrapOptions extends object, TApp extends WebApp> = {
|
|
17
|
+
readonly bootstrap: (rootModule: ModuleType, options: TBootstrapOptions) => Promise<TApp>;
|
|
18
|
+
readonly createBootstrapOptions: (options: WebHttpErrorRepresentationBootstrapOptions) => TBootstrapOptions;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Verifies that a disconnected network request commits neither HTML nor canonical JSON fallback.
|
|
23
|
+
*
|
|
24
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
25
|
+
* @returns A promise that resolves after native disconnect and no-representation-write checks pass.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertNetworkHttpErrorRepresentationAbortPortability<TBootstrapOptions extends object, TApp extends NetworkApp>(options: NetworkHarnessOptions<TBootstrapOptions, TApp>): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Verifies that an aborted Web request commits neither HTML nor canonical JSON fallback.
|
|
30
|
+
*
|
|
31
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
32
|
+
* @returns A promise that resolves after Web abort and no-representation-write checks pass.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertWebHttpErrorRepresentationAbortPortability<TBootstrapOptions extends object, TApp extends WebApp>(options: WebHarnessOptions<TBootstrapOptions, TApp>): Promise<void>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=error-representation-abort-portability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-representation-abort-portability.d.ts","sourceRoot":"","sources":["../../src/portability/error-representation-abort-portability.ts"],"names":[],"mappings":"AAKA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE,OAAO,KAAK,EACV,8CAA8C,EAC9C,0CAA0C,EAC3C,MAAM,uCAAuC,CAAC;AAE/C,KAAK,UAAU,GAAG;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C,CAAC;AAEF,KAAK,qBAAqB,CAAC,iBAAiB,SAAS,MAAM,EAAE,IAAI,SAAS,UAAU,IAAI;IACtF,QAAQ,CAAC,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,QAAQ,CAAC,sBAAsB,EAAE,CAC/B,OAAO,EAAE,8CAA8C,KACpD,iBAAiB,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,iBAAiB,CAAC,iBAAiB,SAAS,MAAM,EAAE,IAAI,SAAS,MAAM,IAAI;IAC9E,QAAQ,CAAC,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,QAAQ,CAAC,sBAAsB,EAAE,CAC/B,OAAO,EAAE,0CAA0C,KAChD,iBAAiB,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAoKF;;;;;GAKG;AACH,wBAAsB,oDAAoD,CACxE,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,UAAU,EACvB,OAAO,EAAE,qBAAqB,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BxE;AAED;;;;;GAKG;AACH,wBAAsB,gDAAgD,CACpE,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,MAAM,EACnB,OAAO,EAAE,iBAAiB,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBpE"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { defineModule } from '@fluojs/runtime';
|
|
2
|
+
function createDeferred() {
|
|
3
|
+
let resolve = () => {};
|
|
4
|
+
const promise = new Promise(resolvePromise => {
|
|
5
|
+
resolve = resolvePromise;
|
|
6
|
+
});
|
|
7
|
+
return {
|
|
8
|
+
promise,
|
|
9
|
+
resolve
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function createAbortProbe() {
|
|
13
|
+
const providerAborted = createDeferred();
|
|
14
|
+
const providerStarted = createDeferred();
|
|
15
|
+
const requestFinished = createDeferred();
|
|
16
|
+
let providerCalls = 0;
|
|
17
|
+
const representationWrites = [];
|
|
18
|
+
const middleware = {
|
|
19
|
+
async handle(context, next) {
|
|
20
|
+
const send = context.response.send.bind(context.response);
|
|
21
|
+
context.response.send = body => {
|
|
22
|
+
if (body !== undefined) {
|
|
23
|
+
representationWrites.push(body);
|
|
24
|
+
}
|
|
25
|
+
return send(body);
|
|
26
|
+
};
|
|
27
|
+
await next();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const requestObserver = {
|
|
31
|
+
onRequestFinish() {
|
|
32
|
+
requestFinished.resolve();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const html = {
|
|
36
|
+
render({
|
|
37
|
+
request
|
|
38
|
+
}) {
|
|
39
|
+
providerCalls += 1;
|
|
40
|
+
providerStarted.resolve();
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const signal = request.signal;
|
|
43
|
+
if (signal === undefined) {
|
|
44
|
+
reject(new Error('The adapter did not expose an AbortSignal to the HTML error provider.'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const completeAfterAbort = () => {
|
|
48
|
+
providerAborted.resolve();
|
|
49
|
+
resolve('<html><body>late error document</body></html>');
|
|
50
|
+
};
|
|
51
|
+
if (signal.aborted) {
|
|
52
|
+
completeAfterAbort();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
signal.addEventListener('abort', completeAfterAbort, {
|
|
56
|
+
once: true
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
assertNoCommit(name) {
|
|
63
|
+
if (providerCalls !== 1) {
|
|
64
|
+
throw new Error(`${name} did not execute exactly one in-flight HTML error provider before abort.`);
|
|
65
|
+
}
|
|
66
|
+
if (representationWrites.length !== 0) {
|
|
67
|
+
throw new Error(`${name} committed an HTML or canonical JSON fallback response after abort: ${JSON.stringify(representationWrites)}.`);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
bootstrapOptions: {
|
|
71
|
+
cors: false,
|
|
72
|
+
errorRepresentation: {
|
|
73
|
+
html
|
|
74
|
+
},
|
|
75
|
+
middleware: [middleware]
|
|
76
|
+
},
|
|
77
|
+
providerAborted: providerAborted.promise,
|
|
78
|
+
providerStarted: providerStarted.promise,
|
|
79
|
+
requestFinished: requestFinished.promise,
|
|
80
|
+
requestObserver
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function hasListenTarget(value) {
|
|
84
|
+
return typeof value === 'object' && value !== null && 'getListenTarget' in value && typeof value.getListenTarget === 'function';
|
|
85
|
+
}
|
|
86
|
+
function resolveListeningUrl(app, name) {
|
|
87
|
+
const adapter = Reflect.get(app, 'adapter');
|
|
88
|
+
if (!hasListenTarget(adapter)) {
|
|
89
|
+
throw new Error(`${name} abort portability check could not resolve its listener URL.`);
|
|
90
|
+
}
|
|
91
|
+
return adapter.getListenTarget().url;
|
|
92
|
+
}
|
|
93
|
+
async function withTimeout(promise, name, phase) {
|
|
94
|
+
let timeout;
|
|
95
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
96
|
+
timeout = setTimeout(() => {
|
|
97
|
+
reject(new Error(`${name} timed out while waiting for ${phase}.`));
|
|
98
|
+
}, 2_000);
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
await Promise.race([promise, timeoutPromise]);
|
|
102
|
+
} finally {
|
|
103
|
+
if (timeout !== undefined) {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function closeAfterAbortAssertion(app, name, assertion) {
|
|
109
|
+
let assertionError;
|
|
110
|
+
try {
|
|
111
|
+
await assertion();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
assertionError = error;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await app.close();
|
|
117
|
+
} catch (cleanupError) {
|
|
118
|
+
throw assertionError === undefined ? cleanupError : new AggregateError([assertionError, cleanupError], `${name} abort assertion and cleanup both failed.`);
|
|
119
|
+
}
|
|
120
|
+
if (assertionError !== undefined) {
|
|
121
|
+
throw assertionError;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function createEmptyModule() {
|
|
125
|
+
class AppModule {}
|
|
126
|
+
defineModule(AppModule, {});
|
|
127
|
+
return AppModule;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Verifies that a disconnected network request commits neither HTML nor canonical JSON fallback.
|
|
132
|
+
*
|
|
133
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
134
|
+
* @returns A promise that resolves after native disconnect and no-representation-write checks pass.
|
|
135
|
+
*/
|
|
136
|
+
export async function assertNetworkHttpErrorRepresentationAbortPortability(options) {
|
|
137
|
+
const probe = createAbortProbe();
|
|
138
|
+
const app = await options.bootstrap(createEmptyModule(), options.createBootstrapOptions({
|
|
139
|
+
...probe.bootstrapOptions,
|
|
140
|
+
observers: [probe.requestObserver],
|
|
141
|
+
port: 0
|
|
142
|
+
}));
|
|
143
|
+
await closeAfterAbortAssertion(app, options.name, async () => {
|
|
144
|
+
await app.listen();
|
|
145
|
+
const {
|
|
146
|
+
request
|
|
147
|
+
} = await import('node:http');
|
|
148
|
+
const clientRequest = request(`${resolveListeningUrl(app, options.name)}/abort-error-representation`, {
|
|
149
|
+
headers: {
|
|
150
|
+
accept: 'text/html'
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
clientRequest.on('error', () => {});
|
|
154
|
+
try {
|
|
155
|
+
clientRequest.end();
|
|
156
|
+
await withTimeout(probe.providerStarted, options.name, 'the HTML error provider to start');
|
|
157
|
+
clientRequest.destroy();
|
|
158
|
+
await withTimeout(probe.providerAborted, options.name, 'the provider request signal to abort');
|
|
159
|
+
await withTimeout(probe.requestFinished, options.name, 'the aborted request lifecycle to finish');
|
|
160
|
+
probe.assertNoCommit(options.name);
|
|
161
|
+
} finally {
|
|
162
|
+
clientRequest.destroy();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Verifies that an aborted Web request commits neither HTML nor canonical JSON fallback.
|
|
169
|
+
*
|
|
170
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
171
|
+
* @returns A promise that resolves after Web abort and no-representation-write checks pass.
|
|
172
|
+
*/
|
|
173
|
+
export async function assertWebHttpErrorRepresentationAbortPortability(options) {
|
|
174
|
+
const probe = createAbortProbe();
|
|
175
|
+
const app = await options.bootstrap(createEmptyModule(), options.createBootstrapOptions(probe.bootstrapOptions));
|
|
176
|
+
await closeAfterAbortAssertion(app, options.name, async () => {
|
|
177
|
+
const abortController = new AbortController();
|
|
178
|
+
const dispatch = app.dispatch(new Request('https://runtime.test/abort-error-representation', {
|
|
179
|
+
headers: {
|
|
180
|
+
accept: 'text/html'
|
|
181
|
+
},
|
|
182
|
+
signal: abortController.signal
|
|
183
|
+
}));
|
|
184
|
+
await withTimeout(probe.providerStarted, options.name, 'the HTML error provider to start');
|
|
185
|
+
abortController.abort();
|
|
186
|
+
await withTimeout(probe.providerAborted, options.name, 'the provider request signal to abort');
|
|
187
|
+
await withTimeout(dispatch.then(() => undefined), options.name, 'the aborted Web request dispatch to finish');
|
|
188
|
+
probe.assertNoCommit(options.name);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type HttpErrorRepresentationOptions, type Middleware } from '@fluojs/http';
|
|
2
|
+
import { type ModuleType } from '@fluojs/runtime';
|
|
3
|
+
type ErrorRepresentationBootstrapOptions = {
|
|
4
|
+
readonly cors: false;
|
|
5
|
+
readonly errorRepresentation: HttpErrorRepresentationOptions | undefined;
|
|
6
|
+
readonly middleware: Middleware[];
|
|
7
|
+
};
|
|
8
|
+
type RepresentationResponses = {
|
|
9
|
+
readonly binary: Response;
|
|
10
|
+
readonly binaryHead: Response;
|
|
11
|
+
readonly committed: Response;
|
|
12
|
+
readonly head: Response;
|
|
13
|
+
readonly html: Response;
|
|
14
|
+
readonly json: Response;
|
|
15
|
+
readonly jsonHead: Response;
|
|
16
|
+
readonly plain: Response;
|
|
17
|
+
readonly plainHead: Response;
|
|
18
|
+
readonly success: Response;
|
|
19
|
+
readonly successHead: Response;
|
|
20
|
+
readonly unsupported: Response;
|
|
21
|
+
readonly unsupportedHead: Response;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Verifies provider-less canonical JSON metadata and body suppression for a HEAD response.
|
|
25
|
+
*
|
|
26
|
+
* @param name Adapter name included in assertion failures.
|
|
27
|
+
* @param response Provider-less HEAD response returned by the adapter.
|
|
28
|
+
* @returns A promise that resolves when the response preserves the portable contract.
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertProviderlessHeadResponse(name: string, response: Response): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Verifies negotiated error and successful HEAD responses returned by an adapter.
|
|
33
|
+
*
|
|
34
|
+
* @param name Adapter name included in assertion failures.
|
|
35
|
+
* @param responses Responses collected from the shared representation fixture.
|
|
36
|
+
* @returns A promise that resolves when every response preserves the portable contract.
|
|
37
|
+
*/
|
|
38
|
+
export declare function assertRepresentationResponses(name: string, responses: RepresentationResponses): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Creates the shared application module used by error-representation portability checks.
|
|
41
|
+
*
|
|
42
|
+
* @returns A module containing success, error, HEAD, and committed-response routes.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createRepresentationFixture(): ModuleType;
|
|
45
|
+
/**
|
|
46
|
+
* Creates bootstrap options with the shared HTML error-representation provider enabled.
|
|
47
|
+
*
|
|
48
|
+
* @returns Common adapter bootstrap options for negotiated representation checks.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createErrorRepresentationOptions(): ErrorRepresentationBootstrapOptions;
|
|
51
|
+
/**
|
|
52
|
+
* Creates bootstrap options without an HTML error-representation provider.
|
|
53
|
+
*
|
|
54
|
+
* @returns Common adapter bootstrap options for canonical JSON fallback checks.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createProviderlessErrorRepresentationOptions(): ErrorRepresentationBootstrapOptions;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=error-representation-portability-fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-representation-portability-fixture.d.ts","sourceRoot":"","sources":["../../src/portability/error-representation-portability-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,8BAA8B,EACnC,KAAK,UAAU,EAGhB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE,KAAK,mCAAmC,GAAG;IACzC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,mBAAmB,EAAE,8BAA8B,GAAG,SAAS,CAAC;IACzE,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC;CACnC,CAAC;AAEF,KAAK,uBAAuB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC;CACpC,CAAC;AAsCF;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAKpG;AAED;;;;;;GAMG;AACH,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,uBAAuB,GACjC,OAAO,CAAC,IAAI,CAAC,CAgEf;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,IAAI,UAAU,CAyDxD;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,IAAI,mCAAmC,CAYtF;AAED;;;;GAIG;AACH,wBAAgB,4CAA4C,IAAI,mCAAmC,CAMlG"}
|