@fluojs/testing 1.0.0 → 1.0.2
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 +5 -3
- package/README.md +5 -3
- package/dist/conformance/platform-conformance.d.ts.map +1 -1
- package/dist/conformance/platform-conformance.js +42 -23
- package/dist/portability/http-adapter-portability.d.ts.map +1 -1
- package/dist/portability/http-adapter-portability.js +22 -8
- package/dist/portability/web-runtime-adapter-portability.d.ts +4 -0
- package/dist/portability/web-runtime-adapter-portability.d.ts.map +1 -1
- package/dist/portability/web-runtime-adapter-portability.js +64 -10
- package/package.json +11 -11
package/README.ko.md
CHANGED
|
@@ -23,7 +23,7 @@ fluo 애플리케이션을 위한 기본 request-level 테스트 헬퍼, 모듈
|
|
|
23
23
|
pnpm add -D @fluojs/testing vitest
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`vitest`는 mock 헬퍼와 `@fluojs/testing/vitest` 엔트리포인트가 요구하는 peer dependency입니다.
|
|
26
|
+
`vitest`는 mock 헬퍼와 `@fluojs/testing/vitest` 엔트리포인트가 요구하는 peer dependency입니다. `@babel/core`는 Vitest decorators plugin이 사용하는 워크스페이스의 Babel을 로드하기 때문에 peer로 선언되어 있습니다. 따라서 non-Vitest harness subpath만 사용하더라도 패키지 매니저가 해당 peer 경고를 표시할 수 있습니다.
|
|
27
27
|
|
|
28
28
|
`@fluojs/testing/vitest`를 사용할 때는 `fluoBabelDecoratorsPlugin()`이 런타임에 Babel을 호출하므로, 사용하는 워크스페이스에 `@babel/core`도 함께 설치해야 합니다. Vitest 플러그인은 Vite query/hash suffix를 제거한 뒤 `.ts`, `.tsx`, `.mts`, `.cts` 소스 id를 변환하고, `node_modules`는 건너뛰며, 가장 가까운 root Babel config인 `babel.config.cjs`, `babel.config.mjs`, `babel.config.js`, `babel.config.json`을 해석합니다.
|
|
29
29
|
|
|
@@ -134,9 +134,11 @@ 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이 이미 실패한 경우에는
|
|
137
|
+
이식성 하니스의 cleanup도 계약에 포함됩니다. 앱이 bootstrap된 뒤 setup, `listen()`, assertion이 실패하면 하니스는 해당 partial app을 닫습니다. `app.close()`가 실패하면 하니스는 cleanup 실패를 보고하며, setup 또는 assertion이 이미 실패한 경우에는 원래 실패와 cleanup 실패를 모두 보존하는 aggregate error를 발생시킵니다.
|
|
138
138
|
|
|
139
|
-
`HttpAdapterPortabilityHarness
|
|
139
|
+
`HttpAdapterPortabilityHarness`와 web-runtime portability harness 메서드는 공개 어댑터 계약 체크입니다. 직접 같은 검증을 다시 만들기보다 `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, `assertRemovesShutdownSignalListenersAfterClose()`처럼 초점이 분명한 assertion을 사용하세요.
|
|
140
|
+
|
|
141
|
+
HTTP 어댑터가 런타임 전반에서 `rawBody`의 byte-sensitive payload byte를 그대로 보존하는지 증명해야 할 때는 `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`를 사용하세요.
|
|
140
142
|
|
|
141
143
|
## canonical TDD ladder
|
|
142
144
|
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Default request-level testing helpers, testing module construction, and provider
|
|
|
21
21
|
npm install --save-dev @fluojs/testing vitest
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
`vitest` is a required peer dependency for the mock helpers and the `@fluojs/testing/vitest` entrypoint.
|
|
24
|
+
`vitest` is a required peer dependency for the mock helpers and the `@fluojs/testing/vitest` entrypoint. `@babel/core` is declared as a peer because the Vitest decorators plugin loads Babel from the consuming workspace; package managers may surface that peer even when you only use the non-Vitest harness subpaths.
|
|
25
25
|
|
|
26
26
|
If you use `@fluojs/testing/vitest`, install `@babel/core` in the consuming workspace as well because `fluoBabelDecoratorsPlugin()` invokes Babel at runtime. The Vitest plugin transforms `.ts`, `.tsx`, `.mts`, and `.cts` source ids after removing Vite query/hash suffixes, skips `node_modules`, and resolves the nearest root Babel config named `babel.config.cjs`, `babel.config.mjs`, `babel.config.js`, or `babel.config.json`:
|
|
27
27
|
|
|
@@ -132,9 +132,11 @@ 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
|
|
135
|
+
Portability harness cleanup is part of the contract: if setup, `listen()`, or an assertion fails after an app has been bootstrapped, the harness closes that partial app. If `app.close()` fails, the harness reports that cleanup failure, and when setup or an assertion already failed it raises an aggregate error that preserves both the original failure and the cleanup failure.
|
|
136
136
|
|
|
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.
|
|
137
|
+
`HttpAdapterPortabilityHarness` and web-runtime portability harness 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.
|
|
138
|
+
|
|
139
|
+
Use `assertPreservesExactRawBodyBytesForByteSensitivePayloads()` when an HTTP adapter must prove `rawBody` keeps byte-sensitive payload bytes intact across runtimes.
|
|
138
140
|
|
|
139
141
|
## Canonical TDD Ladder
|
|
140
142
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-conformance.d.ts","sourceRoot":"","sources":["../../src/conformance/platform-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACzB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,UAAU,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,qCAAqC;IACpD,OAAO,CAAC,EAAE,CACR,SAAS,EAAE,iBAAiB,EAC5B,UAAU,EAAE,wBAAwB,KACjC,YAAY,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAC;IACtD,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2BAA2B,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,kCAAkC;IACjD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IACrD,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD,4BAA4B,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,WAAW,CAAC,EAAE,qCAAqC,CAAC;IACpD,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,2BAA2B,CAAC;QACtC,MAAM,EAAE,2BAA2B,CAAC;KACrC,CAAC;IACF,QAAQ,CAAC,EAAE,kCAAkC,CAAC;CAC/C;
|
|
1
|
+
{"version":3,"file":"platform-conformance.d.ts","sourceRoot":"","sources":["../../src/conformance/platform-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACzB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,UAAU,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,qCAAqC;IACpD,OAAO,CAAC,EAAE,CACR,SAAS,EAAE,iBAAiB,EAC5B,UAAU,EAAE,wBAAwB,KACjC,YAAY,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAC;IACtD,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2BAA2B,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,kCAAkC;IACjD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IACrD,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD,4BAA4B,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,WAAW,CAAC,EAAE,qCAAqC,CAAC;IACpD,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,2BAA2B,CAAC;QACtC,MAAM,EAAE,2BAA2B,CAAC;KACrC,CAAC;IACF,QAAQ,CAAC,EAAE,kCAAkC,CAAC;CAC/C;AAmGD;;GAEG;AACH,qBAAa,0BAA0B;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iCAAiC;IAEjE,yCAAyC,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B1D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B3C,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCvC,2CAA2C,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC5D,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CxC,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAQjC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
|
|
@@ -48,6 +48,19 @@ async function captureOutcome(action) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
async function runCleanupWithAssertionContext(cleanup, cleanupLabel, assertionError) {
|
|
52
|
+
try {
|
|
53
|
+
await cleanup();
|
|
54
|
+
} catch (cleanupError) {
|
|
55
|
+
if (assertionError !== undefined) {
|
|
56
|
+
throw new AggregateError([assertionError, cleanupError], `${cleanupLabel} failed while preserving the original conformance assertion failure.`);
|
|
57
|
+
}
|
|
58
|
+
throw new AggregateError([cleanupError], `${cleanupLabel} failed during platform conformance cleanup.`);
|
|
59
|
+
}
|
|
60
|
+
if (assertionError !== undefined) {
|
|
61
|
+
throw assertionError;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
51
64
|
function collectForbiddenKeyPaths(value, patterns, allowPatterns, currentPath, violations) {
|
|
52
65
|
if (Array.isArray(value)) {
|
|
53
66
|
value.forEach((entry, index) => {
|
|
@@ -97,20 +110,25 @@ export class PlatformConformanceHarness {
|
|
|
97
110
|
async assertStartIsDeterministic() {
|
|
98
111
|
const component = this.options.createComponent();
|
|
99
112
|
const compare = this.options.snapshot?.compare ?? defaultCompare;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
let assertionError;
|
|
114
|
+
try {
|
|
115
|
+
const firstStart = await captureOutcome(() => component.start());
|
|
116
|
+
const firstSnapshot = component.snapshot();
|
|
117
|
+
const secondStart = await captureOutcome(() => component.start());
|
|
118
|
+
const secondSnapshot = component.snapshot();
|
|
119
|
+
if (firstStart.ok !== secondStart.ok) {
|
|
120
|
+
throw new Error('start() is not deterministic: first and second calls had different outcomes.');
|
|
121
|
+
}
|
|
122
|
+
if (!firstStart.ok && !secondStart.ok && firstStart.message !== secondStart.message) {
|
|
123
|
+
throw new Error('start() rejection messages changed across duplicate calls.');
|
|
124
|
+
}
|
|
125
|
+
if (firstStart.ok && secondStart.ok && !compare(firstSnapshot, secondSnapshot)) {
|
|
126
|
+
throw new Error('start() is not idempotent: duplicate calls changed component snapshot output.');
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
assertionError = error;
|
|
112
130
|
}
|
|
113
|
-
await
|
|
131
|
+
await runCleanupWithAssertionContext(() => component.stop(), 'stop() after start() determinism check', assertionError);
|
|
114
132
|
}
|
|
115
133
|
async assertStopIsIdempotent() {
|
|
116
134
|
const component = this.options.createComponent();
|
|
@@ -145,17 +163,17 @@ export class PlatformConformanceHarness {
|
|
|
145
163
|
}
|
|
146
164
|
for (const scenario of [scenarios.degraded, scenarios.failed]) {
|
|
147
165
|
const component = scenario.createComponent();
|
|
148
|
-
await scenario.enterState(component);
|
|
149
|
-
if (scenario.expectedState !== undefined && component.state() !== scenario.expectedState) {
|
|
150
|
-
throw new Error(`Scenario "${scenario.name}" expected state "${scenario.expectedState}" but received "${component.state()}".`);
|
|
151
|
-
}
|
|
152
166
|
try {
|
|
167
|
+
await scenario.enterState(component);
|
|
168
|
+
if (scenario.expectedState !== undefined && component.state() !== scenario.expectedState) {
|
|
169
|
+
throw new Error(`Scenario "${scenario.name}" expected state "${scenario.expectedState}" but received "${component.state()}".`);
|
|
170
|
+
}
|
|
153
171
|
component.snapshot();
|
|
154
172
|
} catch (error) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await captureOutcome(() => component.stop());
|
|
173
|
+
const assertionError = error instanceof Error && error.message.startsWith('Scenario ') ? error : new Error(`snapshot() must be safe in "${scenario.name}" state: ${toErrorMessage(error)}`);
|
|
174
|
+
await runCleanupWithAssertionContext(() => component.stop(), `stop() after "${scenario.name}" snapshot scenario`, assertionError);
|
|
158
175
|
}
|
|
176
|
+
await runCleanupWithAssertionContext(() => component.stop(), `stop() after "${scenario.name}" snapshot scenario`);
|
|
159
177
|
}
|
|
160
178
|
}
|
|
161
179
|
async assertStableDiagnostics() {
|
|
@@ -169,10 +187,10 @@ export class PlatformConformanceHarness {
|
|
|
169
187
|
const requiredFixHintSeverities = this.options.diagnostics?.requireFixHintForSeverities ?? DEFAULT_REQUIRED_FIX_HINT_SEVERITIES;
|
|
170
188
|
for (const issue of diagnostics) {
|
|
171
189
|
if (issue.code.trim().length === 0) {
|
|
172
|
-
throw new Error(
|
|
190
|
+
throw new Error(`Diagnostics must provide a stable non-empty code. Received message: ${issue.message}`);
|
|
173
191
|
}
|
|
174
192
|
if (requiredFixHintSeverities.includes(issue.severity) && (!issue.fixHint || issue.fixHint.trim().length === 0)) {
|
|
175
|
-
throw new Error(`Diagnostic ${issue.code} (${issue.severity}) must provide a fixHint
|
|
193
|
+
throw new Error(`Diagnostic ${issue.code} (${issue.severity}) must provide a fixHint. Message: ${issue.message}`);
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
196
|
const expectedCodes = this.options.diagnostics?.expectedCodes;
|
|
@@ -183,7 +201,8 @@ export class PlatformConformanceHarness {
|
|
|
183
201
|
const actualCodes = normalizeCodes(diagnostics.map(diagnostic => diagnostic.code));
|
|
184
202
|
const normalizedExpectedCodes = normalizeCodes(expectedCodes);
|
|
185
203
|
if (!defaultCompare(actualCodes, normalizedExpectedCodes)) {
|
|
186
|
-
|
|
204
|
+
const actualDetails = diagnostics.map(diagnostic => `${diagnostic.code}(${diagnostic.severity}): ${diagnostic.message}`).join('; ');
|
|
205
|
+
throw new Error(`Diagnostic code set changed. Expected [${normalizedExpectedCodes.join(', ')}] but received [${actualCodes.join(', ')}]. Actual diagnostics: ${actualDetails}`);
|
|
187
206
|
}
|
|
188
207
|
}
|
|
189
208
|
async assertSnapshotSanitized() {
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAuGD;;;;;;;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"}
|
|
@@ -78,6 +78,19 @@ async function runWithCleanup(app, adapterName, assertion) {
|
|
|
78
78
|
throw assertionError;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
async function prepareAndListenWithCleanup(app, adapterName, prepare) {
|
|
82
|
+
try {
|
|
83
|
+
await prepare?.();
|
|
84
|
+
await app.listen();
|
|
85
|
+
} catch (setupError) {
|
|
86
|
+
try {
|
|
87
|
+
await app.close();
|
|
88
|
+
} catch (cleanupError) {
|
|
89
|
+
throw new AggregateError([setupError, cleanupError], `${adapterName} adapter portability setup failed and app.close() also failed during harness cleanup.`);
|
|
90
|
+
}
|
|
91
|
+
throw setupError;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
81
94
|
|
|
82
95
|
/**
|
|
83
96
|
* A portability harness for testing HTTP adapters to ensure they behave
|
|
@@ -130,7 +143,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
130
143
|
cors: false,
|
|
131
144
|
port
|
|
132
145
|
});
|
|
133
|
-
await app.
|
|
146
|
+
await prepareAndListenWithCleanup(app, this.options.name);
|
|
134
147
|
await runWithCleanup(app, this.options.name, async () => {
|
|
135
148
|
const response = await fetch(`http://127.0.0.1:${String(port)}/cookies`, {
|
|
136
149
|
headers: {
|
|
@@ -185,7 +198,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
185
198
|
port,
|
|
186
199
|
rawBody: true
|
|
187
200
|
});
|
|
188
|
-
await app.
|
|
201
|
+
await prepareAndListenWithCleanup(app, this.options.name);
|
|
189
202
|
await runWithCleanup(app, this.options.name, async () => {
|
|
190
203
|
const [jsonResponse, textResponse] = await Promise.all([fetch(`http://127.0.0.1:${String(port)}/webhooks/json`, {
|
|
191
204
|
body: JSON.stringify({
|
|
@@ -254,8 +267,9 @@ export class HttpAdapterPortabilityHarness {
|
|
|
254
267
|
port,
|
|
255
268
|
rawBody: true
|
|
256
269
|
});
|
|
257
|
-
await this.options.
|
|
258
|
-
|
|
270
|
+
await prepareAndListenWithCleanup(app, this.options.name, async () => {
|
|
271
|
+
await this.options.prepareExactRawBodyByteTest?.(app);
|
|
272
|
+
});
|
|
259
273
|
await runWithCleanup(app, this.options.name, async () => {
|
|
260
274
|
const payload = Uint8Array.from([0xe9, 0x41]);
|
|
261
275
|
const contentType = this.options.exactRawBodyByteContentType ?? 'text/plain; charset=latin1';
|
|
@@ -311,7 +325,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
311
325
|
port,
|
|
312
326
|
rawBody: true
|
|
313
327
|
});
|
|
314
|
-
await app.
|
|
328
|
+
await prepareAndListenWithCleanup(app, this.options.name);
|
|
315
329
|
await runWithCleanup(app, this.options.name, async () => {
|
|
316
330
|
const form = new FormData();
|
|
317
331
|
form.set('name', 'Ada');
|
|
@@ -373,7 +387,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
373
387
|
},
|
|
374
388
|
port
|
|
375
389
|
});
|
|
376
|
-
await app.
|
|
390
|
+
await prepareAndListenWithCleanup(app, this.options.name);
|
|
377
391
|
await runWithCleanup(app, this.options.name, async () => {
|
|
378
392
|
const form = new FormData();
|
|
379
393
|
form.set('name', 'Ada');
|
|
@@ -433,7 +447,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
433
447
|
cors: false,
|
|
434
448
|
port
|
|
435
449
|
});
|
|
436
|
-
await app.
|
|
450
|
+
await prepareAndListenWithCleanup(app, this.options.name);
|
|
437
451
|
await runWithCleanup(app, this.options.name, async () => {
|
|
438
452
|
const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
|
|
439
453
|
headers: {
|
|
@@ -501,7 +515,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
501
515
|
cors: false,
|
|
502
516
|
port
|
|
503
517
|
});
|
|
504
|
-
await app.
|
|
518
|
+
await prepareAndListenWithCleanup(app, this.options.name);
|
|
505
519
|
await runWithCleanup(app, this.options.name, async () => {
|
|
506
520
|
const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
|
|
507
521
|
headers: {
|
|
@@ -25,6 +25,10 @@ export declare class WebRuntimeHttpAdapterPortabilityHarness<TBootstrapOptions e
|
|
|
25
25
|
assertPreservesQueryArraysAndDecoding(): Promise<void>;
|
|
26
26
|
assertPreservesMalformedCookieValues(): Promise<void>;
|
|
27
27
|
assertPreservesRawBodyForJsonAndText(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Asserts that byte-sensitive request bodies preserve their exact raw bytes.
|
|
30
|
+
*/
|
|
31
|
+
assertPreservesExactRawBodyBytesForByteSensitivePayloads(): Promise<void>;
|
|
28
32
|
assertExcludesRawBodyForMultipart(): Promise<void>;
|
|
29
33
|
assertSupportsSseStreaming(): Promise<void>;
|
|
30
34
|
}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;IAsE3D;;OAEG;IACG,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA0CzE,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"}
|
|
@@ -195,18 +195,72 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
|
-
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Asserts that byte-sensitive request bodies preserve their exact raw bytes.
|
|
201
|
+
*/
|
|
202
|
+
async assertPreservesExactRawBodyBytesForByteSensitivePayloads() {
|
|
199
203
|
let _initProto4, _initClass4;
|
|
204
|
+
let _WebhookController2;
|
|
205
|
+
class WebhookController {
|
|
206
|
+
static {
|
|
207
|
+
({
|
|
208
|
+
e: [_initProto4],
|
|
209
|
+
c: [_WebhookController2, _initClass4]
|
|
210
|
+
} = _applyDecs(this, [Controller('/webhooks')], [[Post('/bytes'), 2, "handleBytes"]]));
|
|
211
|
+
}
|
|
212
|
+
constructor() {
|
|
213
|
+
_initProto4(this);
|
|
214
|
+
}
|
|
215
|
+
handleBytes(_input, context) {
|
|
216
|
+
return {
|
|
217
|
+
rawBytes: Array.from(context.request.rawBody ?? new Uint8Array())
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
static {
|
|
221
|
+
_initClass4();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
class AppModule {}
|
|
225
|
+
defineModule(AppModule, {
|
|
226
|
+
controllers: [_WebhookController2]
|
|
227
|
+
});
|
|
228
|
+
const app = await this.options.bootstrap(AppModule, {
|
|
229
|
+
cors: false,
|
|
230
|
+
rawBody: true
|
|
231
|
+
});
|
|
232
|
+
await runWithCleanup(app, this.options.name, async () => {
|
|
233
|
+
const payload = Uint8Array.from([0xe9, 0x41]);
|
|
234
|
+
const response = await app.dispatch(new Request('https://runtime.test/webhooks/bytes', {
|
|
235
|
+
body: payload,
|
|
236
|
+
headers: {
|
|
237
|
+
'content-type': 'application/octet-stream'
|
|
238
|
+
},
|
|
239
|
+
method: 'POST'
|
|
240
|
+
}));
|
|
241
|
+
if (response.status !== 201) {
|
|
242
|
+
throw new Error(`${this.options.name} adapter changed byte-sensitive rawBody response status semantics.`);
|
|
243
|
+
}
|
|
244
|
+
const body = await response.json();
|
|
245
|
+
if (JSON.stringify(body) !== JSON.stringify({
|
|
246
|
+
rawBytes: Array.from(payload)
|
|
247
|
+
})) {
|
|
248
|
+
throw new Error(`${this.options.name} adapter changed exact-byte rawBody semantics.`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async assertExcludesRawBodyForMultipart() {
|
|
253
|
+
let _initProto5, _initClass5;
|
|
200
254
|
let _UploadController;
|
|
201
255
|
class UploadController {
|
|
202
256
|
static {
|
|
203
257
|
({
|
|
204
|
-
e: [
|
|
205
|
-
c: [_UploadController,
|
|
258
|
+
e: [_initProto5],
|
|
259
|
+
c: [_UploadController, _initClass5]
|
|
206
260
|
} = _applyDecs(this, [Controller('/uploads')], [[Post('/'), 2, "upload"]]));
|
|
207
261
|
}
|
|
208
262
|
constructor() {
|
|
209
|
-
|
|
263
|
+
_initProto5(this);
|
|
210
264
|
}
|
|
211
265
|
upload(_input, context) {
|
|
212
266
|
return {
|
|
@@ -216,7 +270,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
216
270
|
};
|
|
217
271
|
}
|
|
218
272
|
static {
|
|
219
|
-
|
|
273
|
+
_initClass5();
|
|
220
274
|
}
|
|
221
275
|
}
|
|
222
276
|
class AppModule {}
|
|
@@ -253,17 +307,17 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
253
307
|
});
|
|
254
308
|
}
|
|
255
309
|
async assertSupportsSseStreaming() {
|
|
256
|
-
let
|
|
310
|
+
let _initProto6, _initClass6;
|
|
257
311
|
let _EventsController;
|
|
258
312
|
class EventsController {
|
|
259
313
|
static {
|
|
260
314
|
({
|
|
261
|
-
e: [
|
|
262
|
-
c: [_EventsController,
|
|
315
|
+
e: [_initProto6],
|
|
316
|
+
c: [_EventsController, _initClass6]
|
|
263
317
|
} = _applyDecs(this, [Controller('/events')], [[Get('/'), 2, "stream"]]));
|
|
264
318
|
}
|
|
265
319
|
constructor() {
|
|
266
|
-
|
|
320
|
+
_initProto6(this);
|
|
267
321
|
}
|
|
268
322
|
stream(_input, context) {
|
|
269
323
|
const stream = new SseResponse(context);
|
|
@@ -278,7 +332,7 @@ export class WebRuntimeHttpAdapterPortabilityHarness {
|
|
|
278
332
|
return stream;
|
|
279
333
|
}
|
|
280
334
|
static {
|
|
281
|
-
|
|
335
|
+
_initClass6();
|
|
282
336
|
}
|
|
283
337
|
}
|
|
284
338
|
class AppModule {}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"override",
|
|
10
10
|
"module-builder"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.2",
|
|
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/config": "^1.0.
|
|
80
|
-
"@fluojs/core": "^1.0.
|
|
79
|
+
"@fluojs/config": "^1.0.2",
|
|
80
|
+
"@fluojs/core": "^1.0.2",
|
|
81
81
|
"@fluojs/http": "^1.0.0",
|
|
82
|
-
"@fluojs/di": "^1.0.
|
|
83
|
-
"@fluojs/runtime": "^1.
|
|
82
|
+
"@fluojs/di": "^1.0.2",
|
|
83
|
+
"@fluojs/runtime": "^1.1.0"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"@babel/core": ">=7.0.0",
|
|
@@ -88,12 +88,12 @@
|
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"vitest": "^3.2.4",
|
|
91
|
-
"@fluojs/platform-bun": "^1.0.
|
|
92
|
-
"@fluojs/platform-cloudflare-workers": "^1.0.
|
|
93
|
-
"@fluojs/platform-deno": "^1.0.
|
|
94
|
-
"@fluojs/platform-nodejs": "^1.0.
|
|
95
|
-
"@fluojs/platform-express": "^1.0.
|
|
96
|
-
"@fluojs/platform-fastify": "^1.0.
|
|
91
|
+
"@fluojs/platform-bun": "^1.0.2",
|
|
92
|
+
"@fluojs/platform-cloudflare-workers": "^1.0.2",
|
|
93
|
+
"@fluojs/platform-deno": "^1.0.2",
|
|
94
|
+
"@fluojs/platform-nodejs": "^1.0.2",
|
|
95
|
+
"@fluojs/platform-express": "^1.0.2",
|
|
96
|
+
"@fluojs/platform-fastify": "^1.0.2"
|
|
97
97
|
},
|
|
98
98
|
"scripts": {
|
|
99
99
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|