@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.
Files changed (49) hide show
  1. package/README.ko.md +98 -16
  2. package/README.md +100 -16
  3. package/dist/babel-decorators-plugin.d.ts +1 -0
  4. package/dist/babel-decorators-plugin.d.ts.map +1 -1
  5. package/dist/babel-decorators-plugin.js +1 -0
  6. package/dist/byte-range-portability-consumer.test-fixture.d.ts +2 -0
  7. package/dist/byte-range-portability-consumer.test-fixture.d.ts.map +1 -0
  8. package/dist/byte-range-portability-consumer.test-fixture.js +11 -0
  9. package/dist/conformance/fetch-style-websocket-conformance.d.ts +1 -0
  10. package/dist/conformance/fetch-style-websocket-conformance.d.ts.map +1 -1
  11. package/dist/conformance/fetch-style-websocket-conformance.js +1 -1
  12. package/dist/conformance/platform-shell-lifecycle-conformance.d.ts +30 -0
  13. package/dist/conformance/platform-shell-lifecycle-conformance.d.ts.map +1 -0
  14. package/dist/conformance/platform-shell-lifecycle-conformance.js +259 -0
  15. package/dist/http.d.ts +2 -1
  16. package/dist/http.d.ts.map +1 -1
  17. package/dist/http.js +8 -3
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -2
  21. package/dist/mock-types.d.ts +2 -0
  22. package/dist/mock-types.d.ts.map +1 -0
  23. package/dist/mock-types.js +1 -0
  24. package/dist/mock.d.ts +3 -2
  25. package/dist/mock.d.ts.map +1 -1
  26. package/dist/mock.js +1 -1
  27. package/dist/module.d.ts.map +1 -1
  28. package/dist/module.js +103 -113
  29. package/dist/portability/error-representation-abort-portability.d.ts +36 -0
  30. package/dist/portability/error-representation-abort-portability.d.ts.map +1 -0
  31. package/dist/portability/error-representation-abort-portability.js +190 -0
  32. package/dist/portability/error-representation-portability-fixture.d.ts +58 -0
  33. package/dist/portability/error-representation-portability-fixture.d.ts.map +1 -0
  34. package/dist/portability/error-representation-portability-fixture.js +202 -0
  35. package/dist/portability/error-representation-portability.d.ts +50 -0
  36. package/dist/portability/error-representation-portability.d.ts.map +1 -0
  37. package/dist/portability/error-representation-portability.js +153 -0
  38. package/dist/portability/http-adapter-portability.d.ts +25 -8
  39. package/dist/portability/http-adapter-portability.d.ts.map +1 -1
  40. package/dist/portability/http-adapter-portability.js +446 -69
  41. package/dist/portability/response-cookie-portability.d.ts +16 -0
  42. package/dist/portability/response-cookie-portability.d.ts.map +1 -0
  43. package/dist/portability/response-cookie-portability.js +81 -0
  44. package/dist/portability/web-runtime-adapter-portability.d.ts +24 -8
  45. package/dist/portability/web-runtime-adapter-portability.d.ts.map +1 -1
  46. package/dist/portability/web-runtime-adapter-portability.js +356 -31
  47. package/dist/types.d.ts +68 -4
  48. package/dist/types.d.ts.map +1 -1
  49. 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 { bootstrapModule } from '@fluojs/runtime';
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
- for (let index = 0; index < effectiveProviders.length; index += 1) {
157
- const effectiveProvider = effectiveProviders[index];
158
- if (!effectiveProvider) {
159
- continue;
160
- }
161
- if (seenProviders.has(effectiveProvider)) {
162
- continue;
163
- }
164
- seenProviders.add(effectiveProvider);
165
- if (!isSingletonLifecycleProvider(effectiveProvider)) {
166
- continue;
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
- if (state.syncSingletonValues.has(provider.provide)) {
359
- return state.syncSingletonValues.get(provider.provide);
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
- state.syncSingletonValues.set(provider.provide, instance);
366
- state.singletonCache.set(provider.provide, Promise.resolve(instance));
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
- if (state.syncMultiSingletonValues.has(provider)) {
377
- return state.syncMultiSingletonValues.get(provider);
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
- state.syncMultiSingletonValues.set(provider, instance);
384
- state.multiSingletonCache.set(provider, Promise.resolve(instance));
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 factoryResolutionKinds = new WeakMap();
409
- installFactoryResolutionTracking(introspection, factoryResolutionKinds);
363
+ const rootIntrospection = rootContainerIntrospection(introspection);
410
364
  const state = {
411
- factoryResolutionKinds,
365
+ cacheOwner: rootIntrospection.cacheOwner,
366
+ factoryResolutionKinds: rootIntrospection.factoryResolutionKinds,
412
367
  introspection,
413
- multiSingletonCache: rootContainerIntrospection(introspection).multiSingletonCache,
368
+ multiSingletonCache: rootIntrospection.multiSingletonCache,
414
369
  resolutionChain: new Set(),
415
- singletonCache: rootContainerIntrospection(introspection).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 => resolveSyncToken(token, state),
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, await promise);
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, await promise);
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
- if (this.overrides.length > 0) {
535
- bootstrapped.container.override(...this.overrides);
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: async token => {
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"}