@ricsam/isolate-fetch 0.1.1 → 0.1.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.md +93 -0
- package/dist/cjs/index.cjs +1800 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/cjs/stream-state.cjs +230 -0
- package/dist/cjs/stream-state.cjs.map +10 -0
- package/{src/index.ts → dist/mjs/index.mjs} +357 -923
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/mjs/stream-state.mjs +199 -0
- package/dist/mjs/stream-state.mjs.map +10 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/isolate.d.ts +267 -0
- package/dist/types/stream-state.d.ts +61 -0
- package/package.json +41 -13
- package/CHANGELOG.md +0 -9
- package/src/debug-delayed.test.ts +0 -89
- package/src/debug-streaming.test.ts +0 -81
- package/src/download-streaming-simple.test.ts +0 -167
- package/src/download-streaming.test.ts +0 -286
- package/src/form-data.test.ts +0 -824
- package/src/formdata.test.ts +0 -212
- package/src/headers.test.ts +0 -582
- package/src/host-backed-stream.test.ts +0 -363
- package/src/index.test.ts +0 -274
- package/src/integration.test.ts +0 -665
- package/src/request.test.ts +0 -482
- package/src/response.test.ts +0 -520
- package/src/serve.test.ts +0 -425
- package/src/stream-state.test.ts +0 -338
- package/src/stream-state.ts +0 -337
- package/src/upload-streaming.test.ts +0 -373
- package/src/websocket.test.ts +0 -627
- package/tsconfig.json +0 -8
package/src/form-data.test.ts
DELETED
|
@@ -1,824 +0,0 @@
|
|
|
1
|
-
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import {
|
|
4
|
-
createFetchTestContext,
|
|
5
|
-
evalCode,
|
|
6
|
-
runTestCode,
|
|
7
|
-
type FetchTestContext,
|
|
8
|
-
} from "@ricsam/isolate-test-utils";
|
|
9
|
-
|
|
10
|
-
describe("FormData", () => {
|
|
11
|
-
let ctx: FetchTestContext;
|
|
12
|
-
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
ctx = await createFetchTestContext();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
ctx.dispose();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("append string value", () => {
|
|
22
|
-
const data = evalCode<string>(
|
|
23
|
-
ctx.context,
|
|
24
|
-
`
|
|
25
|
-
const formData = new FormData();
|
|
26
|
-
formData.append("name", "John Doe");
|
|
27
|
-
formData.append("email", "john@example.com");
|
|
28
|
-
|
|
29
|
-
JSON.stringify({
|
|
30
|
-
name: formData.get("name"),
|
|
31
|
-
email: formData.get("email"),
|
|
32
|
-
hasName: formData.has("name"),
|
|
33
|
-
})
|
|
34
|
-
`
|
|
35
|
-
);
|
|
36
|
-
const result = JSON.parse(data) as {
|
|
37
|
-
name: string;
|
|
38
|
-
email: string;
|
|
39
|
-
hasName: boolean;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
assert.strictEqual(result.name, "John Doe");
|
|
43
|
-
assert.strictEqual(result.email, "john@example.com");
|
|
44
|
-
assert.strictEqual(result.hasName, true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("forEach callback iterates entries", () => {
|
|
48
|
-
const data = evalCode<string>(
|
|
49
|
-
ctx.context,
|
|
50
|
-
`
|
|
51
|
-
const formData = new FormData();
|
|
52
|
-
formData.append("name", "John");
|
|
53
|
-
formData.append("age", "30");
|
|
54
|
-
formData.append("city", "NYC");
|
|
55
|
-
|
|
56
|
-
const entries = [];
|
|
57
|
-
formData.forEach((value, key) => {
|
|
58
|
-
entries.push({ key, value });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
JSON.stringify(entries)
|
|
62
|
-
`
|
|
63
|
-
);
|
|
64
|
-
const entries = JSON.parse(data) as { key: string; value: string }[];
|
|
65
|
-
|
|
66
|
-
assert.strictEqual(entries.length, 3);
|
|
67
|
-
assert.deepStrictEqual(
|
|
68
|
-
entries.map((e) => e.key),
|
|
69
|
-
["name", "age", "city"]
|
|
70
|
-
);
|
|
71
|
-
assert.deepStrictEqual(
|
|
72
|
-
entries.map((e) => e.value),
|
|
73
|
-
["John", "30", "NYC"]
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("FormData is iterable with for...of", () => {
|
|
78
|
-
const result = evalCode<string>(
|
|
79
|
-
ctx.context,
|
|
80
|
-
`
|
|
81
|
-
const formData = new FormData();
|
|
82
|
-
formData.append("name", "Alice");
|
|
83
|
-
formData.append("age", "30");
|
|
84
|
-
const entries = [];
|
|
85
|
-
for (const [key, value] of formData) {
|
|
86
|
-
entries.push([key, value]);
|
|
87
|
-
}
|
|
88
|
-
JSON.stringify(entries);
|
|
89
|
-
`
|
|
90
|
-
);
|
|
91
|
-
const entries = JSON.parse(result) as string[][];
|
|
92
|
-
|
|
93
|
-
assert.ok(entries.some(([k, v]) => k === "name" && v === "Alice"));
|
|
94
|
-
assert.ok(entries.some(([k, v]) => k === "age" && v === "30"));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("instanceof FormData returns true", () => {
|
|
98
|
-
const data = evalCode<string>(
|
|
99
|
-
ctx.context,
|
|
100
|
-
`
|
|
101
|
-
const formData = new FormData();
|
|
102
|
-
JSON.stringify({ instanceofFormData: formData instanceof FormData })
|
|
103
|
-
`
|
|
104
|
-
);
|
|
105
|
-
const result = JSON.parse(data) as { instanceofFormData: boolean };
|
|
106
|
-
assert.strictEqual(result.instanceofFormData, true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("constructor.name is 'FormData'", () => {
|
|
110
|
-
const data = evalCode<string>(
|
|
111
|
-
ctx.context,
|
|
112
|
-
`
|
|
113
|
-
const formData = new FormData();
|
|
114
|
-
JSON.stringify({ constructorName: formData.constructor.name })
|
|
115
|
-
`
|
|
116
|
-
);
|
|
117
|
-
const result = JSON.parse(data) as { constructorName: string };
|
|
118
|
-
assert.strictEqual(result.constructorName, "FormData");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("getAll returns all values for a key", () => {
|
|
122
|
-
const data = evalCode<string>(
|
|
123
|
-
ctx.context,
|
|
124
|
-
`
|
|
125
|
-
const formData = new FormData();
|
|
126
|
-
formData.append("tags", "javascript");
|
|
127
|
-
formData.append("tags", "typescript");
|
|
128
|
-
formData.append("tags", "node");
|
|
129
|
-
JSON.stringify({
|
|
130
|
-
first: formData.get("tags"),
|
|
131
|
-
all: formData.getAll("tags"),
|
|
132
|
-
})
|
|
133
|
-
`
|
|
134
|
-
);
|
|
135
|
-
const result = JSON.parse(data) as { first: string; all: string[] };
|
|
136
|
-
|
|
137
|
-
assert.strictEqual(result.first, "javascript");
|
|
138
|
-
assert.deepStrictEqual(result.all, ["javascript", "typescript", "node"]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("set replaces existing value", () => {
|
|
142
|
-
const data = evalCode<string>(
|
|
143
|
-
ctx.context,
|
|
144
|
-
`
|
|
145
|
-
const formData = new FormData();
|
|
146
|
-
formData.append("name", "John");
|
|
147
|
-
formData.append("name", "Jane");
|
|
148
|
-
formData.set("name", "Alice");
|
|
149
|
-
JSON.stringify({
|
|
150
|
-
name: formData.get("name"),
|
|
151
|
-
all: formData.getAll("name"),
|
|
152
|
-
})
|
|
153
|
-
`
|
|
154
|
-
);
|
|
155
|
-
const result = JSON.parse(data) as { name: string; all: string[] };
|
|
156
|
-
|
|
157
|
-
assert.strictEqual(result.name, "Alice");
|
|
158
|
-
assert.deepStrictEqual(result.all, ["Alice"]);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("delete removes all values for a key", () => {
|
|
162
|
-
const data = evalCode<string>(
|
|
163
|
-
ctx.context,
|
|
164
|
-
`
|
|
165
|
-
const formData = new FormData();
|
|
166
|
-
formData.append("name", "John");
|
|
167
|
-
formData.append("name", "Jane");
|
|
168
|
-
formData.delete("name");
|
|
169
|
-
JSON.stringify({
|
|
170
|
-
hasName: formData.has("name"),
|
|
171
|
-
name: formData.get("name"),
|
|
172
|
-
})
|
|
173
|
-
`
|
|
174
|
-
);
|
|
175
|
-
const result = JSON.parse(data) as { hasName: boolean; name: string | null };
|
|
176
|
-
|
|
177
|
-
assert.strictEqual(result.hasName, false);
|
|
178
|
-
assert.strictEqual(result.name, null);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("handles File objects", () => {
|
|
182
|
-
const data = evalCode<string>(
|
|
183
|
-
ctx.context,
|
|
184
|
-
`
|
|
185
|
-
const formData = new FormData();
|
|
186
|
-
const file = new File(['content'], 'test.txt', { type: 'text/plain' });
|
|
187
|
-
formData.append('file', file);
|
|
188
|
-
const retrieved = formData.get('file');
|
|
189
|
-
JSON.stringify({
|
|
190
|
-
isFile: retrieved instanceof File,
|
|
191
|
-
name: retrieved.name,
|
|
192
|
-
type: retrieved.type
|
|
193
|
-
})
|
|
194
|
-
`
|
|
195
|
-
);
|
|
196
|
-
const result = JSON.parse(data) as {
|
|
197
|
-
isFile: boolean;
|
|
198
|
-
name: string;
|
|
199
|
-
type: string;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
assert.strictEqual(result.isFile, true);
|
|
203
|
-
assert.strictEqual(result.name, "test.txt");
|
|
204
|
-
assert.strictEqual(result.type, "text/plain");
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Native FormData -> Isolate tests
|
|
210
|
-
*
|
|
211
|
-
* These tests verify that native FormData objects passed into the isolate
|
|
212
|
-
* behave identically to FormData instances created with `new FormData()` in the isolate.
|
|
213
|
-
*/
|
|
214
|
-
describe("Native FormData -> Isolate", () => {
|
|
215
|
-
let ctx: FetchTestContext;
|
|
216
|
-
|
|
217
|
-
beforeEach(async () => {
|
|
218
|
-
ctx = await createFetchTestContext();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
afterEach(() => {
|
|
222
|
-
ctx.dispose();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
test("native FormData should pass instanceof check in isolate", () => {
|
|
226
|
-
const nativeFormData = new FormData();
|
|
227
|
-
nativeFormData.append("name", "John");
|
|
228
|
-
|
|
229
|
-
const runtime = runTestCode(
|
|
230
|
-
ctx.context,
|
|
231
|
-
`
|
|
232
|
-
const formData = testingInput.formData;
|
|
233
|
-
log("instanceof", formData instanceof FormData);
|
|
234
|
-
log("constructorName", formData.constructor.name);
|
|
235
|
-
`
|
|
236
|
-
).input({
|
|
237
|
-
formData: nativeFormData,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
assert.deepStrictEqual(runtime.logs, {
|
|
241
|
-
instanceof: true,
|
|
242
|
-
constructorName: "FormData",
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test("get() returns correct values", () => {
|
|
247
|
-
const nativeFormData = new FormData();
|
|
248
|
-
nativeFormData.append("name", "John");
|
|
249
|
-
nativeFormData.append("email", "john@example.com");
|
|
250
|
-
|
|
251
|
-
const runtime = runTestCode(
|
|
252
|
-
ctx.context,
|
|
253
|
-
`
|
|
254
|
-
const formData = testingInput.formData;
|
|
255
|
-
log("name", formData.get("name"));
|
|
256
|
-
log("email", formData.get("email"));
|
|
257
|
-
log("missing", formData.get("missing"));
|
|
258
|
-
`
|
|
259
|
-
).input({
|
|
260
|
-
formData: nativeFormData,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
assert.deepStrictEqual(runtime.logs, {
|
|
264
|
-
name: "John",
|
|
265
|
-
email: "john@example.com",
|
|
266
|
-
missing: null,
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("has() returns correct boolean", () => {
|
|
271
|
-
const nativeFormData = new FormData();
|
|
272
|
-
nativeFormData.append("existing", "value");
|
|
273
|
-
|
|
274
|
-
const runtime = runTestCode(
|
|
275
|
-
ctx.context,
|
|
276
|
-
`
|
|
277
|
-
const formData = testingInput.formData;
|
|
278
|
-
log("hasExisting", formData.has("existing"));
|
|
279
|
-
log("hasMissing", formData.has("missing"));
|
|
280
|
-
`
|
|
281
|
-
).input({
|
|
282
|
-
formData: nativeFormData,
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
assert.deepStrictEqual(runtime.logs, {
|
|
286
|
-
hasExisting: true,
|
|
287
|
-
hasMissing: false,
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("append() and set() work correctly", () => {
|
|
292
|
-
const nativeFormData = new FormData();
|
|
293
|
-
nativeFormData.append("initial", "value");
|
|
294
|
-
|
|
295
|
-
const runtime = runTestCode(
|
|
296
|
-
ctx.context,
|
|
297
|
-
`
|
|
298
|
-
const formData = testingInput.formData;
|
|
299
|
-
formData.append("added", "new-value");
|
|
300
|
-
formData.set("initial", "updated-value");
|
|
301
|
-
log("added", formData.get("added"));
|
|
302
|
-
log("initial", formData.get("initial"));
|
|
303
|
-
`
|
|
304
|
-
).input({
|
|
305
|
-
formData: nativeFormData,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
assert.deepStrictEqual(runtime.logs, {
|
|
309
|
-
added: "new-value",
|
|
310
|
-
initial: "updated-value",
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test("delete() removes entries", () => {
|
|
315
|
-
const nativeFormData = new FormData();
|
|
316
|
-
nativeFormData.append("toDelete", "value");
|
|
317
|
-
nativeFormData.append("toKeep", "value");
|
|
318
|
-
|
|
319
|
-
const runtime = runTestCode(
|
|
320
|
-
ctx.context,
|
|
321
|
-
`
|
|
322
|
-
const formData = testingInput.formData;
|
|
323
|
-
log("beforeDelete", formData.has("toDelete"));
|
|
324
|
-
formData.delete("toDelete");
|
|
325
|
-
log("afterDelete", formData.has("toDelete"));
|
|
326
|
-
log("kept", formData.has("toKeep"));
|
|
327
|
-
`
|
|
328
|
-
).input({
|
|
329
|
-
formData: nativeFormData,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
assert.deepStrictEqual(runtime.logs, {
|
|
333
|
-
beforeDelete: true,
|
|
334
|
-
afterDelete: false,
|
|
335
|
-
kept: true,
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test("entries() returns all entries", () => {
|
|
340
|
-
const nativeFormData = new FormData();
|
|
341
|
-
nativeFormData.append("name", "Alice");
|
|
342
|
-
nativeFormData.append("age", "30");
|
|
343
|
-
|
|
344
|
-
const runtime = runTestCode(
|
|
345
|
-
ctx.context,
|
|
346
|
-
`
|
|
347
|
-
const formData = testingInput.formData;
|
|
348
|
-
log("entries", Array.from(formData.entries()));
|
|
349
|
-
`
|
|
350
|
-
).input({
|
|
351
|
-
formData: nativeFormData,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
const entries = runtime.logs.entries as Array<[string, string]>;
|
|
355
|
-
assert.ok(entries.some(([k, v]) => k === "name" && v === "Alice"));
|
|
356
|
-
assert.ok(entries.some(([k, v]) => k === "age" && v === "30"));
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test("for...of iteration works", () => {
|
|
360
|
-
const nativeFormData = new FormData();
|
|
361
|
-
nativeFormData.append("x", "1");
|
|
362
|
-
nativeFormData.append("y", "2");
|
|
363
|
-
|
|
364
|
-
const runtime = runTestCode(
|
|
365
|
-
ctx.context,
|
|
366
|
-
`
|
|
367
|
-
const formData = testingInput.formData;
|
|
368
|
-
const entries = [];
|
|
369
|
-
for (const [key, value] of formData) {
|
|
370
|
-
entries.push([key, value]);
|
|
371
|
-
}
|
|
372
|
-
log("entries", entries);
|
|
373
|
-
`
|
|
374
|
-
).input({
|
|
375
|
-
formData: nativeFormData,
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const entries = runtime.logs.entries as Array<[string, string]>;
|
|
379
|
-
assert.ok(entries.some(([k, v]) => k === "x" && v === "1"));
|
|
380
|
-
assert.ok(entries.some(([k, v]) => k === "y" && v === "2"));
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
describe("Bidirectional Conversion (Native->Isolate->Native)", () => {
|
|
384
|
-
test("FormData created in isolate should return as native FormData", () => {
|
|
385
|
-
const runtime = runTestCode(
|
|
386
|
-
ctx.context,
|
|
387
|
-
`
|
|
388
|
-
const formData = new FormData();
|
|
389
|
-
formData.append("name", "John");
|
|
390
|
-
formData.append("email", "john@example.com");
|
|
391
|
-
log("formData", formData);
|
|
392
|
-
`
|
|
393
|
-
).input({});
|
|
394
|
-
|
|
395
|
-
assert.ok(runtime.logs.formData instanceof FormData);
|
|
396
|
-
assert.strictEqual((runtime.logs.formData as FormData).get("name"), "John");
|
|
397
|
-
assert.strictEqual(
|
|
398
|
-
(runtime.logs.formData as FormData).get("email"),
|
|
399
|
-
"john@example.com"
|
|
400
|
-
);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
test("native FormData passed through isolate returns as native FormData", () => {
|
|
404
|
-
const nativeFormData = new FormData();
|
|
405
|
-
nativeFormData.append("key1", "value1");
|
|
406
|
-
nativeFormData.append("key2", "value2");
|
|
407
|
-
|
|
408
|
-
const runtime = runTestCode(
|
|
409
|
-
ctx.context,
|
|
410
|
-
`
|
|
411
|
-
const formData = testingInput.formData;
|
|
412
|
-
log("formData", formData);
|
|
413
|
-
`
|
|
414
|
-
).input({
|
|
415
|
-
formData: nativeFormData,
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
assert.ok(runtime.logs.formData instanceof FormData);
|
|
419
|
-
assert.strictEqual((runtime.logs.formData as FormData).get("key1"), "value1");
|
|
420
|
-
assert.strictEqual((runtime.logs.formData as FormData).get("key2"), "value2");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test("modifications in isolate are preserved when returning as native FormData", () => {
|
|
424
|
-
const nativeFormData = new FormData();
|
|
425
|
-
nativeFormData.append("original", "value");
|
|
426
|
-
|
|
427
|
-
const runtime = runTestCode(
|
|
428
|
-
ctx.context,
|
|
429
|
-
`
|
|
430
|
-
const formData = testingInput.formData;
|
|
431
|
-
formData.append("added", "newValue");
|
|
432
|
-
formData.set("updated", "updatedValue");
|
|
433
|
-
log("formData", formData);
|
|
434
|
-
`
|
|
435
|
-
).input({
|
|
436
|
-
formData: nativeFormData,
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
assert.ok(runtime.logs.formData instanceof FormData);
|
|
440
|
-
const formData = runtime.logs.formData as FormData;
|
|
441
|
-
assert.strictEqual(formData.get("original"), "value");
|
|
442
|
-
assert.strictEqual(formData.get("added"), "newValue");
|
|
443
|
-
assert.strictEqual(formData.get("updated"), "updatedValue");
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
test("nested object with FormData converts properly", () => {
|
|
447
|
-
const nativeFormData = new FormData();
|
|
448
|
-
nativeFormData.append("field", "test");
|
|
449
|
-
|
|
450
|
-
const runtime = runTestCode(
|
|
451
|
-
ctx.context,
|
|
452
|
-
`
|
|
453
|
-
const formData = testingInput.formData;
|
|
454
|
-
log("result", {
|
|
455
|
-
formData: formData,
|
|
456
|
-
metadata: { submitted: true }
|
|
457
|
-
});
|
|
458
|
-
`
|
|
459
|
-
).input({
|
|
460
|
-
formData: nativeFormData,
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
const result = runtime.logs.result as {
|
|
464
|
-
formData: FormData;
|
|
465
|
-
metadata: { submitted: boolean };
|
|
466
|
-
};
|
|
467
|
-
assert.ok(result.formData instanceof FormData);
|
|
468
|
-
assert.strictEqual(result.formData.get("field"), "test");
|
|
469
|
-
assert.deepStrictEqual(result.metadata, { submitted: true });
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Multipart FormData Parsing and Serialization Tests
|
|
476
|
-
*/
|
|
477
|
-
describe("Multipart FormData", () => {
|
|
478
|
-
let ctx: FetchTestContext;
|
|
479
|
-
|
|
480
|
-
beforeEach(async () => {
|
|
481
|
-
ctx = await createFetchTestContext();
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
afterEach(() => {
|
|
485
|
-
ctx.dispose();
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
describe("Parsing", () => {
|
|
489
|
-
test("parses multipart with text fields only", async () => {
|
|
490
|
-
const data = await ctx.context.eval(
|
|
491
|
-
`
|
|
492
|
-
(async () => {
|
|
493
|
-
const boundary = "----TestBoundary";
|
|
494
|
-
const body = [
|
|
495
|
-
"------TestBoundary\\r\\n",
|
|
496
|
-
'Content-Disposition: form-data; name="field1"\\r\\n',
|
|
497
|
-
"\\r\\n",
|
|
498
|
-
"value1",
|
|
499
|
-
"\\r\\n------TestBoundary\\r\\n",
|
|
500
|
-
'Content-Disposition: form-data; name="field2"\\r\\n',
|
|
501
|
-
"\\r\\n",
|
|
502
|
-
"value2",
|
|
503
|
-
"\\r\\n------TestBoundary--\\r\\n"
|
|
504
|
-
].join("");
|
|
505
|
-
|
|
506
|
-
const response = new Response(body, {
|
|
507
|
-
headers: { 'Content-Type': 'multipart/form-data; boundary=----TestBoundary' }
|
|
508
|
-
});
|
|
509
|
-
const formData = await response.formData();
|
|
510
|
-
return JSON.stringify({
|
|
511
|
-
field1: formData.get('field1'),
|
|
512
|
-
field2: formData.get('field2')
|
|
513
|
-
});
|
|
514
|
-
})()
|
|
515
|
-
`,
|
|
516
|
-
{ promise: true }
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
const result = JSON.parse(data as string) as { field1: string; field2: string };
|
|
520
|
-
assert.strictEqual(result.field1, "value1");
|
|
521
|
-
assert.strictEqual(result.field2, "value2");
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
test("parses multipart with file fields - returns File instances", async () => {
|
|
525
|
-
const data = await ctx.context.eval(
|
|
526
|
-
`
|
|
527
|
-
(async () => {
|
|
528
|
-
const encoder = new TextEncoder();
|
|
529
|
-
const body = encoder.encode([
|
|
530
|
-
'------TestBoundary\\r\\n',
|
|
531
|
-
'Content-Disposition: form-data; name="file"; filename="test.txt"\\r\\n',
|
|
532
|
-
'Content-Type: text/plain\\r\\n',
|
|
533
|
-
'\\r\\n',
|
|
534
|
-
'Hello World',
|
|
535
|
-
'\\r\\n------TestBoundary--\\r\\n'
|
|
536
|
-
].join(''));
|
|
537
|
-
|
|
538
|
-
const response = new Response(body, {
|
|
539
|
-
headers: { 'Content-Type': 'multipart/form-data; boundary=----TestBoundary' }
|
|
540
|
-
});
|
|
541
|
-
const formData = await response.formData();
|
|
542
|
-
const file = formData.get('file');
|
|
543
|
-
|
|
544
|
-
return JSON.stringify({
|
|
545
|
-
isFile: file instanceof File,
|
|
546
|
-
name: file.name,
|
|
547
|
-
type: file.type,
|
|
548
|
-
size: file.size
|
|
549
|
-
});
|
|
550
|
-
})()
|
|
551
|
-
`,
|
|
552
|
-
{ promise: true }
|
|
553
|
-
);
|
|
554
|
-
|
|
555
|
-
const result = JSON.parse(data as string) as {
|
|
556
|
-
isFile: boolean;
|
|
557
|
-
name: string;
|
|
558
|
-
type: string;
|
|
559
|
-
size: number;
|
|
560
|
-
};
|
|
561
|
-
assert.strictEqual(result.isFile, true);
|
|
562
|
-
assert.strictEqual(result.name, "test.txt");
|
|
563
|
-
assert.strictEqual(result.type, "text/plain");
|
|
564
|
-
assert.strictEqual(result.size, 11);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
test("File.text() works on parsed file", async () => {
|
|
568
|
-
const data = await ctx.context.eval(
|
|
569
|
-
`
|
|
570
|
-
(async () => {
|
|
571
|
-
const encoder = new TextEncoder();
|
|
572
|
-
const body = encoder.encode([
|
|
573
|
-
'------TestBoundary\\r\\n',
|
|
574
|
-
'Content-Disposition: form-data; name="file"; filename="test.txt"\\r\\n',
|
|
575
|
-
'Content-Type: text/plain\\r\\n',
|
|
576
|
-
'\\r\\n',
|
|
577
|
-
'File content here',
|
|
578
|
-
'\\r\\n------TestBoundary--\\r\\n'
|
|
579
|
-
].join(''));
|
|
580
|
-
|
|
581
|
-
const response = new Response(body, {
|
|
582
|
-
headers: { 'Content-Type': 'multipart/form-data; boundary=----TestBoundary' }
|
|
583
|
-
});
|
|
584
|
-
const formData = await response.formData();
|
|
585
|
-
const file = formData.get('file');
|
|
586
|
-
return await file.text();
|
|
587
|
-
})()
|
|
588
|
-
`,
|
|
589
|
-
{ promise: true }
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
assert.strictEqual(data, "File content here");
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
test("parses multipart with mixed text and file fields", async () => {
|
|
596
|
-
const data = await ctx.context.eval(
|
|
597
|
-
`
|
|
598
|
-
(async () => {
|
|
599
|
-
const encoder = new TextEncoder();
|
|
600
|
-
const body = encoder.encode([
|
|
601
|
-
'------TestBoundary\\r\\n',
|
|
602
|
-
'Content-Disposition: form-data; name="name"\\r\\n',
|
|
603
|
-
'\\r\\n',
|
|
604
|
-
'John Doe',
|
|
605
|
-
'\\r\\n------TestBoundary\\r\\n',
|
|
606
|
-
'Content-Disposition: form-data; name="avatar"; filename="photo.jpg"\\r\\n',
|
|
607
|
-
'Content-Type: image/jpeg\\r\\n',
|
|
608
|
-
'\\r\\n',
|
|
609
|
-
'binary-image-data',
|
|
610
|
-
'\\r\\n------TestBoundary--\\r\\n'
|
|
611
|
-
].join(''));
|
|
612
|
-
|
|
613
|
-
const response = new Response(body, {
|
|
614
|
-
headers: { 'Content-Type': 'multipart/form-data; boundary=----TestBoundary' }
|
|
615
|
-
});
|
|
616
|
-
const formData = await response.formData();
|
|
617
|
-
|
|
618
|
-
return JSON.stringify({
|
|
619
|
-
name: formData.get('name'),
|
|
620
|
-
isFile: formData.get('avatar') instanceof File,
|
|
621
|
-
filename: formData.get('avatar').name,
|
|
622
|
-
filetype: formData.get('avatar').type
|
|
623
|
-
});
|
|
624
|
-
})()
|
|
625
|
-
`,
|
|
626
|
-
{ promise: true }
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
const result = JSON.parse(data as string) as {
|
|
630
|
-
name: string;
|
|
631
|
-
isFile: boolean;
|
|
632
|
-
filename: string;
|
|
633
|
-
filetype: string;
|
|
634
|
-
};
|
|
635
|
-
assert.strictEqual(result.name, "John Doe");
|
|
636
|
-
assert.strictEqual(result.isFile, true);
|
|
637
|
-
assert.strictEqual(result.filename, "photo.jpg");
|
|
638
|
-
assert.strictEqual(result.filetype, "image/jpeg");
|
|
639
|
-
});
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
describe("Serialization", () => {
|
|
643
|
-
test("serializes FormData with File as multipart", async () => {
|
|
644
|
-
const data = await ctx.context.eval(
|
|
645
|
-
`
|
|
646
|
-
(async () => {
|
|
647
|
-
const file = new File(["test content"], "test.txt", { type: "text/plain" });
|
|
648
|
-
const formData = new FormData();
|
|
649
|
-
formData.append("file", file);
|
|
650
|
-
formData.append("name", "John");
|
|
651
|
-
|
|
652
|
-
const { body, contentType } = __serializeFormData(formData);
|
|
653
|
-
const text = new TextDecoder().decode(body);
|
|
654
|
-
|
|
655
|
-
return JSON.stringify({
|
|
656
|
-
hasBoundary: contentType.includes('boundary='),
|
|
657
|
-
hasFilename: text.includes('filename="test.txt"'),
|
|
658
|
-
hasFileContent: text.includes('test content'),
|
|
659
|
-
hasName: text.includes('name="name"'),
|
|
660
|
-
hasJohn: text.includes('John')
|
|
661
|
-
});
|
|
662
|
-
})()
|
|
663
|
-
`,
|
|
664
|
-
{ promise: true }
|
|
665
|
-
);
|
|
666
|
-
|
|
667
|
-
const result = JSON.parse(data as string) as {
|
|
668
|
-
hasBoundary: boolean;
|
|
669
|
-
hasFilename: boolean;
|
|
670
|
-
hasFileContent: boolean;
|
|
671
|
-
hasName: boolean;
|
|
672
|
-
hasJohn: boolean;
|
|
673
|
-
};
|
|
674
|
-
assert.strictEqual(result.hasBoundary, true);
|
|
675
|
-
assert.strictEqual(result.hasFilename, true);
|
|
676
|
-
assert.strictEqual(result.hasFileContent, true);
|
|
677
|
-
assert.strictEqual(result.hasName, true);
|
|
678
|
-
assert.strictEqual(result.hasJohn, true);
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
test("Request with FormData + File uses multipart Content-Type", async () => {
|
|
682
|
-
const data = await ctx.context.eval(
|
|
683
|
-
`
|
|
684
|
-
(async () => {
|
|
685
|
-
const file = new File(["uploaded content"], "upload.txt", { type: "text/plain" });
|
|
686
|
-
const formData = new FormData();
|
|
687
|
-
formData.append("file", file);
|
|
688
|
-
|
|
689
|
-
const request = new Request("http://test/upload", {
|
|
690
|
-
method: "POST",
|
|
691
|
-
body: formData
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
const contentType = request.headers.get('content-type');
|
|
695
|
-
return JSON.stringify({
|
|
696
|
-
isMultipart: contentType.includes('multipart/form-data'),
|
|
697
|
-
hasBoundary: contentType.includes('boundary=')
|
|
698
|
-
});
|
|
699
|
-
})()
|
|
700
|
-
`,
|
|
701
|
-
{ promise: true }
|
|
702
|
-
);
|
|
703
|
-
|
|
704
|
-
const result = JSON.parse(data as string) as {
|
|
705
|
-
isMultipart: boolean;
|
|
706
|
-
hasBoundary: boolean;
|
|
707
|
-
};
|
|
708
|
-
assert.strictEqual(result.isMultipart, true);
|
|
709
|
-
assert.strictEqual(result.hasBoundary, true);
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
test("Request with string-only FormData uses url-encoded", async () => {
|
|
713
|
-
const data = await ctx.context.eval(
|
|
714
|
-
`
|
|
715
|
-
(async () => {
|
|
716
|
-
const formData = new FormData();
|
|
717
|
-
formData.append("name", "John");
|
|
718
|
-
formData.append("email", "john@example.com");
|
|
719
|
-
|
|
720
|
-
const request = new Request("http://test/submit", {
|
|
721
|
-
method: "POST",
|
|
722
|
-
body: formData
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
return request.headers.get('content-type');
|
|
726
|
-
})()
|
|
727
|
-
`,
|
|
728
|
-
{ promise: true }
|
|
729
|
-
);
|
|
730
|
-
|
|
731
|
-
assert.strictEqual(data, "application/x-www-form-urlencoded");
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
describe("Round-trip", () => {
|
|
736
|
-
test("serialize then parse recovers original data", async () => {
|
|
737
|
-
const data = await ctx.context.eval(
|
|
738
|
-
`
|
|
739
|
-
(async () => {
|
|
740
|
-
// Create original FormData with file and text
|
|
741
|
-
const originalFile = new File(["Hello, World!"], "greeting.txt", { type: "text/plain" });
|
|
742
|
-
const originalFormData = new FormData();
|
|
743
|
-
originalFormData.append("message", "Test message");
|
|
744
|
-
originalFormData.append("attachment", originalFile);
|
|
745
|
-
|
|
746
|
-
// Serialize to multipart
|
|
747
|
-
const { body, contentType } = __serializeFormData(originalFormData);
|
|
748
|
-
|
|
749
|
-
// Parse it back
|
|
750
|
-
const response = new Response(body, {
|
|
751
|
-
headers: { 'Content-Type': contentType }
|
|
752
|
-
});
|
|
753
|
-
const parsedFormData = await response.formData();
|
|
754
|
-
|
|
755
|
-
// Check values
|
|
756
|
-
const parsedFile = parsedFormData.get('attachment');
|
|
757
|
-
const parsedFileContent = await parsedFile.text();
|
|
758
|
-
|
|
759
|
-
return JSON.stringify({
|
|
760
|
-
message: parsedFormData.get('message'),
|
|
761
|
-
isFile: parsedFile instanceof File,
|
|
762
|
-
filename: parsedFile.name,
|
|
763
|
-
filetype: parsedFile.type,
|
|
764
|
-
fileContent: parsedFileContent
|
|
765
|
-
});
|
|
766
|
-
})()
|
|
767
|
-
`,
|
|
768
|
-
{ promise: true }
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
const result = JSON.parse(data as string) as {
|
|
772
|
-
message: string;
|
|
773
|
-
isFile: boolean;
|
|
774
|
-
filename: string;
|
|
775
|
-
filetype: string;
|
|
776
|
-
fileContent: string;
|
|
777
|
-
};
|
|
778
|
-
assert.strictEqual(result.message, "Test message");
|
|
779
|
-
assert.strictEqual(result.isFile, true);
|
|
780
|
-
assert.strictEqual(result.filename, "greeting.txt");
|
|
781
|
-
assert.strictEqual(result.filetype, "text/plain");
|
|
782
|
-
assert.strictEqual(result.fileContent, "Hello, World!");
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
test("handles Blob entries in FormData", async () => {
|
|
786
|
-
const data = await ctx.context.eval(
|
|
787
|
-
`
|
|
788
|
-
(async () => {
|
|
789
|
-
const blob = new Blob(["blob content"], { type: "application/octet-stream" });
|
|
790
|
-
const formData = new FormData();
|
|
791
|
-
formData.append("data", blob);
|
|
792
|
-
|
|
793
|
-
// Serialize and parse back
|
|
794
|
-
const { body, contentType } = __serializeFormData(formData);
|
|
795
|
-
const response = new Response(body, {
|
|
796
|
-
headers: { 'Content-Type': contentType }
|
|
797
|
-
});
|
|
798
|
-
const parsedFormData = await response.formData();
|
|
799
|
-
|
|
800
|
-
const parsedBlob = parsedFormData.get('data');
|
|
801
|
-
const content = await parsedBlob.text();
|
|
802
|
-
|
|
803
|
-
return JSON.stringify({
|
|
804
|
-
isFile: parsedBlob instanceof File,
|
|
805
|
-
filename: parsedBlob.name,
|
|
806
|
-
content: content
|
|
807
|
-
});
|
|
808
|
-
})()
|
|
809
|
-
`,
|
|
810
|
-
{ promise: true }
|
|
811
|
-
);
|
|
812
|
-
|
|
813
|
-
const result = JSON.parse(data as string) as {
|
|
814
|
-
isFile: boolean;
|
|
815
|
-
filename: string;
|
|
816
|
-
content: string;
|
|
817
|
-
};
|
|
818
|
-
// Blob is serialized as File with default name "blob"
|
|
819
|
-
assert.strictEqual(result.isFile, true);
|
|
820
|
-
assert.strictEqual(result.filename, "blob");
|
|
821
|
-
assert.strictEqual(result.content, "blob content");
|
|
822
|
-
});
|
|
823
|
-
});
|
|
824
|
-
});
|