@fluojs/testing 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +117 -0
- package/README.md +115 -0
- package/dist/app.d.ts +16 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +54 -0
- package/dist/babel-decorators-plugin.d.ts +36 -0
- package/dist/babel-decorators-plugin.d.ts.map +1 -0
- package/dist/babel-decorators-plugin.js +67 -0
- package/dist/conformance/fetch-style-websocket-conformance.d.ts +14 -0
- package/dist/conformance/fetch-style-websocket-conformance.d.ts.map +1 -0
- package/dist/conformance/fetch-style-websocket-conformance.js +34 -0
- package/dist/conformance/platform-conformance.d.ts +42 -0
- package/dist/conformance/platform-conformance.d.ts.map +1 -0
- package/dist/conformance/platform-conformance.js +193 -0
- package/dist/http.d.ts +73 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +239 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/mock.d.ts +26 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +60 -0
- package/dist/module.d.ts +45 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +405 -0
- package/dist/portability/http-adapter-portability.d.ts +83 -0
- package/dist/portability/http-adapter-portability.d.ts.map +1 -0
- package/dist/portability/http-adapter-portability.js +528 -0
- package/dist/portability/web-runtime-adapter-portability.d.ts +26 -0
- package/dist/portability/web-runtime-adapter-portability.d.ts.map +1 -0
- package/dist/portability/web-runtime-adapter-portability.js +260 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/vitest.d.ts +9 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +11 -0
- package/package.json +102 -0
|
@@ -0,0 +1,528 @@
|
|
|
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 { createServer } from 'node:net';
|
|
7
|
+
import { request as httpsRequest } from 'node:https';
|
|
8
|
+
import { Controller, Get, Post, SseResponse } from '@fluojs/http';
|
|
9
|
+
import { defineModule } from '@fluojs/runtime';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for configuring the HTTP adapter portability harness.
|
|
13
|
+
*
|
|
14
|
+
* @template TBootstrapOptions - Type for bootstrap-specific options.
|
|
15
|
+
* @template TRunOptions - Type for run-specific options.
|
|
16
|
+
* @template TApp - Type for the application instance.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
async function findAvailablePort() {
|
|
20
|
+
return await new Promise((resolve, reject) => {
|
|
21
|
+
const server = createServer();
|
|
22
|
+
server.once('error', reject);
|
|
23
|
+
server.listen(0, () => {
|
|
24
|
+
const address = server.address();
|
|
25
|
+
if (!address || typeof address === 'string') {
|
|
26
|
+
reject(new Error('Failed to resolve an available port.'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
server.close(error => {
|
|
30
|
+
if (error) {
|
|
31
|
+
reject(error);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
resolve(address.port);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function requestHttps(url) {
|
|
40
|
+
return await new Promise((resolve, reject) => {
|
|
41
|
+
const request = httpsRequest(url, {
|
|
42
|
+
rejectUnauthorized: false
|
|
43
|
+
}, response => {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
response.on('data', chunk => {
|
|
46
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
47
|
+
});
|
|
48
|
+
response.on('end', () => {
|
|
49
|
+
resolve({
|
|
50
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
51
|
+
statusCode: response.statusCode ?? 0
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
response.on('error', reject);
|
|
55
|
+
});
|
|
56
|
+
request.on('error', reject);
|
|
57
|
+
request.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function closeSilently(app) {
|
|
61
|
+
try {
|
|
62
|
+
await app.close();
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A portability harness for testing HTTP adapters to ensure they behave
|
|
68
|
+
* consistently across different environments.
|
|
69
|
+
*
|
|
70
|
+
* @template TBootstrapOptions - Type for bootstrap-specific options.
|
|
71
|
+
* @template TRunOptions - Type for run-specific options.
|
|
72
|
+
* @template TApp - Type for the application instance.
|
|
73
|
+
*/
|
|
74
|
+
export class HttpAdapterPortabilityHarness {
|
|
75
|
+
/**
|
|
76
|
+
* Creates a new instance of the {@link HttpAdapterPortabilityHarness}.
|
|
77
|
+
*
|
|
78
|
+
* @param options - Configuration options for the harness.
|
|
79
|
+
*/
|
|
80
|
+
constructor(options) {
|
|
81
|
+
this.options = options;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Asserts that the adapter preserves malformed cookie values without crashing
|
|
86
|
+
* or incorrectly normalizing them.
|
|
87
|
+
*/
|
|
88
|
+
async assertPreservesMalformedCookieValues() {
|
|
89
|
+
let _initProto, _initClass;
|
|
90
|
+
let _CookieController;
|
|
91
|
+
class CookieController {
|
|
92
|
+
static {
|
|
93
|
+
({
|
|
94
|
+
e: [_initProto],
|
|
95
|
+
c: [_CookieController, _initClass]
|
|
96
|
+
} = _applyDecs(this, [Controller('/cookies')], [[Get('/'), 2, "readCookies"]]));
|
|
97
|
+
}
|
|
98
|
+
constructor() {
|
|
99
|
+
_initProto(this);
|
|
100
|
+
}
|
|
101
|
+
readCookies(_input, context) {
|
|
102
|
+
return context.request.cookies;
|
|
103
|
+
}
|
|
104
|
+
static {
|
|
105
|
+
_initClass();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
class AppModule {}
|
|
109
|
+
defineModule(AppModule, {
|
|
110
|
+
controllers: [_CookieController]
|
|
111
|
+
});
|
|
112
|
+
const port = await findAvailablePort();
|
|
113
|
+
const app = await this.options.bootstrap(AppModule, {
|
|
114
|
+
cors: false,
|
|
115
|
+
port
|
|
116
|
+
});
|
|
117
|
+
await app.listen();
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`http://127.0.0.1:${String(port)}/cookies`, {
|
|
120
|
+
headers: {
|
|
121
|
+
cookie: 'good=hello%20world; bad=%E0%A4%A'
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
if (response.status !== 200) {
|
|
125
|
+
throw new Error(`${this.options.name} adapter changed malformed-cookie handling: expected 200 but received ${String(response.status)}.`);
|
|
126
|
+
}
|
|
127
|
+
const body = await response.json();
|
|
128
|
+
if (typeof body !== 'object' || body === null || !('bad' in body) || !('good' in body) || body.bad !== '%E0%A4%A' || body.good !== 'hello world' || Object.keys(body).length !== 2) {
|
|
129
|
+
throw new Error(`${this.options.name} adapter changed malformed-cookie normalization.`);
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
await closeSilently(app);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async assertPreservesRawBodyForJsonAndText() {
|
|
136
|
+
let _initProto2, _initClass2;
|
|
137
|
+
let _WebhookController;
|
|
138
|
+
class WebhookController {
|
|
139
|
+
static {
|
|
140
|
+
({
|
|
141
|
+
e: [_initProto2],
|
|
142
|
+
c: [_WebhookController, _initClass2]
|
|
143
|
+
} = _applyDecs(this, [Controller('/webhooks')], [[Post('/json'), 2, "handleJson"], [Post('/text'), 2, "handleText"]]));
|
|
144
|
+
}
|
|
145
|
+
constructor() {
|
|
146
|
+
_initProto2(this);
|
|
147
|
+
}
|
|
148
|
+
handleJson(_input, context) {
|
|
149
|
+
return {
|
|
150
|
+
parsed: context.request.body,
|
|
151
|
+
raw: Buffer.from(context.request.rawBody ?? new Uint8Array()).toString('utf8')
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
handleText(_input, context) {
|
|
155
|
+
return {
|
|
156
|
+
parsed: context.request.body,
|
|
157
|
+
raw: Buffer.from(context.request.rawBody ?? new Uint8Array()).toString('utf8')
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
static {
|
|
161
|
+
_initClass2();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
class AppModule {}
|
|
165
|
+
defineModule(AppModule, {
|
|
166
|
+
controllers: [_WebhookController]
|
|
167
|
+
});
|
|
168
|
+
const port = await findAvailablePort();
|
|
169
|
+
const app = await this.options.bootstrap(AppModule, {
|
|
170
|
+
cors: false,
|
|
171
|
+
port,
|
|
172
|
+
rawBody: true
|
|
173
|
+
});
|
|
174
|
+
await app.listen();
|
|
175
|
+
try {
|
|
176
|
+
const [jsonResponse, textResponse] = await Promise.all([fetch(`http://127.0.0.1:${String(port)}/webhooks/json`, {
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
provider: 'stripe'
|
|
179
|
+
}),
|
|
180
|
+
headers: {
|
|
181
|
+
'content-type': 'application/json'
|
|
182
|
+
},
|
|
183
|
+
method: 'POST'
|
|
184
|
+
}), fetch(`http://127.0.0.1:${String(port)}/webhooks/text`, {
|
|
185
|
+
body: 'ping=1',
|
|
186
|
+
headers: {
|
|
187
|
+
'content-type': 'text/plain; charset=utf-8'
|
|
188
|
+
},
|
|
189
|
+
method: 'POST'
|
|
190
|
+
})]);
|
|
191
|
+
if (jsonResponse.status !== 201 || textResponse.status !== 201) {
|
|
192
|
+
throw new Error(`${this.options.name} adapter changed rawBody response status semantics.`);
|
|
193
|
+
}
|
|
194
|
+
const [jsonBody, textBody] = await Promise.all([jsonResponse.json(), textResponse.json()]);
|
|
195
|
+
if (JSON.stringify(jsonBody) !== JSON.stringify({
|
|
196
|
+
parsed: {
|
|
197
|
+
provider: 'stripe'
|
|
198
|
+
},
|
|
199
|
+
raw: '{"provider":"stripe"}'
|
|
200
|
+
})) {
|
|
201
|
+
throw new Error(`${this.options.name} adapter changed JSON rawBody semantics.`);
|
|
202
|
+
}
|
|
203
|
+
if (JSON.stringify(textBody) !== JSON.stringify({
|
|
204
|
+
parsed: 'ping=1',
|
|
205
|
+
raw: 'ping=1'
|
|
206
|
+
})) {
|
|
207
|
+
throw new Error(`${this.options.name} adapter changed text rawBody semantics.`);
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
await closeSilently(app);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async assertExcludesRawBodyForMultipart() {
|
|
214
|
+
let _initProto3, _initClass3;
|
|
215
|
+
let _UploadController;
|
|
216
|
+
class UploadController {
|
|
217
|
+
static {
|
|
218
|
+
({
|
|
219
|
+
e: [_initProto3],
|
|
220
|
+
c: [_UploadController, _initClass3]
|
|
221
|
+
} = _applyDecs(this, [Controller('/uploads')], [[Post('/'), 2, "upload"]]));
|
|
222
|
+
}
|
|
223
|
+
constructor() {
|
|
224
|
+
_initProto3(this);
|
|
225
|
+
}
|
|
226
|
+
upload(_input, context) {
|
|
227
|
+
return {
|
|
228
|
+
body: context.request.body,
|
|
229
|
+
fileCount: context.request.files?.length ?? 0,
|
|
230
|
+
hasRawBody: context.request.rawBody !== undefined
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
static {
|
|
234
|
+
_initClass3();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
class AppModule {}
|
|
238
|
+
defineModule(AppModule, {
|
|
239
|
+
controllers: [_UploadController]
|
|
240
|
+
});
|
|
241
|
+
const port = await findAvailablePort();
|
|
242
|
+
const app = await this.options.bootstrap(AppModule, {
|
|
243
|
+
cors: false,
|
|
244
|
+
port,
|
|
245
|
+
rawBody: true
|
|
246
|
+
});
|
|
247
|
+
await app.listen();
|
|
248
|
+
try {
|
|
249
|
+
const form = new FormData();
|
|
250
|
+
form.set('name', 'Ada');
|
|
251
|
+
form.set('payload', new Blob(['hello'], {
|
|
252
|
+
type: 'text/plain'
|
|
253
|
+
}), 'payload.txt');
|
|
254
|
+
const response = await fetch(`http://127.0.0.1:${String(port)}/uploads`, {
|
|
255
|
+
body: form,
|
|
256
|
+
method: 'POST'
|
|
257
|
+
});
|
|
258
|
+
if (response.status !== 201) {
|
|
259
|
+
throw new Error(`${this.options.name} adapter changed multipart response status semantics.`);
|
|
260
|
+
}
|
|
261
|
+
const body = await response.json();
|
|
262
|
+
if (JSON.stringify(body) !== JSON.stringify({
|
|
263
|
+
body: {
|
|
264
|
+
name: 'Ada'
|
|
265
|
+
},
|
|
266
|
+
fileCount: 1,
|
|
267
|
+
hasRawBody: false
|
|
268
|
+
})) {
|
|
269
|
+
throw new Error(`${this.options.name} adapter changed multipart rawBody semantics.`);
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
await closeSilently(app);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async assertSupportsSseStreaming() {
|
|
276
|
+
let _initProto4, _initClass4;
|
|
277
|
+
let _EventsController;
|
|
278
|
+
class EventsController {
|
|
279
|
+
static {
|
|
280
|
+
({
|
|
281
|
+
e: [_initProto4],
|
|
282
|
+
c: [_EventsController, _initClass4]
|
|
283
|
+
} = _applyDecs(this, [Controller('/events')], [[Get('/'), 2, "stream"]]));
|
|
284
|
+
}
|
|
285
|
+
constructor() {
|
|
286
|
+
_initProto4(this);
|
|
287
|
+
}
|
|
288
|
+
stream(_input, context) {
|
|
289
|
+
const stream = new SseResponse(context);
|
|
290
|
+
stream.comment('connected');
|
|
291
|
+
stream.send({
|
|
292
|
+
ready: true
|
|
293
|
+
}, {
|
|
294
|
+
event: 'ready',
|
|
295
|
+
id: 'evt-1'
|
|
296
|
+
});
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
stream.close();
|
|
299
|
+
}, 10);
|
|
300
|
+
return stream;
|
|
301
|
+
}
|
|
302
|
+
static {
|
|
303
|
+
_initClass4();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
class AppModule {}
|
|
307
|
+
defineModule(AppModule, {
|
|
308
|
+
controllers: [_EventsController]
|
|
309
|
+
});
|
|
310
|
+
const port = await findAvailablePort();
|
|
311
|
+
const app = await this.options.bootstrap(AppModule, {
|
|
312
|
+
cors: false,
|
|
313
|
+
port
|
|
314
|
+
});
|
|
315
|
+
await app.listen();
|
|
316
|
+
try {
|
|
317
|
+
const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
|
|
318
|
+
headers: {
|
|
319
|
+
accept: 'text/event-stream'
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const body = await response.text();
|
|
323
|
+
if (response.status !== 200) {
|
|
324
|
+
throw new Error(`${this.options.name} adapter changed SSE response status semantics.`);
|
|
325
|
+
}
|
|
326
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
327
|
+
if (!contentType.includes('text/event-stream')) {
|
|
328
|
+
throw new Error(`${this.options.name} adapter does not expose text/event-stream content-type.`);
|
|
329
|
+
}
|
|
330
|
+
if (!body.includes('event: ready') || !body.includes('data: {"ready":true}')) {
|
|
331
|
+
throw new Error(`${this.options.name} adapter changed SSE body framing.`);
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
await closeSilently(app);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async assertReportsConfiguredHostInStartupLogs() {
|
|
338
|
+
let _initProto5, _initClass5;
|
|
339
|
+
const loggerEvents = [];
|
|
340
|
+
const logger = {
|
|
341
|
+
debug() {},
|
|
342
|
+
error(message, error, context) {
|
|
343
|
+
loggerEvents.push(`error:${context}:${message}:${error instanceof Error ? error.message : 'none'}`);
|
|
344
|
+
},
|
|
345
|
+
log(message, context) {
|
|
346
|
+
loggerEvents.push(`log:${context}:${message}`);
|
|
347
|
+
},
|
|
348
|
+
warn() {}
|
|
349
|
+
};
|
|
350
|
+
let _HealthController;
|
|
351
|
+
class HealthController {
|
|
352
|
+
static {
|
|
353
|
+
({
|
|
354
|
+
e: [_initProto5],
|
|
355
|
+
c: [_HealthController, _initClass5]
|
|
356
|
+
} = _applyDecs(this, [Controller('/health')], [[Get('/'), 2, "getHealth"]]));
|
|
357
|
+
}
|
|
358
|
+
constructor() {
|
|
359
|
+
_initProto5(this);
|
|
360
|
+
}
|
|
361
|
+
getHealth() {
|
|
362
|
+
return {
|
|
363
|
+
ok: true
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
static {
|
|
367
|
+
_initClass5();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
class AppModule {}
|
|
371
|
+
defineModule(AppModule, {
|
|
372
|
+
controllers: [_HealthController]
|
|
373
|
+
});
|
|
374
|
+
const port = await findAvailablePort();
|
|
375
|
+
const app = await this.options.run(AppModule, {
|
|
376
|
+
cors: false,
|
|
377
|
+
host: '127.0.0.1',
|
|
378
|
+
logger,
|
|
379
|
+
port
|
|
380
|
+
});
|
|
381
|
+
try {
|
|
382
|
+
const response = await fetch(`http://127.0.0.1:${String(port)}/health`);
|
|
383
|
+
if (response.status !== 200) {
|
|
384
|
+
throw new Error(`${this.options.name} adapter changed host-bound health response semantics.`);
|
|
385
|
+
}
|
|
386
|
+
const body = await response.json();
|
|
387
|
+
if (JSON.stringify(body) !== JSON.stringify({
|
|
388
|
+
ok: true
|
|
389
|
+
})) {
|
|
390
|
+
throw new Error(`${this.options.name} adapter changed host-bound response payload.`);
|
|
391
|
+
}
|
|
392
|
+
const expectedLog = `log:FluoFactory:Listening on http://127.0.0.1:${String(port)}`;
|
|
393
|
+
if (!loggerEvents.includes(expectedLog)) {
|
|
394
|
+
throw new Error(`${this.options.name} adapter changed startup host logging.`);
|
|
395
|
+
}
|
|
396
|
+
} finally {
|
|
397
|
+
await closeSilently(app);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async assertReportsHttpsStartupUrl(https) {
|
|
401
|
+
let _initProto6, _initClass6;
|
|
402
|
+
const loggerEvents = [];
|
|
403
|
+
const logger = {
|
|
404
|
+
debug() {},
|
|
405
|
+
error(message, error, context) {
|
|
406
|
+
loggerEvents.push(`error:${context}:${message}:${error instanceof Error ? error.message : 'none'}`);
|
|
407
|
+
},
|
|
408
|
+
log(message, context) {
|
|
409
|
+
loggerEvents.push(`log:${context}:${message}`);
|
|
410
|
+
},
|
|
411
|
+
warn() {}
|
|
412
|
+
};
|
|
413
|
+
let _HealthController2;
|
|
414
|
+
class HealthController {
|
|
415
|
+
static {
|
|
416
|
+
({
|
|
417
|
+
e: [_initProto6],
|
|
418
|
+
c: [_HealthController2, _initClass6]
|
|
419
|
+
} = _applyDecs(this, [Controller('/health')], [[Get('/'), 2, "getHealth"]]));
|
|
420
|
+
}
|
|
421
|
+
constructor() {
|
|
422
|
+
_initProto6(this);
|
|
423
|
+
}
|
|
424
|
+
getHealth() {
|
|
425
|
+
return {
|
|
426
|
+
ok: true
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
static {
|
|
430
|
+
_initClass6();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
class AppModule {}
|
|
434
|
+
defineModule(AppModule, {
|
|
435
|
+
controllers: [_HealthController2]
|
|
436
|
+
});
|
|
437
|
+
const port = await findAvailablePort();
|
|
438
|
+
const app = await this.options.run(AppModule, {
|
|
439
|
+
cors: false,
|
|
440
|
+
host: '127.0.0.1',
|
|
441
|
+
https,
|
|
442
|
+
logger,
|
|
443
|
+
port
|
|
444
|
+
});
|
|
445
|
+
try {
|
|
446
|
+
const response = await requestHttps(`https://127.0.0.1:${String(port)}/health`);
|
|
447
|
+
if (response.statusCode !== 200) {
|
|
448
|
+
throw new Error(`${this.options.name} adapter changed HTTPS response status semantics.`);
|
|
449
|
+
}
|
|
450
|
+
if (JSON.stringify(JSON.parse(response.body)) !== JSON.stringify({
|
|
451
|
+
ok: true
|
|
452
|
+
})) {
|
|
453
|
+
throw new Error(`${this.options.name} adapter changed HTTPS response payload semantics.`);
|
|
454
|
+
}
|
|
455
|
+
const expectedLog = `log:FluoFactory:Listening on https://127.0.0.1:${String(port)}`;
|
|
456
|
+
if (!loggerEvents.includes(expectedLog)) {
|
|
457
|
+
throw new Error(`${this.options.name} adapter changed HTTPS startup logging.`);
|
|
458
|
+
}
|
|
459
|
+
} finally {
|
|
460
|
+
await closeSilently(app);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async assertRemovesShutdownSignalListenersAfterClose() {
|
|
464
|
+
let _initProto7, _initClass7;
|
|
465
|
+
const logger = {
|
|
466
|
+
debug() {},
|
|
467
|
+
error() {},
|
|
468
|
+
log() {},
|
|
469
|
+
warn() {}
|
|
470
|
+
};
|
|
471
|
+
let _HealthController3;
|
|
472
|
+
class HealthController {
|
|
473
|
+
static {
|
|
474
|
+
({
|
|
475
|
+
e: [_initProto7],
|
|
476
|
+
c: [_HealthController3, _initClass7]
|
|
477
|
+
} = _applyDecs(this, [Controller('/health')], [[Get('/'), 2, "getHealth"]]));
|
|
478
|
+
}
|
|
479
|
+
constructor() {
|
|
480
|
+
_initProto7(this);
|
|
481
|
+
}
|
|
482
|
+
getHealth() {
|
|
483
|
+
return {
|
|
484
|
+
ok: true
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
static {
|
|
488
|
+
_initClass7();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
class AppModule {}
|
|
492
|
+
defineModule(AppModule, {
|
|
493
|
+
controllers: [_HealthController3]
|
|
494
|
+
});
|
|
495
|
+
const signal = 'SIGTERM';
|
|
496
|
+
const listenersBefore = process.listeners(signal).length;
|
|
497
|
+
const port = await findAvailablePort();
|
|
498
|
+
const app = await this.options.run(AppModule, {
|
|
499
|
+
cors: false,
|
|
500
|
+
logger,
|
|
501
|
+
port,
|
|
502
|
+
shutdownSignals: [signal]
|
|
503
|
+
});
|
|
504
|
+
try {
|
|
505
|
+
if (process.listeners(signal).length !== listenersBefore + 1) {
|
|
506
|
+
throw new Error(`${this.options.name} adapter did not register the expected shutdown listener.`);
|
|
507
|
+
}
|
|
508
|
+
} finally {
|
|
509
|
+
await closeSilently(app);
|
|
510
|
+
}
|
|
511
|
+
if (process.listeners(signal).length !== listenersBefore) {
|
|
512
|
+
throw new Error(`${this.options.name} adapter leaked shutdown signal listeners after close().`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Creates a new {@link HttpAdapterPortabilityHarness} instance with the provided options.
|
|
519
|
+
*
|
|
520
|
+
* @template TBootstrapOptions - Type for bootstrap-specific options.
|
|
521
|
+
* @template TRunOptions - Type for run-specific options.
|
|
522
|
+
* @template TApp - Type for the application instance.
|
|
523
|
+
* @param options - Configuration options for the harness.
|
|
524
|
+
* @returns A new portability harness instance.
|
|
525
|
+
*/
|
|
526
|
+
export function createHttpAdapterPortabilityHarness(options) {
|
|
527
|
+
return new HttpAdapterPortabilityHarness(options);
|
|
528
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ModuleType, type UploadedFile } from '@fluojs/runtime';
|
|
2
|
+
declare module '@fluojs/http' {
|
|
3
|
+
interface FrameworkRequest {
|
|
4
|
+
files?: UploadedFile[];
|
|
5
|
+
rawBody?: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
type WebRuntimePortabilityAppLike = {
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
dispatch(request: Request): Promise<Response>;
|
|
11
|
+
};
|
|
12
|
+
export interface WebRuntimeHttpAdapterPortabilityHarnessOptions<TBootstrapOptions extends object, TApp extends WebRuntimePortabilityAppLike = WebRuntimePortabilityAppLike> {
|
|
13
|
+
bootstrap: (rootModule: ModuleType, options: TBootstrapOptions) => Promise<TApp>;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class WebRuntimeHttpAdapterPortabilityHarness<TBootstrapOptions extends object, TApp extends WebRuntimePortabilityAppLike = WebRuntimePortabilityAppLike> {
|
|
17
|
+
private readonly options;
|
|
18
|
+
constructor(options: WebRuntimeHttpAdapterPortabilityHarnessOptions<TBootstrapOptions, TApp>);
|
|
19
|
+
assertPreservesMalformedCookieValues(): Promise<void>;
|
|
20
|
+
assertPreservesRawBodyForJsonAndText(): Promise<void>;
|
|
21
|
+
assertExcludesRawBodyForMultipart(): Promise<void>;
|
|
22
|
+
assertSupportsSseStreaming(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createWebRuntimeHttpAdapterPortabilityHarness<TBootstrapOptions extends object, TApp extends WebRuntimePortabilityAppLike = WebRuntimePortabilityAppLike>(options: WebRuntimeHttpAdapterPortabilityHarnessOptions<TBootstrapOptions, TApp>): WebRuntimeHttpAdapterPortabilityHarness<TBootstrapOptions, TApp>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=web-runtime-adapter-portability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-runtime-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/web-runtime-adapter-portability.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,4BAA4B,GAAG;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,WAAW,8CAA8C,CAC7D,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B;IAExE,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAYD,qBAAa,uCAAuC,CAClD,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B;IAE5D,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,8CAA8C,CAAC,iBAAiB,EAAE,IAAI,CAAC;IAEvG,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IA2DrD,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IA2ClD,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;CA4ClD;AAED,wBAAgB,6CAA6C,CAC3D,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,4BAA4B,GAAG,4BAA4B,EAExE,OAAO,EAAE,8CAA8C,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAC/E,uCAAuC,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAElE"}
|