@fluojs/testing 2.0.0 → 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 +94 -12
- package/README.md +96 -12
- 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/mock.js +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +45 -73
- 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 +24 -1
- package/dist/portability/http-adapter-portability.d.ts.map +1 -1
- package/dist/portability/http-adapter-portability.js +441 -68
- 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 +23 -1
- 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 +5 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -15
|
@@ -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"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
|
|
2
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
3
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
4
|
+
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
5
|
+
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
6
|
+
import { Controller, Get, Head, Header, HttpCode, NotFoundException } from '@fluojs/http';
|
|
7
|
+
import { defineModule } from '@fluojs/runtime';
|
|
8
|
+
function hasErrorCode(value, code) {
|
|
9
|
+
if (typeof value !== 'object' || value === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const error = Reflect.get(value, 'error');
|
|
13
|
+
return typeof error === 'object' && error !== null && Reflect.get(error, 'code') === code;
|
|
14
|
+
}
|
|
15
|
+
function assertStatus(response, expected, name, scenario) {
|
|
16
|
+
if (response.status !== expected) {
|
|
17
|
+
throw new Error(`${name} changed ${scenario} status: expected ${String(expected)}, received ${String(response.status)}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function assertHeadRepresentationParity(name, pair) {
|
|
21
|
+
const contentType = pair.response.headers.get('content-type');
|
|
22
|
+
const headContentType = pair.headResponse.headers.get('content-type');
|
|
23
|
+
if (contentType === null || headContentType !== contentType) {
|
|
24
|
+
throw new Error(`${name} changed ${pair.scenario} HEAD content type parity: GET=${String(contentType)}, HEAD=${String(headContentType)}.`);
|
|
25
|
+
}
|
|
26
|
+
if ((await pair.response.arrayBuffer().then(body => body.byteLength)) === 0 || (await pair.headResponse.text()) !== '') {
|
|
27
|
+
throw new Error(`${name} changed ${pair.scenario} GET body or HEAD body suppression semantics.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verifies provider-less canonical JSON metadata and body suppression for a HEAD response.
|
|
33
|
+
*
|
|
34
|
+
* @param name Adapter name included in assertion failures.
|
|
35
|
+
* @param response Provider-less HEAD response returned by the adapter.
|
|
36
|
+
* @returns A promise that resolves when the response preserves the portable contract.
|
|
37
|
+
*/
|
|
38
|
+
export async function assertProviderlessHeadResponse(name, response) {
|
|
39
|
+
assertStatus(response, 404, name, 'provider-less canonical JSON HEAD error representation');
|
|
40
|
+
if (response.headers.get('content-type') !== 'application/json; charset=utf-8' || (await response.text()) !== '') {
|
|
41
|
+
throw new Error(`${name} changed provider-less canonical JSON HEAD representation metadata or body suppression.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Verifies negotiated error and successful HEAD responses returned by an adapter.
|
|
47
|
+
*
|
|
48
|
+
* @param name Adapter name included in assertion failures.
|
|
49
|
+
* @param responses Responses collected from the shared representation fixture.
|
|
50
|
+
* @returns A promise that resolves when every response preserves the portable contract.
|
|
51
|
+
*/
|
|
52
|
+
export async function assertRepresentationResponses(name, responses) {
|
|
53
|
+
assertStatus(responses.html, 404, name, 'HTML error representation');
|
|
54
|
+
assertStatus(responses.json, 404, name, 'JSON error representation');
|
|
55
|
+
assertStatus(responses.head, 404, name, 'HEAD error representation');
|
|
56
|
+
assertStatus(responses.jsonHead, 404, name, 'canonical JSON HEAD error representation');
|
|
57
|
+
assertStatus(responses.success, 202, name, 'successful JSON response');
|
|
58
|
+
assertStatus(responses.successHead, 202, name, 'successful HEAD response');
|
|
59
|
+
assertStatus(responses.plain, 200, name, 'successful plain text response');
|
|
60
|
+
assertStatus(responses.plainHead, 200, name, 'successful plain text HEAD response');
|
|
61
|
+
assertStatus(responses.binary, 200, name, 'successful binary response');
|
|
62
|
+
assertStatus(responses.binaryHead, 200, name, 'successful binary HEAD response');
|
|
63
|
+
assertStatus(responses.unsupported, 406, name, 'unsupported error representation');
|
|
64
|
+
assertStatus(responses.unsupportedHead, 406, name, 'unsupported HEAD error representation');
|
|
65
|
+
assertStatus(responses.committed, 202, name, 'already-committed response');
|
|
66
|
+
const [html, json, head, jsonHead, unsupported, unsupportedHead, committed] = await Promise.all([responses.html.text(), responses.json.json(), responses.head.text(), responses.jsonHead.text(), responses.unsupported.json(), responses.unsupportedHead.text(), responses.committed.text()]);
|
|
67
|
+
if (!responses.html.headers.get('content-type')?.includes('text/html') || !html.includes('404:NOT_FOUND')) {
|
|
68
|
+
throw new Error(`${name} changed negotiated HTML error representation semantics.`);
|
|
69
|
+
}
|
|
70
|
+
if (!hasErrorCode(json, 'NOT_FOUND')) {
|
|
71
|
+
throw new Error(`${name} changed canonical JSON error representation semantics.`);
|
|
72
|
+
}
|
|
73
|
+
if (!responses.head.headers.get('content-type')?.includes('text/html') || head !== '') {
|
|
74
|
+
throw new Error(`${name} changed HEAD error representation body suppression.`);
|
|
75
|
+
}
|
|
76
|
+
if (responses.jsonHead.headers.get('content-type') !== 'application/json; charset=utf-8' || jsonHead !== '') {
|
|
77
|
+
throw new Error(`${name} changed canonical JSON HEAD error representation metadata or body suppression.`);
|
|
78
|
+
}
|
|
79
|
+
if (responses.successHead.headers.get('x-head-contract') !== 'preserved') {
|
|
80
|
+
throw new Error(`${name} changed successful HEAD explicit header semantics.`);
|
|
81
|
+
}
|
|
82
|
+
await assertHeadRepresentationParity(name, {
|
|
83
|
+
headResponse: responses.successHead,
|
|
84
|
+
response: responses.success,
|
|
85
|
+
scenario: 'successful JSON'
|
|
86
|
+
});
|
|
87
|
+
await assertHeadRepresentationParity(name, {
|
|
88
|
+
headResponse: responses.plainHead,
|
|
89
|
+
response: responses.plain,
|
|
90
|
+
scenario: 'successful plain text'
|
|
91
|
+
});
|
|
92
|
+
await assertHeadRepresentationParity(name, {
|
|
93
|
+
headResponse: responses.binaryHead,
|
|
94
|
+
response: responses.binary,
|
|
95
|
+
scenario: 'successful binary'
|
|
96
|
+
});
|
|
97
|
+
if (!hasErrorCode(unsupported, 'NOT_ACCEPTABLE')) {
|
|
98
|
+
throw new Error(`${name} changed unsupported error representation fallback semantics.`);
|
|
99
|
+
}
|
|
100
|
+
if (responses.unsupportedHead.headers.get('content-type') !== 'application/json; charset=utf-8' || unsupportedHead !== '') {
|
|
101
|
+
throw new Error(`${name} changed unsupported HEAD error representation metadata or body suppression.`);
|
|
102
|
+
}
|
|
103
|
+
if (committed !== 'handler-owned') {
|
|
104
|
+
throw new Error(`${name} rewrote an already-committed response through the error provider.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates the shared application module used by error-representation portability checks.
|
|
110
|
+
*
|
|
111
|
+
* @returns A module containing success, error, HEAD, and committed-response routes.
|
|
112
|
+
*/
|
|
113
|
+
export function createRepresentationFixture() {
|
|
114
|
+
let _initProto, _initClass;
|
|
115
|
+
let _ErrorRepresentationC;
|
|
116
|
+
class ErrorRepresentationController {
|
|
117
|
+
static {
|
|
118
|
+
({
|
|
119
|
+
e: [_initProto],
|
|
120
|
+
c: [_ErrorRepresentationC, _initClass]
|
|
121
|
+
} = _applyDecs(this, [Controller('/error-representations')], [[Get('/json'), 2, "json"], [Head('/head'), 2, "head"], [[Head('/success-head'), Header('x-head-contract', 'preserved'), HttpCode(202)], 2, "successHead"], [[Get('/success-head'), HttpCode(202)], 2, "success"], [Head('/plain-head'), 2, "plainHead"], [Get('/plain-head'), 2, "plain"], [Head('/binary-head'), 2, "binaryHead"], [Get('/binary-head'), 2, "binary"], [Get('/committed'), 2, "committed"]]));
|
|
122
|
+
}
|
|
123
|
+
constructor() {
|
|
124
|
+
_initProto(this);
|
|
125
|
+
}
|
|
126
|
+
json() {
|
|
127
|
+
throw new NotFoundException('Matched resource missing.');
|
|
128
|
+
}
|
|
129
|
+
head() {
|
|
130
|
+
throw new NotFoundException('HEAD resource missing.');
|
|
131
|
+
}
|
|
132
|
+
successHead() {
|
|
133
|
+
return {
|
|
134
|
+
ok: true
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
success() {
|
|
138
|
+
return {
|
|
139
|
+
ok: true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
plainHead() {
|
|
143
|
+
return 'plain response';
|
|
144
|
+
}
|
|
145
|
+
plain() {
|
|
146
|
+
return 'plain response';
|
|
147
|
+
}
|
|
148
|
+
binaryHead() {
|
|
149
|
+
return new Uint8Array([1, 2, 3]);
|
|
150
|
+
}
|
|
151
|
+
binary() {
|
|
152
|
+
return new Uint8Array([1, 2, 3]);
|
|
153
|
+
}
|
|
154
|
+
async committed(_input, context) {
|
|
155
|
+
context.response.setStatus(202);
|
|
156
|
+
await context.response.send('handler-owned');
|
|
157
|
+
throw new NotFoundException('Committed response must not be replaced.');
|
|
158
|
+
}
|
|
159
|
+
static {
|
|
160
|
+
_initClass();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
class AppModule {}
|
|
164
|
+
defineModule(AppModule, {
|
|
165
|
+
controllers: [_ErrorRepresentationC]
|
|
166
|
+
});
|
|
167
|
+
return AppModule;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Creates bootstrap options with the shared HTML error-representation provider enabled.
|
|
172
|
+
*
|
|
173
|
+
* @returns Common adapter bootstrap options for negotiated representation checks.
|
|
174
|
+
*/
|
|
175
|
+
export function createErrorRepresentationOptions() {
|
|
176
|
+
return {
|
|
177
|
+
cors: false,
|
|
178
|
+
errorRepresentation: {
|
|
179
|
+
html: {
|
|
180
|
+
render({
|
|
181
|
+
json
|
|
182
|
+
}) {
|
|
183
|
+
return `<html><body>${String(json.error.status)}:${json.error.code}</body></html>`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
middleware: []
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Creates bootstrap options without an HTML error-representation provider.
|
|
193
|
+
*
|
|
194
|
+
* @returns Common adapter bootstrap options for canonical JSON fallback checks.
|
|
195
|
+
*/
|
|
196
|
+
export function createProviderlessErrorRepresentationOptions() {
|
|
197
|
+
return {
|
|
198
|
+
cors: false,
|
|
199
|
+
errorRepresentation: undefined,
|
|
200
|
+
middleware: []
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HttpErrorRepresentationOptions, Middleware, RequestObserver } from '@fluojs/http';
|
|
2
|
+
import type { ModuleType } from '@fluojs/runtime';
|
|
3
|
+
type NetworkApp = {
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
listen(): Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
type NetworkHarnessOptions<TBootstrapOptions extends object, TApp extends NetworkApp> = {
|
|
8
|
+
readonly bootstrap: (rootModule: ModuleType, options: TBootstrapOptions) => Promise<TApp>;
|
|
9
|
+
readonly createBootstrapOptions: (options: NetworkHttpErrorRepresentationBootstrapOptions) => TBootstrapOptions;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
};
|
|
12
|
+
type WebApp = {
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
dispatch(request: Request): Promise<Response>;
|
|
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
|
+
/** Adapter bootstrap fields required by the network error-representation portability scenario. */
|
|
22
|
+
export type NetworkHttpErrorRepresentationBootstrapOptions = {
|
|
23
|
+
readonly cors: false;
|
|
24
|
+
readonly errorRepresentation: HttpErrorRepresentationOptions | undefined;
|
|
25
|
+
readonly middleware: Middleware[];
|
|
26
|
+
readonly observers: RequestObserver[];
|
|
27
|
+
readonly port: 0;
|
|
28
|
+
};
|
|
29
|
+
/** Adapter bootstrap fields required by the Web error-representation portability scenario. */
|
|
30
|
+
export type WebHttpErrorRepresentationBootstrapOptions = {
|
|
31
|
+
readonly cors: false;
|
|
32
|
+
readonly errorRepresentation: HttpErrorRepresentationOptions | undefined;
|
|
33
|
+
readonly middleware: Middleware[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Verifies negotiated HTTP error representations through a listening adapter.
|
|
37
|
+
*
|
|
38
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
39
|
+
* @returns A promise that resolves after JSON, HTML, HEAD, 406, and commit-guard checks pass.
|
|
40
|
+
*/
|
|
41
|
+
export declare function assertNetworkHttpErrorRepresentationPortability<TBootstrapOptions extends object, TApp extends NetworkApp>(options: NetworkHarnessOptions<TBootstrapOptions, TApp>): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Verifies negotiated HTTP error representations through a fetch-style adapter.
|
|
44
|
+
*
|
|
45
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
46
|
+
* @returns A promise that resolves after JSON, HTML, HEAD, 406, and commit-guard checks pass.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertWebHttpErrorRepresentationPortability<TBootstrapOptions extends object, TApp extends WebApp>(options: WebHarnessOptions<TBootstrapOptions, TApp>): Promise<void>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=error-representation-portability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-representation-portability.d.ts","sourceRoot":"","sources":["../../src/portability/error-representation-portability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAChG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AASlD,KAAK,UAAU,GAAG;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,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,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,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;AAEF,kGAAkG;AAClG,MAAM,MAAM,8CAA8C,GAAG;IAC3D,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,mBAAmB,EAAE,8BAA8B,GAAG,SAAS,CAAC;IACzE,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,eAAe,EAAE,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;CAClB,CAAC;AAEF,8FAA8F;AAC9F,MAAM,MAAM,0CAA0C,GAAG;IACvD,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;AAyCF;;;;;GAKG;AACH,wBAAsB,+CAA+C,CACnE,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,UAAU,EACvB,OAAO,EAAE,qBAAqB,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CxE;AAED;;;;;GAKG;AACH,wBAAsB,2CAA2C,CAC/D,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,MAAM,EACnB,OAAO,EAAE,iBAAiB,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCpE"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { assertProviderlessHeadResponse, assertRepresentationResponses, createErrorRepresentationOptions, createProviderlessErrorRepresentationOptions, createRepresentationFixture } from './error-representation-portability-fixture.js';
|
|
2
|
+
|
|
3
|
+
/** Adapter bootstrap fields required by the network error-representation portability scenario. */
|
|
4
|
+
|
|
5
|
+
/** Adapter bootstrap fields required by the Web error-representation portability scenario. */
|
|
6
|
+
|
|
7
|
+
function hasListenTarget(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && 'getListenTarget' in value && typeof value.getListenTarget === 'function';
|
|
9
|
+
}
|
|
10
|
+
function resolveListeningUrl(app, name) {
|
|
11
|
+
const adapter = Reflect.get(app, 'adapter');
|
|
12
|
+
if (!hasListenTarget(adapter)) {
|
|
13
|
+
throw new Error(`${name} error representation portability check could not resolve its listener URL.`);
|
|
14
|
+
}
|
|
15
|
+
return adapter.getListenTarget().url;
|
|
16
|
+
}
|
|
17
|
+
async function closeAfterAssertion(app, name, assertion) {
|
|
18
|
+
let assertionError;
|
|
19
|
+
try {
|
|
20
|
+
await assertion();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
assertionError = error;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await app.close();
|
|
26
|
+
} catch (cleanupError) {
|
|
27
|
+
throw assertionError === undefined ? cleanupError : new AggregateError([assertionError, cleanupError], `${name} representation assertion and cleanup both failed.`);
|
|
28
|
+
}
|
|
29
|
+
if (assertionError !== undefined) {
|
|
30
|
+
throw assertionError;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Verifies negotiated HTTP error representations through a listening adapter.
|
|
36
|
+
*
|
|
37
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
38
|
+
* @returns A promise that resolves after JSON, HTML, HEAD, 406, and commit-guard checks pass.
|
|
39
|
+
*/
|
|
40
|
+
export async function assertNetworkHttpErrorRepresentationPortability(options) {
|
|
41
|
+
const app = await options.bootstrap(createRepresentationFixture(), options.createBootstrapOptions({
|
|
42
|
+
...createErrorRepresentationOptions(),
|
|
43
|
+
observers: [],
|
|
44
|
+
port: 0
|
|
45
|
+
}));
|
|
46
|
+
await closeAfterAssertion(app, options.name, async () => {
|
|
47
|
+
await app.listen();
|
|
48
|
+
const baseUrl = resolveListeningUrl(app, options.name);
|
|
49
|
+
await assertRepresentationResponses(options.name, {
|
|
50
|
+
binary: await fetch(`${baseUrl}/error-representations/binary-head`),
|
|
51
|
+
binaryHead: await fetch(`${baseUrl}/error-representations/binary-head`, {
|
|
52
|
+
method: 'HEAD'
|
|
53
|
+
}),
|
|
54
|
+
committed: await fetch(`${baseUrl}/error-representations/committed`, {
|
|
55
|
+
headers: {
|
|
56
|
+
accept: 'text/html'
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
head: await fetch(`${baseUrl}/error-representations/head`, {
|
|
60
|
+
headers: {
|
|
61
|
+
accept: 'text/html'
|
|
62
|
+
},
|
|
63
|
+
method: 'HEAD'
|
|
64
|
+
}),
|
|
65
|
+
html: await fetch(`${baseUrl}/not-registered`, {
|
|
66
|
+
headers: {
|
|
67
|
+
accept: 'text/html'
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
json: await fetch(`${baseUrl}/error-representations/json`, {
|
|
71
|
+
headers: {
|
|
72
|
+
accept: 'application/json'
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
jsonHead: await fetch(`${baseUrl}/error-representations/head`, {
|
|
76
|
+
headers: {
|
|
77
|
+
accept: 'application/json'
|
|
78
|
+
},
|
|
79
|
+
method: 'HEAD'
|
|
80
|
+
}),
|
|
81
|
+
plain: await fetch(`${baseUrl}/error-representations/plain-head`),
|
|
82
|
+
plainHead: await fetch(`${baseUrl}/error-representations/plain-head`, {
|
|
83
|
+
method: 'HEAD'
|
|
84
|
+
}),
|
|
85
|
+
success: await fetch(`${baseUrl}/error-representations/success-head`),
|
|
86
|
+
successHead: await fetch(`${baseUrl}/error-representations/success-head`, {
|
|
87
|
+
method: 'HEAD'
|
|
88
|
+
}),
|
|
89
|
+
unsupported: await fetch(`${baseUrl}/not-registered`, {
|
|
90
|
+
headers: {
|
|
91
|
+
accept: 'image/avif'
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
unsupportedHead: await fetch(`${baseUrl}/not-registered`, {
|
|
95
|
+
headers: {
|
|
96
|
+
accept: 'image/avif'
|
|
97
|
+
},
|
|
98
|
+
method: 'HEAD'
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
const providerlessApp = await options.bootstrap(createRepresentationFixture(), options.createBootstrapOptions({
|
|
103
|
+
...createProviderlessErrorRepresentationOptions(),
|
|
104
|
+
observers: [],
|
|
105
|
+
port: 0
|
|
106
|
+
}));
|
|
107
|
+
await closeAfterAssertion(providerlessApp, options.name, async () => {
|
|
108
|
+
await providerlessApp.listen();
|
|
109
|
+
const baseUrl = resolveListeningUrl(providerlessApp, options.name);
|
|
110
|
+
await assertProviderlessHeadResponse(options.name, await fetch(`${baseUrl}/not-registered`, {
|
|
111
|
+
method: 'HEAD'
|
|
112
|
+
}));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Verifies negotiated HTTP error representations through a fetch-style adapter.
|
|
118
|
+
*
|
|
119
|
+
* @param options Adapter bootstrap and identity callbacks.
|
|
120
|
+
* @returns A promise that resolves after JSON, HTML, HEAD, 406, and commit-guard checks pass.
|
|
121
|
+
*/
|
|
122
|
+
export async function assertWebHttpErrorRepresentationPortability(options) {
|
|
123
|
+
const app = await options.bootstrap(createRepresentationFixture(), options.createBootstrapOptions(createErrorRepresentationOptions()));
|
|
124
|
+
await closeAfterAssertion(app, options.name, async () => {
|
|
125
|
+
const request = (path, accept, method = 'GET') => new Request(`https://runtime.test${path}`, {
|
|
126
|
+
headers: {
|
|
127
|
+
accept
|
|
128
|
+
},
|
|
129
|
+
method
|
|
130
|
+
});
|
|
131
|
+
await assertRepresentationResponses(options.name, {
|
|
132
|
+
binary: await app.dispatch(request('/error-representations/binary-head', '*/*')),
|
|
133
|
+
binaryHead: await app.dispatch(request('/error-representations/binary-head', '*/*', 'HEAD')),
|
|
134
|
+
committed: await app.dispatch(request('/error-representations/committed', 'text/html')),
|
|
135
|
+
head: await app.dispatch(request('/error-representations/head', 'text/html', 'HEAD')),
|
|
136
|
+
html: await app.dispatch(request('/not-registered', 'text/html')),
|
|
137
|
+
json: await app.dispatch(request('/error-representations/json', 'application/json')),
|
|
138
|
+
jsonHead: await app.dispatch(request('/error-representations/head', 'application/json', 'HEAD')),
|
|
139
|
+
plain: await app.dispatch(request('/error-representations/plain-head', '*/*')),
|
|
140
|
+
plainHead: await app.dispatch(request('/error-representations/plain-head', '*/*', 'HEAD')),
|
|
141
|
+
success: await app.dispatch(request('/error-representations/success-head', '*/*')),
|
|
142
|
+
successHead: await app.dispatch(request('/error-representations/success-head', '*/*', 'HEAD')),
|
|
143
|
+
unsupported: await app.dispatch(request('/not-registered', 'image/avif')),
|
|
144
|
+
unsupportedHead: await app.dispatch(request('/not-registered', 'image/avif', 'HEAD'))
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
const providerlessApp = await options.bootstrap(createRepresentationFixture(), options.createBootstrapOptions(createProviderlessErrorRepresentationOptions()));
|
|
148
|
+
await closeAfterAssertion(providerlessApp, options.name, async () => {
|
|
149
|
+
await assertProviderlessHeadResponse(options.name, await providerlessApp.dispatch(new Request('https://runtime.test/not-registered', {
|
|
150
|
+
method: 'HEAD'
|
|
151
|
+
})));
|
|
152
|
+
});
|
|
153
|
+
}
|