@fluojs/testing 1.0.0-beta.3 → 1.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 +2 -0
- package/README.md +2 -0
- package/dist/portability/http-adapter-portability.d.ts.map +1 -1
- package/dist/portability/http-adapter-portability.js +38 -42
- package/dist/portability/web-runtime-adapter-portability.d.ts.map +1 -1
- package/dist/portability/web-runtime-adapter-portability.js +28 -24
- package/package.json +12 -9
package/README.ko.md
CHANGED
|
@@ -134,6 +134,8 @@ const mailer = createDeepMock(MailService);
|
|
|
134
134
|
|
|
135
135
|
프레임워크 지향 플랫폼 패키지를 작성할 때는 `@fluojs/testing/platform-conformance`, `@fluojs/testing/http-adapter-portability`, `@fluojs/testing/web-runtime-adapter-portability` 같은 서브패스를 사용해 적합성 및 이식성 검증을 수행합니다.
|
|
136
136
|
|
|
137
|
+
이식성 하니스의 cleanup도 계약에 포함됩니다. `app.close()`가 실패하면 하니스는 cleanup 실패를 보고하며, assertion이 이미 실패한 경우에는 assertion 실패와 cleanup 실패를 모두 보존하는 aggregate error를 발생시킵니다.
|
|
138
|
+
|
|
137
139
|
`HttpAdapterPortabilityHarness` 메서드는 공개 어댑터 계약 체크입니다. 직접 같은 검증을 다시 만들기보다 `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, `assertRemovesShutdownSignalListenersAfterClose()`처럼 초점이 분명한 assertion을 사용하세요.
|
|
138
140
|
|
|
139
141
|
## canonical TDD ladder
|
package/README.md
CHANGED
|
@@ -132,6 +132,8 @@ Install `vitest` in the consuming workspace before using the mock helpers so the
|
|
|
132
132
|
|
|
133
133
|
Use subpaths like `@fluojs/testing/platform-conformance`, `@fluojs/testing/http-adapter-portability`, and `@fluojs/testing/web-runtime-adapter-portability` when authoring framework-facing platform packages.
|
|
134
134
|
|
|
135
|
+
Portability harness cleanup is part of the contract: if `app.close()` fails, the harness reports that cleanup failure, and when an assertion already failed it raises an aggregate error that preserves both the assertion failure and the cleanup failure.
|
|
136
|
+
|
|
135
137
|
`HttpAdapterPortabilityHarness` methods are the public adapter contract checks. Prefer focused assertions such as `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, and `assertRemovesShutdownSignalListenersAfterClose()` instead of hand-rolled equivalents.
|
|
136
138
|
|
|
137
139
|
## Canonical TDD Ladder
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/http-adapter-portability.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"http-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/http-adapter-portability.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwC,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE3G,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,OAAO,GAAG;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,oCAAoC,CACnD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAE9B;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjF;;OAEG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,2BAA2B,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;AAiFD;;;;;;;GAOG;AACH,qBAAa,6BAA6B,CACxC,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAOlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC;;;;OAIG;gBAC0B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC;IAEhH;;;OAGG;IACG,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAkDrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAsErD,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CzE,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IAuDlD,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;IAsD/D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAmDjD;;;OAGG;IACG,mCAAmC,IAAI,OAAO,CAAC,IAAI,CAAC;IA0DpD,wCAAwC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDzD,4BAA4B,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDjF,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;CA4CtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mCAAmC,CACjD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO,EAE9B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,GAClF,6BAA6B,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,CAErE"}
|
|
@@ -57,10 +57,26 @@ async function requestHttps(url) {
|
|
|
57
57
|
request.end();
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
-
async function
|
|
60
|
+
async function runWithCleanup(app, adapterName, assertion) {
|
|
61
|
+
let hasAssertionError = false;
|
|
62
|
+
let assertionError;
|
|
63
|
+
try {
|
|
64
|
+
await assertion();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
hasAssertionError = true;
|
|
67
|
+
assertionError = error;
|
|
68
|
+
}
|
|
61
69
|
try {
|
|
62
70
|
await app.close();
|
|
63
|
-
} catch {
|
|
71
|
+
} catch (cleanupError) {
|
|
72
|
+
if (hasAssertionError) {
|
|
73
|
+
throw new AggregateError([assertionError, cleanupError], `${adapterName} adapter portability assertion failed and app.close() also failed during harness cleanup.`);
|
|
74
|
+
}
|
|
75
|
+
throw new AggregateError([cleanupError], `${adapterName} adapter app.close() failed during portability harness cleanup.`);
|
|
76
|
+
}
|
|
77
|
+
if (hasAssertionError) {
|
|
78
|
+
throw assertionError;
|
|
79
|
+
}
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
/**
|
|
@@ -115,7 +131,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
115
131
|
port
|
|
116
132
|
});
|
|
117
133
|
await app.listen();
|
|
118
|
-
|
|
134
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
119
135
|
const response = await fetch(`http://127.0.0.1:${String(port)}/cookies`, {
|
|
120
136
|
headers: {
|
|
121
137
|
cookie: 'good=hello%20world; bad=%E0%A4%A'
|
|
@@ -128,9 +144,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
128
144
|
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
145
|
throw new Error(`${this.options.name} adapter changed malformed-cookie normalization.`);
|
|
130
146
|
}
|
|
131
|
-
}
|
|
132
|
-
await closeSilently(app);
|
|
133
|
-
}
|
|
147
|
+
});
|
|
134
148
|
}
|
|
135
149
|
async assertPreservesRawBodyForJsonAndText() {
|
|
136
150
|
let _initProto2, _initClass2;
|
|
@@ -172,7 +186,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
172
186
|
rawBody: true
|
|
173
187
|
});
|
|
174
188
|
await app.listen();
|
|
175
|
-
|
|
189
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
176
190
|
const [jsonResponse, textResponse] = await Promise.all([fetch(`http://127.0.0.1:${String(port)}/webhooks/json`, {
|
|
177
191
|
body: JSON.stringify({
|
|
178
192
|
provider: 'stripe'
|
|
@@ -206,9 +220,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
206
220
|
})) {
|
|
207
221
|
throw new Error(`${this.options.name} adapter changed text rawBody semantics.`);
|
|
208
222
|
}
|
|
209
|
-
}
|
|
210
|
-
await closeSilently(app);
|
|
211
|
-
}
|
|
223
|
+
});
|
|
212
224
|
}
|
|
213
225
|
async assertPreservesExactRawBodyBytesForByteSensitivePayloads() {
|
|
214
226
|
let _initProto3, _initClass3;
|
|
@@ -244,7 +256,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
244
256
|
});
|
|
245
257
|
await this.options.prepareExactRawBodyByteTest?.(app);
|
|
246
258
|
await app.listen();
|
|
247
|
-
|
|
259
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
248
260
|
const payload = Uint8Array.from([0xe9, 0x41]);
|
|
249
261
|
const contentType = this.options.exactRawBodyByteContentType ?? 'text/plain; charset=latin1';
|
|
250
262
|
const response = await fetch(`http://127.0.0.1:${String(port)}/webhooks/bytes`, {
|
|
@@ -263,9 +275,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
263
275
|
})) {
|
|
264
276
|
throw new Error(`${this.options.name} adapter changed exact-byte rawBody semantics.`);
|
|
265
277
|
}
|
|
266
|
-
}
|
|
267
|
-
await closeSilently(app);
|
|
268
|
-
}
|
|
278
|
+
});
|
|
269
279
|
}
|
|
270
280
|
async assertExcludesRawBodyForMultipart() {
|
|
271
281
|
let _initProto4, _initClass4;
|
|
@@ -302,7 +312,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
302
312
|
rawBody: true
|
|
303
313
|
});
|
|
304
314
|
await app.listen();
|
|
305
|
-
|
|
315
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
306
316
|
const form = new FormData();
|
|
307
317
|
form.set('name', 'Ada');
|
|
308
318
|
form.set('payload', new Blob(['hello'], {
|
|
@@ -325,9 +335,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
325
335
|
})) {
|
|
326
336
|
throw new Error(`${this.options.name} adapter changed multipart rawBody semantics.`);
|
|
327
337
|
}
|
|
328
|
-
}
|
|
329
|
-
await closeSilently(app);
|
|
330
|
-
}
|
|
338
|
+
});
|
|
331
339
|
}
|
|
332
340
|
async assertDefaultsMultipartTotalLimitToMaxBodySize() {
|
|
333
341
|
let _initProto5, _initClass5;
|
|
@@ -366,7 +374,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
366
374
|
port
|
|
367
375
|
});
|
|
368
376
|
await app.listen();
|
|
369
|
-
|
|
377
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
370
378
|
const form = new FormData();
|
|
371
379
|
form.set('name', 'Ada');
|
|
372
380
|
form.set('payload', new Blob(['12345678'], {
|
|
@@ -383,9 +391,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
383
391
|
if (typeof body !== 'object' || body === null || body.error?.code !== 'PAYLOAD_TOO_LARGE') {
|
|
384
392
|
throw new Error(`${this.options.name} adapter changed multipart limit error semantics.`);
|
|
385
393
|
}
|
|
386
|
-
}
|
|
387
|
-
await closeSilently(app);
|
|
388
|
-
}
|
|
394
|
+
});
|
|
389
395
|
}
|
|
390
396
|
async assertSupportsSseStreaming() {
|
|
391
397
|
let _initProto6, _initClass6;
|
|
@@ -428,7 +434,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
428
434
|
port
|
|
429
435
|
});
|
|
430
436
|
await app.listen();
|
|
431
|
-
|
|
437
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
432
438
|
const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
|
|
433
439
|
headers: {
|
|
434
440
|
accept: 'text/event-stream'
|
|
@@ -445,9 +451,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
445
451
|
if (!body.includes('event: ready') || !body.includes('data: {"ready":true}')) {
|
|
446
452
|
throw new Error(`${this.options.name} adapter changed SSE body framing.`);
|
|
447
453
|
}
|
|
448
|
-
}
|
|
449
|
-
await closeSilently(app);
|
|
450
|
-
}
|
|
454
|
+
});
|
|
451
455
|
}
|
|
452
456
|
|
|
453
457
|
/**
|
|
@@ -498,7 +502,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
498
502
|
port
|
|
499
503
|
});
|
|
500
504
|
await app.listen();
|
|
501
|
-
|
|
505
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
502
506
|
const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
|
|
503
507
|
headers: {
|
|
504
508
|
accept: 'text/event-stream'
|
|
@@ -509,9 +513,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
509
513
|
}
|
|
510
514
|
await response.text();
|
|
511
515
|
await withTimeout(drainWaitSettled, 2_000, `${this.options.name} adapter left response.stream.waitForDrain() pending after close.`);
|
|
512
|
-
}
|
|
513
|
-
await closeSilently(app);
|
|
514
|
-
}
|
|
516
|
+
});
|
|
515
517
|
}
|
|
516
518
|
async assertReportsConfiguredHostInStartupLogs() {
|
|
517
519
|
let _initProto8, _initClass8;
|
|
@@ -557,7 +559,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
557
559
|
logger,
|
|
558
560
|
port
|
|
559
561
|
});
|
|
560
|
-
|
|
562
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
561
563
|
const response = await fetch(`http://127.0.0.1:${String(port)}/health`);
|
|
562
564
|
if (response.status !== 200) {
|
|
563
565
|
throw new Error(`${this.options.name} adapter changed host-bound health response semantics.`);
|
|
@@ -572,9 +574,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
572
574
|
if (!loggerEvents.includes(expectedLog)) {
|
|
573
575
|
throw new Error(`${this.options.name} adapter changed startup host logging.`);
|
|
574
576
|
}
|
|
575
|
-
}
|
|
576
|
-
await closeSilently(app);
|
|
577
|
-
}
|
|
577
|
+
});
|
|
578
578
|
}
|
|
579
579
|
async assertReportsHttpsStartupUrl(https) {
|
|
580
580
|
let _initProto9, _initClass9;
|
|
@@ -621,7 +621,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
621
621
|
logger,
|
|
622
622
|
port
|
|
623
623
|
});
|
|
624
|
-
|
|
624
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
625
625
|
const response = await requestHttps(`https://127.0.0.1:${String(port)}/health`);
|
|
626
626
|
if (response.statusCode !== 200) {
|
|
627
627
|
throw new Error(`${this.options.name} adapter changed HTTPS response status semantics.`);
|
|
@@ -635,9 +635,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
635
635
|
if (!loggerEvents.includes(expectedLog)) {
|
|
636
636
|
throw new Error(`${this.options.name} adapter changed HTTPS startup logging.`);
|
|
637
637
|
}
|
|
638
|
-
}
|
|
639
|
-
await closeSilently(app);
|
|
640
|
-
}
|
|
638
|
+
});
|
|
641
639
|
}
|
|
642
640
|
async assertRemovesShutdownSignalListenersAfterClose() {
|
|
643
641
|
let _initProto0, _initClass0;
|
|
@@ -681,13 +679,11 @@ export class HttpAdapterPortabilityHarness {
|
|
|
681
679
|
shutdownSignals: [signal]
|
|
682
680
|
});
|
|
683
681
|
const registeredListeners = process.listeners(signal).filter(listener => !listenersBefore.has(listener));
|
|
684
|
-
|
|
682
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
685
683
|
if (registeredListeners.length === 0) {
|
|
686
684
|
throw new Error(`${this.options.name} adapter did not register the expected shutdown listener.`);
|
|
687
685
|
}
|
|
688
|
-
}
|
|
689
|
-
await closeSilently(app);
|
|
690
|
-
}
|
|
686
|
+
});
|
|
691
687
|
const remainingListeners = process.listeners(signal);
|
|
692
688
|
const leakedListeners = registeredListeners.filter(listener => remainingListeners.includes(listener));
|
|
693
689
|
if (leakedListeners.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-runtime-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/web-runtime-adapter-portability.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"web-runtime-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/web-runtime-adapter-portability.ts"],"names":[],"mappings":"AACA,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;;GAEG;AACH,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;AA0CD;;GAEG;AACH,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,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC;IAyCtD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAgDrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAsErD,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDlD,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;CA8ClD;AAED;;;;;GAKG;AACH,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"}
|
|
@@ -3,9 +3,7 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
3
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
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
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
|
-
// @ts-ignore Worktree-local LSP does not resolve workspace package aliases.
|
|
7
6
|
import { Controller, Get, Post, SseResponse } from '@fluojs/http';
|
|
8
|
-
// @ts-ignore Worktree-local LSP does not resolve workspace package aliases.
|
|
9
7
|
import { defineModule } from '@fluojs/runtime';
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -15,10 +13,26 @@ import { defineModule } from '@fluojs/runtime';
|
|
|
15
13
|
function decodeUtf8(input) {
|
|
16
14
|
return new TextDecoder().decode(input ?? new Uint8Array());
|
|
17
15
|
}
|
|
18
|
-
async function
|
|
16
|
+
async function runWithCleanup(app, adapterName, assertion) {
|
|
17
|
+
let hasAssertionError = false;
|
|
18
|
+
let assertionError;
|
|
19
|
+
try {
|
|
20
|
+
await assertion();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
hasAssertionError = true;
|
|
23
|
+
assertionError = error;
|
|
24
|
+
}
|
|
19
25
|
try {
|
|
20
26
|
await app.close();
|
|
21
|
-
} catch {
|
|
27
|
+
} catch (cleanupError) {
|
|
28
|
+
if (hasAssertionError) {
|
|
29
|
+
throw new AggregateError([assertionError, cleanupError], `${adapterName} adapter portability assertion failed and app.close() also failed during harness cleanup.`);
|
|
30
|
+
}
|
|
31
|
+
throw new AggregateError([cleanupError], `${adapterName} adapter app.close() failed during portability harness cleanup.`);
|
|
32
|
+
}
|
|
33
|
+
if (hasAssertionError) {
|
|
34
|
+
throw assertionError;
|
|
35
|
+
}
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
/**
|
|
@@ -55,7 +69,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
55
69
|
const app = await this.options.bootstrap(AppModule, {
|
|
56
70
|
cors: false
|
|
57
71
|
});
|
|
58
|
-
|
|
72
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
59
73
|
const response = await app.dispatch(new Request('https://runtime.test/query?tag=one&tag=two&encoded=hello+world&flag&bad=%E0%A4%A'));
|
|
60
74
|
if (response.status !== 200) {
|
|
61
75
|
throw new Error(`${this.options.name} adapter changed query response status semantics.`);
|
|
@@ -64,9 +78,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
64
78
|
if (typeof body !== 'object' || body === null || body.bad !== '�%A' || body.encoded !== 'hello world' || !Array.isArray(body.tag) || JSON.stringify(body.tag) !== JSON.stringify(['one', 'two'])) {
|
|
65
79
|
throw new Error(`${this.options.name} adapter changed query decoding semantics.`);
|
|
66
80
|
}
|
|
67
|
-
}
|
|
68
|
-
await closeSilently(app);
|
|
69
|
-
}
|
|
81
|
+
});
|
|
70
82
|
}
|
|
71
83
|
async assertPreservesMalformedCookieValues() {
|
|
72
84
|
let _initProto2, _initClass2;
|
|
@@ -95,7 +107,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
95
107
|
const app = await this.options.bootstrap(AppModule, {
|
|
96
108
|
cors: false
|
|
97
109
|
});
|
|
98
|
-
|
|
110
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
99
111
|
const response = await app.dispatch(new Request('https://runtime.test/cookies', {
|
|
100
112
|
headers: {
|
|
101
113
|
cookie: 'good=hello%20world; bad=%E0%A4%A'
|
|
@@ -108,9 +120,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
108
120
|
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) {
|
|
109
121
|
throw new Error(`${this.options.name} adapter changed malformed-cookie normalization.`);
|
|
110
122
|
}
|
|
111
|
-
}
|
|
112
|
-
await closeSilently(app);
|
|
113
|
-
}
|
|
123
|
+
});
|
|
114
124
|
}
|
|
115
125
|
async assertPreservesRawBodyForJsonAndText() {
|
|
116
126
|
let _initProto3, _initClass3;
|
|
@@ -149,7 +159,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
149
159
|
cors: false,
|
|
150
160
|
rawBody: true
|
|
151
161
|
});
|
|
152
|
-
|
|
162
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
153
163
|
const [jsonResponse, textResponse] = await Promise.all([app.dispatch(new Request('https://runtime.test/webhooks/json', {
|
|
154
164
|
body: JSON.stringify({
|
|
155
165
|
provider: 'stripe'
|
|
@@ -183,9 +193,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
183
193
|
})) {
|
|
184
194
|
throw new Error(`${this.options.name} adapter changed text rawBody semantics.`);
|
|
185
195
|
}
|
|
186
|
-
}
|
|
187
|
-
await closeSilently(app);
|
|
188
|
-
}
|
|
196
|
+
});
|
|
189
197
|
}
|
|
190
198
|
async assertExcludesRawBodyForMultipart() {
|
|
191
199
|
let _initProto4, _initClass4;
|
|
@@ -219,7 +227,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
219
227
|
cors: false,
|
|
220
228
|
rawBody: true
|
|
221
229
|
});
|
|
222
|
-
|
|
230
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
223
231
|
const form = new FormData();
|
|
224
232
|
form.set('name', 'Ada');
|
|
225
233
|
form.set('payload', new Blob(['hello'], {
|
|
@@ -242,9 +250,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
242
250
|
})) {
|
|
243
251
|
throw new Error(`${this.options.name} adapter changed multipart rawBody semantics.`);
|
|
244
252
|
}
|
|
245
|
-
}
|
|
246
|
-
await closeSilently(app);
|
|
247
|
-
}
|
|
253
|
+
});
|
|
248
254
|
}
|
|
249
255
|
async assertSupportsSseStreaming() {
|
|
250
256
|
let _initProto5, _initClass5;
|
|
@@ -282,7 +288,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
282
288
|
const app = await this.options.bootstrap(AppModule, {
|
|
283
289
|
cors: false
|
|
284
290
|
});
|
|
285
|
-
|
|
291
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
286
292
|
const response = await app.dispatch(new Request('https://runtime.test/events', {
|
|
287
293
|
headers: {
|
|
288
294
|
accept: 'text/event-stream'
|
|
@@ -299,9 +305,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
299
305
|
if (!body.includes('event: ready') || !body.includes('data: {"ready":true}')) {
|
|
300
306
|
throw new Error(`${this.options.name} adapter changed SSE body framing.`);
|
|
301
307
|
}
|
|
302
|
-
}
|
|
303
|
-
await closeSilently(app);
|
|
304
|
-
}
|
|
308
|
+
});
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"override",
|
|
10
10
|
"module-builder"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.0
|
|
12
|
+
"version": "1.0.0",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -76,11 +76,11 @@
|
|
|
76
76
|
"dist"
|
|
77
77
|
],
|
|
78
78
|
"dependencies": {
|
|
79
|
-
"@fluojs/
|
|
80
|
-
"@fluojs/
|
|
81
|
-
"@fluojs/
|
|
82
|
-
"@fluojs/
|
|
83
|
-
"@fluojs/
|
|
79
|
+
"@fluojs/config": "^1.0.0",
|
|
80
|
+
"@fluojs/core": "^1.0.0",
|
|
81
|
+
"@fluojs/http": "^1.0.0",
|
|
82
|
+
"@fluojs/di": "^1.0.0",
|
|
83
|
+
"@fluojs/runtime": "^1.0.0"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"@babel/core": ">=7.0.0",
|
|
@@ -88,9 +88,12 @@
|
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"vitest": "^3.2.4",
|
|
91
|
-
"@fluojs/platform-
|
|
92
|
-
"@fluojs/platform-
|
|
93
|
-
"@fluojs/platform-
|
|
91
|
+
"@fluojs/platform-bun": "^1.0.0",
|
|
92
|
+
"@fluojs/platform-cloudflare-workers": "^1.0.0",
|
|
93
|
+
"@fluojs/platform-deno": "^1.0.0",
|
|
94
|
+
"@fluojs/platform-nodejs": "^1.0.0",
|
|
95
|
+
"@fluojs/platform-express": "^1.0.0",
|
|
96
|
+
"@fluojs/platform-fastify": "^1.0.0"
|
|
94
97
|
},
|
|
95
98
|
"scripts": {
|
|
96
99
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|