goscript 0.2.2 → 0.2.4
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/compiler/gotest/testdata/browserapi/browserapi_test.go +36 -0
- package/compiler/lowering.go +279 -16
- package/compiler/override-registry_test.go +175 -0
- package/compiler/protobuf-ts-binding.go +154 -6
- package/compiler/protobuf-ts-binding_test.go +7 -2
- package/compiler/runtime-contract.go +2 -0
- package/compiler/runtime-contract_test.go +1 -0
- package/compiler/semantic-model.go +16 -0
- package/compiler/semantic-model_test.go +38 -0
- package/compiler/skeleton_test.go +522 -17
- package/compiler/typescript-emitter.go +4 -0
- package/dist/gs/builtin/builtin.js +7 -9
- package/dist/gs/builtin/builtin.js.map +1 -1
- package/dist/gs/builtin/defer.js +2 -2
- package/dist/gs/builtin/hostio.js +5 -5
- package/dist/gs/builtin/hostio.js.map +1 -1
- package/dist/gs/builtin/map.js +2 -1
- package/dist/gs/builtin/map.js.map +1 -1
- package/dist/gs/builtin/slice.d.ts +3 -0
- package/dist/gs/builtin/slice.js +39 -0
- package/dist/gs/builtin/slice.js.map +1 -1
- package/dist/gs/builtin/type.js +49 -0
- package/dist/gs/builtin/type.js.map +1 -1
- package/dist/gs/compress/gzip/index.d.ts +41 -0
- package/dist/gs/compress/gzip/index.js +235 -0
- package/dist/gs/compress/gzip/index.js.map +1 -0
- package/dist/gs/compress/zlib/index.js +5 -2
- package/dist/gs/compress/zlib/index.js.map +1 -1
- package/dist/gs/crypto/ecdh/index.js +27 -8
- package/dist/gs/crypto/ecdh/index.js.map +1 -1
- package/dist/gs/crypto/ed25519/index.js +3 -3
- package/dist/gs/crypto/ed25519/index.js.map +1 -1
- package/dist/gs/crypto/rand/index.js +6 -3
- package/dist/gs/crypto/rand/index.js.map +1 -1
- package/dist/gs/embed/index.js +9 -3
- package/dist/gs/embed/index.js.map +1 -1
- package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +1 -0
- package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +33 -0
- package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
- package/dist/gs/github.com/mr-tron/base58/base58/index.js +4 -1
- package/dist/gs/github.com/mr-tron/base58/base58/index.js.map +1 -1
- package/dist/gs/golang.org/x/crypto/scrypt/index.d.ts +2 -0
- package/dist/gs/golang.org/x/crypto/scrypt/index.js +39 -0
- package/dist/gs/golang.org/x/crypto/scrypt/index.js.map +1 -0
- package/dist/gs/hash/fnv/index.js +13 -5
- package/dist/gs/hash/fnv/index.js.map +1 -1
- package/dist/gs/io/fs/glob.d.ts +3 -3
- package/dist/gs/io/fs/glob.js +9 -9
- package/dist/gs/io/fs/glob.js.map +1 -1
- package/dist/gs/io/fs/readdir.d.ts +2 -2
- package/dist/gs/io/fs/readdir.js +13 -74
- package/dist/gs/io/fs/readdir.js.map +1 -1
- package/dist/gs/io/fs/readlink.d.ts +1 -1
- package/dist/gs/io/fs/readlink.js +2 -2
- package/dist/gs/io/fs/readlink.js.map +1 -1
- package/dist/gs/io/fs/stat.d.ts +4 -2
- package/dist/gs/io/fs/stat.js +12 -73
- package/dist/gs/io/fs/stat.js.map +1 -1
- package/dist/gs/io/fs/sub.d.ts +2 -2
- package/dist/gs/io/fs/sub.js +11 -11
- package/dist/gs/io/fs/sub.js.map +1 -1
- package/dist/gs/io/fs/walk.js +2 -2
- package/dist/gs/io/fs/walk.js.map +1 -1
- package/dist/gs/maps/iter.js.map +1 -1
- package/dist/gs/maps/maps.js.map +1 -1
- package/dist/gs/mime/index.js +5 -2
- package/dist/gs/mime/index.js.map +1 -1
- package/dist/gs/net/http/httptest/index.js +6 -3
- package/dist/gs/net/http/httptest/index.js.map +1 -1
- package/dist/gs/net/http/index.d.ts +34 -18
- package/dist/gs/net/http/index.js +280 -63
- package/dist/gs/net/http/index.js.map +1 -1
- package/dist/gs/net/http/pprof/index.d.ts +5 -5
- package/dist/gs/net/http/pprof/index.js +21 -21
- package/dist/gs/net/http/pprof/index.js.map +1 -1
- package/dist/gs/reflect/iter.js +1 -1
- package/dist/gs/reflect/iter.js.map +1 -1
- package/dist/gs/reflect/type.d.ts +2 -0
- package/dist/gs/reflect/type.js +53 -21
- package/dist/gs/reflect/type.js.map +1 -1
- package/dist/gs/runtime/pprof/index.js.map +1 -1
- package/dist/gs/runtime/runtime.js +2 -2
- package/dist/gs/runtime/runtime.js.map +1 -1
- package/dist/gs/runtime/trace/index.js.map +1 -1
- package/dist/gs/slices/slices.d.ts +1 -1
- package/dist/gs/slices/slices.js +37 -4
- package/dist/gs/slices/slices.js.map +1 -1
- package/gs/builtin/builtin.ts +11 -14
- package/gs/builtin/defer.ts +2 -2
- package/gs/builtin/hostio.ts +5 -5
- package/gs/builtin/map.ts +4 -1
- package/gs/builtin/runtime-contract.test.ts +25 -0
- package/gs/builtin/slice.test.ts +14 -0
- package/gs/builtin/slice.ts +64 -0
- package/gs/builtin/type.ts +72 -0
- package/gs/bytes/bytes.test.ts +14 -13
- package/gs/compress/gzip/index.test.ts +86 -0
- package/gs/compress/gzip/index.ts +297 -0
- package/gs/compress/gzip/meta.json +6 -0
- package/gs/compress/gzip/parity.json +45 -0
- package/gs/compress/zlib/index.test.ts +19 -5
- package/gs/compress/zlib/index.ts +16 -7
- package/gs/context/context.test.ts +3 -1
- package/gs/crypto/ecdh/index.test.ts +6 -2
- package/gs/crypto/ecdh/index.ts +49 -12
- package/gs/crypto/ed25519/index.ts +20 -7
- package/gs/crypto/rand/index.ts +6 -3
- package/gs/embed/index.test.ts +4 -4
- package/gs/embed/index.ts +9 -3
- package/gs/fmt/fmt.test.ts +29 -4
- package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +126 -0
- package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +46 -0
- package/gs/github.com/mr-tron/base58/base58/index.ts +9 -3
- package/gs/github.com/zeebo/blake3/internal/consts/index.test.ts +2 -8
- package/gs/golang.org/x/crypto/scrypt/index.test.ts +81 -0
- package/gs/golang.org/x/crypto/scrypt/index.ts +54 -0
- package/gs/golang.org/x/crypto/scrypt/meta.json +5 -0
- package/gs/hash/fnv/index.test.ts +1 -8
- package/gs/hash/fnv/index.ts +27 -10
- package/gs/io/fs/glob.ts +14 -11
- package/gs/io/fs/meta.json +5 -0
- package/gs/io/fs/readdir.test.ts +63 -2
- package/gs/io/fs/readdir.ts +33 -30
- package/gs/io/fs/readlink.test.ts +2 -2
- package/gs/io/fs/readlink.ts +5 -2
- package/gs/io/fs/stat.test.ts +79 -0
- package/gs/io/fs/stat.ts +24 -10
- package/gs/io/fs/sub.test.ts +93 -0
- package/gs/io/fs/sub.ts +13 -13
- package/gs/io/fs/walk.ts +2 -2
- package/gs/maps/iter.ts +9 -9
- package/gs/maps/maps.ts +4 -4
- package/gs/math/bits/index.test.ts +10 -1
- package/gs/mime/index.test.ts +33 -15
- package/gs/mime/index.ts +9 -2
- package/gs/net/http/httptest/index.test.ts +17 -3
- package/gs/net/http/httptest/index.ts +8 -3
- package/gs/net/http/index.test.ts +851 -124
- package/gs/net/http/index.ts +612 -146
- package/gs/net/http/meta.json +3 -1
- package/gs/net/http/pprof/index.test.ts +4 -4
- package/gs/net/http/pprof/index.ts +43 -22
- package/gs/os/file_unix_js.test.ts +22 -0
- package/gs/reflect/iter.ts +4 -2
- package/gs/reflect/map.test.ts +56 -1
- package/gs/reflect/type.ts +76 -37
- package/gs/runtime/pprof/index.test.ts +7 -1
- package/gs/runtime/pprof/index.ts +5 -1
- package/gs/runtime/runtime.test.ts +7 -0
- package/gs/runtime/runtime.ts +2 -4
- package/gs/runtime/trace/index.test.ts +9 -1
- package/gs/runtime/trace/index.ts +5 -1
- package/gs/slices/meta.json +3 -0
- package/gs/slices/slices.test.ts +59 -21
- package/gs/slices/slices.ts +61 -20
- package/gs/strconv/complex.test.ts +17 -3
- package/gs/sync/atomic/doc_64.test.ts +2 -9
- package/gs/sync/sync.test.ts +18 -8
- package/gs/syscall/js/index.test.ts +9 -4
- package/package.json +5 -4
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
ErrServerClosed,
|
|
21
21
|
File,
|
|
22
22
|
FileServer,
|
|
23
|
+
FileServerFS,
|
|
23
24
|
FileSystem,
|
|
24
25
|
FS,
|
|
25
26
|
Get,
|
|
@@ -124,12 +125,16 @@ describe('net/http override', () => {
|
|
|
124
125
|
expect(StatusText(StatusUnauthorized)).toBe('Unauthorized')
|
|
125
126
|
expect(StatusText(StatusForbidden)).toBe('Forbidden')
|
|
126
127
|
expect(StatusText(StatusMethodNotAllowed)).toBe('Method Not Allowed')
|
|
127
|
-
expect(StatusText(StatusUnsupportedMediaType)).toBe(
|
|
128
|
+
expect(StatusText(StatusUnsupportedMediaType)).toBe(
|
|
129
|
+
'Unsupported Media Type',
|
|
130
|
+
)
|
|
128
131
|
expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
|
|
129
132
|
expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
|
|
130
133
|
expect(StatusText(StatusBadGateway)).toBe('Bad Gateway')
|
|
131
134
|
expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
|
|
132
|
-
expect(StatusText(StatusNetworkAuthenticationRequired)).toBe(
|
|
135
|
+
expect(StatusText(StatusNetworkAuthenticationRequired)).toBe(
|
|
136
|
+
'Network Authentication Required',
|
|
137
|
+
)
|
|
133
138
|
expect(StatusText(599)).toBe('')
|
|
134
139
|
Header_Set(resp.Header, 'X-Test', 'ok')
|
|
135
140
|
resp.Body = io.NopCloser(bytes.NewReader($.stringToBytes('body')))
|
|
@@ -164,12 +169,18 @@ describe('net/http override', () => {
|
|
|
164
169
|
'text/plain',
|
|
165
170
|
'charset=utf-8',
|
|
166
171
|
])
|
|
167
|
-
expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain(
|
|
168
|
-
|
|
172
|
+
expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain(
|
|
173
|
+
'copy',
|
|
174
|
+
)
|
|
175
|
+
expect(
|
|
176
|
+
Array.from(Header_Values(header, 'Content-Type') ?? []),
|
|
177
|
+
).not.toContain('copy')
|
|
169
178
|
|
|
170
179
|
const written = new bytes.Buffer()
|
|
171
180
|
expect(Header_Write(header, written)).toBeNull()
|
|
172
|
-
expect(Buffer.from(written.Bytes()).toString('utf8')).toContain(
|
|
181
|
+
expect(Buffer.from(written.Bytes()).toString('utf8')).toContain(
|
|
182
|
+
'Content-Type: text/plain\r\n',
|
|
183
|
+
)
|
|
173
184
|
|
|
174
185
|
expect(ParseHTTPVersion('HTTP/2.0')).toEqual([2, 0, true])
|
|
175
186
|
expect(ParseHTTPVersion('h2')).toEqual([0, 0, false])
|
|
@@ -182,6 +193,36 @@ describe('net/http override', () => {
|
|
|
182
193
|
expect(protocols.String()).toBe('{HTTP1,UnencryptedHTTP2}')
|
|
183
194
|
})
|
|
184
195
|
|
|
196
|
+
it('accepts pointer-wrapped headers from generated ResponseWriter methods', () => {
|
|
197
|
+
const header = varRef(new Header())
|
|
198
|
+
|
|
199
|
+
Header_Set(header, 'content-length', '12')
|
|
200
|
+
Header_Add(header, 'content-type', 'text/plain')
|
|
201
|
+
|
|
202
|
+
expect(Header_Get(header, 'Content-Length')).toBe('12')
|
|
203
|
+
expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).toEqual([
|
|
204
|
+
'text/plain',
|
|
205
|
+
])
|
|
206
|
+
|
|
207
|
+
const cloned = Header_Clone(header)
|
|
208
|
+
Header_Del(header, 'Content-Type')
|
|
209
|
+
|
|
210
|
+
expect(Header_Get(header, 'Content-Type')).toBe('')
|
|
211
|
+
expect(Header_Get(cloned, 'Content-Type')).toBe('text/plain')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('accepts interface-boxed named headers from generated interface calls', () => {
|
|
215
|
+
const header = $.namedValueInterfaceValue(new Header(), 'http.Header', {})
|
|
216
|
+
|
|
217
|
+
Header_Set(header, 'content-length', '12')
|
|
218
|
+
Header_Add(header, 'content-type', 'text/plain')
|
|
219
|
+
|
|
220
|
+
expect(Header_Get(header, 'Content-Length')).toBe('12')
|
|
221
|
+
expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).toEqual([
|
|
222
|
+
'text/plain',
|
|
223
|
+
])
|
|
224
|
+
})
|
|
225
|
+
|
|
185
226
|
it('validates outgoing request construction', () => {
|
|
186
227
|
const [req, reqErr] = NewRequestWithContext(
|
|
187
228
|
context.Background(),
|
|
@@ -195,22 +236,58 @@ describe('net/http override', () => {
|
|
|
195
236
|
expect(req?.URL?.Path).toBe('/path')
|
|
196
237
|
expect(req?.URL?.RawQuery).toBe('q=1')
|
|
197
238
|
|
|
198
|
-
expect(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
239
|
+
expect(
|
|
240
|
+
NewRequestWithContext(
|
|
241
|
+
context.Background(),
|
|
242
|
+
'bad method',
|
|
243
|
+
'https://example.invalid/',
|
|
244
|
+
null,
|
|
245
|
+
)[1]?.Error(),
|
|
246
|
+
).toBe('net/http: invalid method "bad method"')
|
|
247
|
+
expect(
|
|
248
|
+
NewRequestWithContext(
|
|
249
|
+
null,
|
|
250
|
+
MethodGet,
|
|
251
|
+
'https://example.invalid/',
|
|
252
|
+
null,
|
|
253
|
+
)[1]?.Error(),
|
|
254
|
+
).toBe('net/http: nil Context')
|
|
255
|
+
expect(
|
|
256
|
+
NewRequestWithContext(
|
|
257
|
+
context.Background(),
|
|
258
|
+
MethodGet,
|
|
259
|
+
'http://[::1',
|
|
260
|
+
null,
|
|
261
|
+
)[1],
|
|
262
|
+
).not.toBeNull()
|
|
263
|
+
expect(
|
|
264
|
+
NewRequestWithContext(
|
|
265
|
+
context.Background(),
|
|
266
|
+
MethodGet,
|
|
267
|
+
'http://x/%zz',
|
|
268
|
+
null,
|
|
269
|
+
)[1],
|
|
270
|
+
).not.toBeNull()
|
|
271
|
+
|
|
272
|
+
const [bodyReq, bodyReqErr] = NewRequest(
|
|
273
|
+
MethodPost,
|
|
274
|
+
'https://example.invalid/upload',
|
|
275
|
+
bytes.NewReader($.stringToBytes('abc')),
|
|
203
276
|
)
|
|
204
|
-
expect(NewRequestWithContext(context.Background(), MethodGet, 'http://[::1', null)[1]).not.toBeNull()
|
|
205
|
-
expect(NewRequestWithContext(context.Background(), MethodGet, 'http://x/%zz', null)[1]).not.toBeNull()
|
|
206
|
-
|
|
207
|
-
const [bodyReq, bodyReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', bytes.NewReader($.stringToBytes('abc')))
|
|
208
277
|
expect(bodyReqErr).toBeNull()
|
|
209
278
|
expect(bodyReq?.ContentLength).toBe(3)
|
|
210
|
-
const [stringReq, stringReqErr] = NewRequest(
|
|
279
|
+
const [stringReq, stringReqErr] = NewRequest(
|
|
280
|
+
MethodPost,
|
|
281
|
+
'https://example.invalid/upload',
|
|
282
|
+
strings.NewReader('abcd'),
|
|
283
|
+
)
|
|
211
284
|
expect(stringReqErr).toBeNull()
|
|
212
285
|
expect(stringReq?.ContentLength).toBe(4)
|
|
213
|
-
const [noBodyReq, noBodyErr] = NewRequest(
|
|
286
|
+
const [noBodyReq, noBodyErr] = NewRequest(
|
|
287
|
+
MethodPost,
|
|
288
|
+
'https://example.invalid/upload',
|
|
289
|
+
NoBody,
|
|
290
|
+
)
|
|
214
291
|
expect(noBodyErr).toBeNull()
|
|
215
292
|
expect(noBodyReq?.ContentLength).toBe(0)
|
|
216
293
|
expect(noBodyReq?.Body).toBe(NoBody)
|
|
@@ -218,44 +295,84 @@ describe('net/http override', () => {
|
|
|
218
295
|
|
|
219
296
|
it('applies cross-origin protection checks', () => {
|
|
220
297
|
const protection = NewCrossOriginProtection()
|
|
221
|
-
const [sameOrigin] = NewRequest(
|
|
298
|
+
const [sameOrigin] = NewRequest(
|
|
299
|
+
MethodPost,
|
|
300
|
+
'https://example.invalid/update',
|
|
301
|
+
null,
|
|
302
|
+
)
|
|
222
303
|
Header_Set(sameOrigin!.Header, 'Sec-Fetch-Site', 'same-origin')
|
|
223
304
|
expect(protection.Check(sameOrigin)).toBeNull()
|
|
224
305
|
|
|
225
|
-
const [noHeaders] = NewRequest(
|
|
306
|
+
const [noHeaders] = NewRequest(
|
|
307
|
+
MethodPost,
|
|
308
|
+
'https://example.invalid/update',
|
|
309
|
+
null,
|
|
310
|
+
)
|
|
226
311
|
expect(protection.Check(noHeaders)).toBeNull()
|
|
227
312
|
|
|
228
|
-
const [safe] = NewRequest(
|
|
313
|
+
const [safe] = NewRequest(
|
|
314
|
+
MethodOptions,
|
|
315
|
+
'https://example.invalid/update',
|
|
316
|
+
null,
|
|
317
|
+
)
|
|
229
318
|
Header_Set(safe!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
230
319
|
expect(protection.Check(safe)).toBeNull()
|
|
231
320
|
|
|
232
|
-
const [matchingOrigin] = NewRequest(
|
|
321
|
+
const [matchingOrigin] = NewRequest(
|
|
322
|
+
MethodPost,
|
|
323
|
+
'https://example.invalid/update',
|
|
324
|
+
null,
|
|
325
|
+
)
|
|
233
326
|
Header_Set(matchingOrigin!.Header, 'Origin', 'https://example.invalid')
|
|
234
327
|
expect(protection.Check(matchingOrigin)).toBeNull()
|
|
235
328
|
|
|
236
|
-
const [crossSite] = NewRequest(
|
|
329
|
+
const [crossSite] = NewRequest(
|
|
330
|
+
MethodPost,
|
|
331
|
+
'https://example.invalid/update',
|
|
332
|
+
null,
|
|
333
|
+
)
|
|
237
334
|
Header_Set(crossSite!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
238
335
|
expect(protection.Check(crossSite)?.Error()).toContain('Sec-Fetch-Site')
|
|
239
336
|
|
|
240
|
-
const [oldBrowser] = NewRequest(
|
|
337
|
+
const [oldBrowser] = NewRequest(
|
|
338
|
+
MethodPost,
|
|
339
|
+
'https://example.invalid/update',
|
|
340
|
+
null,
|
|
341
|
+
)
|
|
241
342
|
Header_Set(oldBrowser!.Header, 'Origin', 'https://attacker.invalid')
|
|
242
|
-
expect(protection.Check(oldBrowser)?.Error()).toContain(
|
|
343
|
+
expect(protection.Check(oldBrowser)?.Error()).toContain(
|
|
344
|
+
'Origin does not match Host',
|
|
345
|
+
)
|
|
243
346
|
|
|
244
347
|
expect(protection.AddTrustedOrigin('https://trusted.invalid')).toBeNull()
|
|
245
|
-
expect(
|
|
246
|
-
|
|
348
|
+
expect(
|
|
349
|
+
protection.AddTrustedOrigin('https://trusted.invalid/'),
|
|
350
|
+
).not.toBeNull()
|
|
351
|
+
const [trusted] = NewRequest(
|
|
352
|
+
MethodPost,
|
|
353
|
+
'https://example.invalid/update',
|
|
354
|
+
null,
|
|
355
|
+
)
|
|
247
356
|
Header_Set(trusted!.Header, 'Origin', 'https://trusted.invalid')
|
|
248
357
|
Header_Set(trusted!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
249
358
|
expect(protection.Check(trusted)).toBeNull()
|
|
250
359
|
|
|
251
360
|
protection.AddInsecureBypassPattern('/bypass/')
|
|
252
361
|
protection.AddInsecureBypassPattern('POST /post-only/')
|
|
253
|
-
const [bypass] = NewRequest(
|
|
362
|
+
const [bypass] = NewRequest(
|
|
363
|
+
MethodPost,
|
|
364
|
+
'https://example.invalid/bypass/ok',
|
|
365
|
+
null,
|
|
366
|
+
)
|
|
254
367
|
Header_Set(bypass!.Header, 'Origin', 'https://attacker.invalid')
|
|
255
368
|
Header_Set(bypass!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
256
369
|
expect(protection.Check(bypass)).toBeNull()
|
|
257
370
|
|
|
258
|
-
const [methodBypass] = NewRequest(
|
|
371
|
+
const [methodBypass] = NewRequest(
|
|
372
|
+
MethodPost,
|
|
373
|
+
'https://example.invalid/post-only/',
|
|
374
|
+
null,
|
|
375
|
+
)
|
|
259
376
|
Header_Set(methodBypass!.Header, 'Origin', 'https://attacker.invalid')
|
|
260
377
|
expect(protection.Check(methodBypass)).toBeNull()
|
|
261
378
|
})
|
|
@@ -269,7 +386,11 @@ describe('net/http override', () => {
|
|
|
269
386
|
w?.WriteHeader(StatusOK)
|
|
270
387
|
},
|
|
271
388
|
})
|
|
272
|
-
const [blocked] = NewRequest(
|
|
389
|
+
const [blocked] = NewRequest(
|
|
390
|
+
MethodPost,
|
|
391
|
+
'https://example.invalid/update',
|
|
392
|
+
null,
|
|
393
|
+
)
|
|
273
394
|
Header_Set(blocked!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
274
395
|
const blockedWriter = new testResponseWriter()
|
|
275
396
|
|
|
@@ -304,7 +425,9 @@ describe('net/http override', () => {
|
|
|
304
425
|
expect(cookies?.[1]?.Value).toBe('v2')
|
|
305
426
|
|
|
306
427
|
expect(ParseCookie('')[1]?.Error()).toBe('http: blank cookie')
|
|
307
|
-
expect(ParseCookie('missing-equals')[1]?.Error()).toBe(
|
|
428
|
+
expect(ParseCookie('missing-equals')[1]?.Error()).toBe(
|
|
429
|
+
"http: '=' not found in cookie",
|
|
430
|
+
)
|
|
308
431
|
expect(ParseCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
|
|
309
432
|
expect(ParseCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
|
|
310
433
|
|
|
@@ -330,9 +453,13 @@ describe('net/http override', () => {
|
|
|
330
453
|
expect(spaced?.Quoted).toBe(true)
|
|
331
454
|
|
|
332
455
|
expect(ParseSetCookie('')[1]?.Error()).toBe('http: blank cookie')
|
|
333
|
-
expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe(
|
|
456
|
+
expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe(
|
|
457
|
+
"http: '=' not found in cookie",
|
|
458
|
+
)
|
|
334
459
|
expect(ParseSetCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
|
|
335
|
-
expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe(
|
|
460
|
+
expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe(
|
|
461
|
+
'http: invalid cookie value',
|
|
462
|
+
)
|
|
336
463
|
})
|
|
337
464
|
|
|
338
465
|
it('exports no-body, limit-reader, and unsupported controller surfaces', async () => {
|
|
@@ -342,34 +469,48 @@ describe('net/http override', () => {
|
|
|
342
469
|
expect(err).toBe(io.EOF)
|
|
343
470
|
expect(NoBody.Close()).toBeNull()
|
|
344
471
|
|
|
345
|
-
const limited = MaxBytesReader(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
p[
|
|
350
|
-
|
|
351
|
-
|
|
472
|
+
const limited = MaxBytesReader(
|
|
473
|
+
null,
|
|
474
|
+
{
|
|
475
|
+
Read: (p: Uint8Array) => {
|
|
476
|
+
p[0] = 1
|
|
477
|
+
if (p.length > 1) {
|
|
478
|
+
p[1] = 2
|
|
479
|
+
}
|
|
480
|
+
return [1, null]
|
|
481
|
+
},
|
|
482
|
+
Close: () => null,
|
|
352
483
|
},
|
|
353
|
-
|
|
354
|
-
|
|
484
|
+
1,
|
|
485
|
+
)
|
|
355
486
|
const limitedBuf = new Uint8Array(2)
|
|
356
487
|
expect(limited.Read(limitedBuf)).toEqual([1, null])
|
|
357
488
|
expect(limitedBuf[0]).toBe(1)
|
|
358
489
|
const [, limitErr] = limited.Read(new Uint8Array(1))
|
|
359
490
|
expect(limitErr).toBeInstanceOf(MaxBytesError)
|
|
360
491
|
|
|
361
|
-
const exactReader = MaxBytesReader(
|
|
492
|
+
const exactReader = MaxBytesReader(
|
|
493
|
+
null,
|
|
494
|
+
io.NopCloser(bytes.NewReader($.stringToBytes('ok'))),
|
|
495
|
+
2,
|
|
496
|
+
)
|
|
362
497
|
const [exactData, exactErr] = await io.ReadAll(exactReader)
|
|
363
498
|
expect(exactErr).toBeNull()
|
|
364
499
|
expect(Buffer.from(exactData ?? []).toString('utf8')).toBe('ok')
|
|
365
500
|
|
|
366
|
-
const tooLarge = MaxBytesReader(
|
|
501
|
+
const tooLarge = MaxBytesReader(
|
|
502
|
+
null,
|
|
503
|
+
io.NopCloser(bytes.NewReader($.stringToBytes('toolarge'))),
|
|
504
|
+
2,
|
|
505
|
+
)
|
|
367
506
|
const tooLargeBuf = new Uint8Array(8)
|
|
368
507
|
const [tooLargeN, tooLargeErr] = tooLarge.Read(tooLargeBuf)
|
|
369
508
|
expect(tooLargeN).toBe(2)
|
|
370
509
|
expect(tooLargeErr).toBeInstanceOf(MaxBytesError)
|
|
371
510
|
expect((tooLargeErr as MaxBytesError).Limit).toBe(2)
|
|
372
|
-
expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe(
|
|
511
|
+
expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe(
|
|
512
|
+
'to',
|
|
513
|
+
)
|
|
373
514
|
|
|
374
515
|
const controller = NewResponseController(null)
|
|
375
516
|
expect(controller.Hijack()[2]).toBe(ErrNotSupported)
|
|
@@ -382,12 +523,15 @@ describe('net/http override', () => {
|
|
|
382
523
|
const body = io.NopCloser(bytes.NewReader($.stringToBytes('ok')))
|
|
383
524
|
const [req] = NewRequest(MethodPost, 'http://example.invalid/upload', body)
|
|
384
525
|
let servedReq: any = null
|
|
385
|
-
const handler = MaxBytesHandler(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
526
|
+
const handler = MaxBytesHandler(
|
|
527
|
+
{
|
|
528
|
+
ServeHTTP(_w, r) {
|
|
529
|
+
servedReq = $.pointerValue(r)
|
|
530
|
+
Header_Set(servedReq.Header, 'X-Shared', 'true')
|
|
531
|
+
},
|
|
389
532
|
},
|
|
390
|
-
|
|
533
|
+
1,
|
|
534
|
+
)
|
|
391
535
|
|
|
392
536
|
handler.ServeHTTP(null, req)
|
|
393
537
|
|
|
@@ -424,7 +568,11 @@ describe('net/http override', () => {
|
|
|
424
568
|
throw new Error('network down')
|
|
425
569
|
},
|
|
426
570
|
})
|
|
427
|
-
const [req, reqErr] = NewRequest(
|
|
571
|
+
const [req, reqErr] = NewRequest(
|
|
572
|
+
MethodPost,
|
|
573
|
+
'https://example.invalid',
|
|
574
|
+
null,
|
|
575
|
+
)
|
|
428
576
|
expect(reqErr).toBeNull()
|
|
429
577
|
expect((req!.URL as any).Path).toBe('/')
|
|
430
578
|
expect(req!.Host).toBe('example.invalid')
|
|
@@ -440,13 +588,21 @@ describe('net/http override', () => {
|
|
|
440
588
|
Read: (p: Uint8Array) => [p.length, null] as [number, null],
|
|
441
589
|
}
|
|
442
590
|
|
|
443
|
-
const [req, reqErr] = NewRequest(
|
|
591
|
+
const [req, reqErr] = NewRequest(
|
|
592
|
+
MethodPost,
|
|
593
|
+
'https://example.invalid/upload',
|
|
594
|
+
reader,
|
|
595
|
+
)
|
|
444
596
|
|
|
445
597
|
expect(reqErr).toBeNull()
|
|
446
598
|
expect(req!.Body).not.toBeNull()
|
|
447
599
|
expect(req!.Body!.Close()).toBeNull()
|
|
448
600
|
|
|
449
|
-
const resp = new Response({
|
|
601
|
+
const resp = new Response({
|
|
602
|
+
StatusCode: StatusCreated,
|
|
603
|
+
ContentLength: -1,
|
|
604
|
+
Request: varRef(req!),
|
|
605
|
+
})
|
|
450
606
|
expect(resp.ContentLength).toBe(-1)
|
|
451
607
|
expect((resp.Request as any).value).toBe(req)
|
|
452
608
|
})
|
|
@@ -462,7 +618,9 @@ describe('net/http override', () => {
|
|
|
462
618
|
const headers = init?.headers as Headers
|
|
463
619
|
expect(headers.get('Range')).toBe('bytes=0-9')
|
|
464
620
|
expect(headers.get('Authorization')).toBe('Bearer test')
|
|
465
|
-
expect(
|
|
621
|
+
expect(
|
|
622
|
+
Buffer.from((init?.body as Uint8Array) ?? []).toString('utf8'),
|
|
623
|
+
).toBe('payload')
|
|
466
624
|
return new globalThis.Response('accepted', {
|
|
467
625
|
status: StatusCreated,
|
|
468
626
|
statusText: 'Created',
|
|
@@ -478,7 +636,11 @@ describe('net/http override', () => {
|
|
|
478
636
|
return null
|
|
479
637
|
},
|
|
480
638
|
}
|
|
481
|
-
const [req] = NewRequest(
|
|
639
|
+
const [req] = NewRequest(
|
|
640
|
+
MethodPost,
|
|
641
|
+
'https://example.invalid/upload',
|
|
642
|
+
requestBody,
|
|
643
|
+
)
|
|
482
644
|
Header_Set(req!.Header, 'Range', 'bytes=0-9')
|
|
483
645
|
Header_Set(req!.Header, 'Authorization', 'Bearer test')
|
|
484
646
|
|
|
@@ -491,6 +653,135 @@ describe('net/http override', () => {
|
|
|
491
653
|
expect(requestBodyClosed).toBe(true)
|
|
492
654
|
})
|
|
493
655
|
|
|
656
|
+
it('returns from fetch-backed RoundTrip after response headers', async () => {
|
|
657
|
+
let arrayBufferCalled = false
|
|
658
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
659
|
+
configurable: true,
|
|
660
|
+
writable: true,
|
|
661
|
+
value: async () => {
|
|
662
|
+
const response = new globalThis.Response(null, {
|
|
663
|
+
status: StatusOK,
|
|
664
|
+
statusText: 'OK',
|
|
665
|
+
headers: { 'Content-Length': '5' },
|
|
666
|
+
})
|
|
667
|
+
Object.defineProperty(response, 'arrayBuffer', {
|
|
668
|
+
configurable: true,
|
|
669
|
+
value: () => {
|
|
670
|
+
arrayBufferCalled = true
|
|
671
|
+
return new Promise<ArrayBuffer>(() => {})
|
|
672
|
+
},
|
|
673
|
+
})
|
|
674
|
+
return response
|
|
675
|
+
},
|
|
676
|
+
})
|
|
677
|
+
const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
|
|
678
|
+
|
|
679
|
+
const [resp, err] = await Promise.race([
|
|
680
|
+
DefaultTransport.RoundTrip(req),
|
|
681
|
+
new Promise<never>((_, reject) =>
|
|
682
|
+
setTimeout(() => reject(new Error('RoundTrip did not return')), 25),
|
|
683
|
+
),
|
|
684
|
+
])
|
|
685
|
+
|
|
686
|
+
expect(err).toBeNull()
|
|
687
|
+
expect(resp?.StatusCode).toBe(StatusOK)
|
|
688
|
+
expect(resp?.ContentLength).toBe(5)
|
|
689
|
+
expect(arrayBufferCalled).toBe(false)
|
|
690
|
+
expect(resp?.Body?.Close()).toBeNull()
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
it('reads fetch-backed response bodies after RoundTrip returns', async () => {
|
|
694
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
695
|
+
configurable: true,
|
|
696
|
+
writable: true,
|
|
697
|
+
value: async () =>
|
|
698
|
+
new globalThis.Response('accepted', {
|
|
699
|
+
status: StatusCreated,
|
|
700
|
+
statusText: 'Created',
|
|
701
|
+
headers: { 'Content-Length': '8' },
|
|
702
|
+
}),
|
|
703
|
+
})
|
|
704
|
+
const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
|
|
705
|
+
|
|
706
|
+
const [resp, err] = await DefaultTransport.RoundTrip(req)
|
|
707
|
+
const buf = new Uint8Array(16)
|
|
708
|
+
const [n, readErr] = await (resp!.Body!.Read(buf) as any)
|
|
709
|
+
|
|
710
|
+
expect(err).toBeNull()
|
|
711
|
+
expect(readErr).toBeNull()
|
|
712
|
+
expect(n).toBe(8)
|
|
713
|
+
expect(Buffer.from(buf.subarray(0, n)).toString('utf8')).toBe('accepted')
|
|
714
|
+
expect(resp?.Body?.Close()).toBeNull()
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
it('aborts pending fetches when the request context is canceled', async () => {
|
|
718
|
+
let fetchSignal: AbortSignal | undefined
|
|
719
|
+
let fetchStarted: (() => void) | null = null
|
|
720
|
+
const fetchStartedPromise = new Promise<void>((resolve) => {
|
|
721
|
+
fetchStarted = resolve
|
|
722
|
+
})
|
|
723
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
724
|
+
configurable: true,
|
|
725
|
+
writable: true,
|
|
726
|
+
value: async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
727
|
+
fetchSignal = init?.signal ?? undefined
|
|
728
|
+
fetchStarted?.()
|
|
729
|
+
return new Promise<globalThis.Response>((_resolve, reject) => {
|
|
730
|
+
fetchSignal?.addEventListener('abort', () => {
|
|
731
|
+
reject(new Error('aborted'))
|
|
732
|
+
})
|
|
733
|
+
})
|
|
734
|
+
},
|
|
735
|
+
})
|
|
736
|
+
const [ctx, cancel] = context.WithCancel(context.Background())
|
|
737
|
+
const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
|
|
738
|
+
|
|
739
|
+
const roundTrip = DefaultTransport.RoundTrip(req!.WithContext(ctx))
|
|
740
|
+
await fetchStartedPromise
|
|
741
|
+
cancel?.()
|
|
742
|
+
const [resp, err] = await roundTrip
|
|
743
|
+
|
|
744
|
+
expect(resp).toBeNull()
|
|
745
|
+
expect(err).toBe(context.Canceled)
|
|
746
|
+
expect(fetchSignal?.aborted).toBe(true)
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
it('aborts pending fetch body reads when the request context is canceled', async () => {
|
|
750
|
+
let fetchSignal: AbortSignal | undefined
|
|
751
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
752
|
+
configurable: true,
|
|
753
|
+
writable: true,
|
|
754
|
+
value: async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
755
|
+
fetchSignal = init?.signal ?? undefined
|
|
756
|
+
const response = new globalThis.Response(null, { status: StatusOK })
|
|
757
|
+
Object.defineProperty(response, 'arrayBuffer', {
|
|
758
|
+
configurable: true,
|
|
759
|
+
value: () =>
|
|
760
|
+
new Promise<ArrayBuffer>((_resolve, reject) => {
|
|
761
|
+
fetchSignal?.addEventListener('abort', () => {
|
|
762
|
+
reject(new Error('body aborted'))
|
|
763
|
+
})
|
|
764
|
+
}),
|
|
765
|
+
})
|
|
766
|
+
return response
|
|
767
|
+
},
|
|
768
|
+
})
|
|
769
|
+
const [ctx, cancel] = context.WithCancel(context.Background())
|
|
770
|
+
const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
|
|
771
|
+
|
|
772
|
+
const [resp, roundTripErr] = await DefaultTransport.RoundTrip(
|
|
773
|
+
req!.WithContext(ctx),
|
|
774
|
+
)
|
|
775
|
+
const read = resp!.Body!.Read(new Uint8Array(8)) as any
|
|
776
|
+
cancel?.()
|
|
777
|
+
const [n, readErr] = await read
|
|
778
|
+
|
|
779
|
+
expect(roundTripErr).toBeNull()
|
|
780
|
+
expect(n).toBe(0)
|
|
781
|
+
expect(readErr).toBe(context.Canceled)
|
|
782
|
+
expect(fetchSignal?.aborted).toBe(true)
|
|
783
|
+
})
|
|
784
|
+
|
|
494
785
|
it('closes request bodies when fetch body reads fail', async () => {
|
|
495
786
|
let requestBodyClosed = false
|
|
496
787
|
Object.defineProperty(globalThis, 'fetch', {
|
|
@@ -523,15 +814,20 @@ describe('net/http override', () => {
|
|
|
523
814
|
writable: true,
|
|
524
815
|
value: undefined,
|
|
525
816
|
})
|
|
526
|
-
const [unsupportedReq] = NewRequest(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
817
|
+
const [unsupportedReq] = NewRequest(
|
|
818
|
+
MethodPost,
|
|
819
|
+
'https://example.invalid/upload',
|
|
820
|
+
{
|
|
821
|
+
Read: () => [0, null],
|
|
822
|
+
Close: () => {
|
|
823
|
+
unsupportedClosed = true
|
|
824
|
+
return null
|
|
825
|
+
},
|
|
531
826
|
},
|
|
532
|
-
|
|
827
|
+
)
|
|
533
828
|
|
|
534
|
-
const [unsupportedResp, unsupportedErr] =
|
|
829
|
+
const [unsupportedResp, unsupportedErr] =
|
|
830
|
+
await DefaultTransport.RoundTrip(unsupportedReq)
|
|
535
831
|
|
|
536
832
|
expect(unsupportedResp).toBeNull()
|
|
537
833
|
expect(unsupportedErr?.Error()).toContain('Client.Do is not implemented')
|
|
@@ -547,15 +843,21 @@ describe('net/http override', () => {
|
|
|
547
843
|
})
|
|
548
844
|
const [ctx, cancel] = context.WithCancel(context.Background())
|
|
549
845
|
cancel?.()
|
|
550
|
-
const [canceledReq] = NewRequest(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
846
|
+
const [canceledReq] = NewRequest(
|
|
847
|
+
MethodPost,
|
|
848
|
+
'https://example.invalid/upload',
|
|
849
|
+
{
|
|
850
|
+
Read: () => [0, null],
|
|
851
|
+
Close: () => {
|
|
852
|
+
canceledClosed = true
|
|
853
|
+
return null
|
|
854
|
+
},
|
|
555
855
|
},
|
|
556
|
-
|
|
856
|
+
)
|
|
557
857
|
|
|
558
|
-
const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(
|
|
858
|
+
const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(
|
|
859
|
+
canceledReq!.WithContext(ctx),
|
|
860
|
+
)
|
|
559
861
|
|
|
560
862
|
expect(canceledResp).toBeNull()
|
|
561
863
|
expect(canceledErr).toBe(context.Canceled)
|
|
@@ -670,10 +972,14 @@ describe('net/http override', () => {
|
|
|
670
972
|
|
|
671
973
|
it('posts URL-encoded forms through clients and package helper', async () => {
|
|
672
974
|
const transport = {
|
|
673
|
-
async RoundTrip(
|
|
975
|
+
async RoundTrip(
|
|
976
|
+
got: Request | $.VarRef<Request> | null,
|
|
977
|
+
): Promise<[Response | null, $.GoError]> {
|
|
674
978
|
const request = $.pointerValue<Request>(got)
|
|
675
979
|
expect(request.Method).toBe(MethodPost)
|
|
676
|
-
expect(Header_Get(request.Header, 'Content-Type')).toBe(
|
|
980
|
+
expect(Header_Get(request.Header, 'Content-Type')).toBe(
|
|
981
|
+
'application/x-www-form-urlencoded',
|
|
982
|
+
)
|
|
677
983
|
const [data, err] = await io.ReadAll(request.Body!)
|
|
678
984
|
expect(err).toBeNull()
|
|
679
985
|
expect($.bytesToString(data)).toBe('a=one&a=two&space=x+y')
|
|
@@ -686,7 +992,10 @@ describe('net/http override', () => {
|
|
|
686
992
|
])
|
|
687
993
|
const client = new Client({ Transport: transport })
|
|
688
994
|
|
|
689
|
-
const [clientResp, clientErr] = await client.PostForm(
|
|
995
|
+
const [clientResp, clientErr] = await client.PostForm(
|
|
996
|
+
'https://example.invalid/form',
|
|
997
|
+
form,
|
|
998
|
+
)
|
|
690
999
|
expect(clientErr).toBeNull()
|
|
691
1000
|
expect(clientResp?.StatusCode).toBe(StatusOK)
|
|
692
1001
|
|
|
@@ -712,7 +1021,10 @@ describe('net/http override', () => {
|
|
|
712
1021
|
|
|
713
1022
|
Header_Add(header, 'x-pack-id', 'pack-1')
|
|
714
1023
|
Header_Add(header, 'X-Pack-Id', 'pack-2')
|
|
715
|
-
expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual([
|
|
1024
|
+
expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual([
|
|
1025
|
+
'pack-1',
|
|
1026
|
+
'pack-2',
|
|
1027
|
+
])
|
|
716
1028
|
|
|
717
1029
|
Header_Del(header, 'x-pack-id')
|
|
718
1030
|
expect(Header_Get(header, 'X-Pack-ID')).toBe('')
|
|
@@ -721,7 +1033,7 @@ describe('net/http override', () => {
|
|
|
721
1033
|
it('accepts server context and shutdown surfaces', () => {
|
|
722
1034
|
const srv = new Server({
|
|
723
1035
|
Addr: ':0',
|
|
724
|
-
BaseContext: () => ({} as any
|
|
1036
|
+
BaseContext: () => ({}) as any,
|
|
725
1037
|
ReadHeaderTimeout: 10,
|
|
726
1038
|
})
|
|
727
1039
|
|
|
@@ -729,7 +1041,9 @@ describe('net/http override', () => {
|
|
|
729
1041
|
expect(srv.BaseContext?.(null)).toEqual({})
|
|
730
1042
|
expect(srv.ReadHeaderTimeout).toBe(10)
|
|
731
1043
|
expect(srv.Shutdown({} as any)).toBeNull()
|
|
732
|
-
expect(srv.ListenAndServeTLS('cert.pem', 'key.pem')?.Error()).toBe(
|
|
1044
|
+
expect(srv.ListenAndServeTLS('cert.pem', 'key.pem')?.Error()).toBe(
|
|
1045
|
+
'net/http: Server.ListenAndServeTLS is not implemented in GoScript',
|
|
1046
|
+
)
|
|
733
1047
|
})
|
|
734
1048
|
|
|
735
1049
|
it('supports handler functions and not-found responses for typechecked server tests', () => {
|
|
@@ -749,6 +1063,116 @@ describe('net/http override', () => {
|
|
|
749
1063
|
expect(writes).toEqual(['status:404', '404 page not found\n'])
|
|
750
1064
|
})
|
|
751
1065
|
|
|
1066
|
+
it('accepts nullable handler function receivers for generated method calls', () => {
|
|
1067
|
+
const writer: ResponseWriter = {
|
|
1068
|
+
Header: () => new Header(),
|
|
1069
|
+
Write: (p) => [p?.length ?? 0, null],
|
|
1070
|
+
WriteHeader: () => undefined,
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
expect(typeof HandlerFunc_ServeHTTP).toBe('function')
|
|
1074
|
+
expect(writer.Header()).toBeInstanceOf(Header)
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
it('registers Handler for generic runtime assertions', () => {
|
|
1078
|
+
const handlerFunc = (_w: ResponseWriter | null, _r: Request | null) =>
|
|
1079
|
+
undefined
|
|
1080
|
+
const handler = $.namedValueInterfaceValue(
|
|
1081
|
+
$.namedFunction(
|
|
1082
|
+
$.functionValue(handlerFunc, {
|
|
1083
|
+
kind: $.TypeKind.Function,
|
|
1084
|
+
params: [
|
|
1085
|
+
'http.ResponseWriter',
|
|
1086
|
+
{
|
|
1087
|
+
kind: $.TypeKind.Pointer,
|
|
1088
|
+
elemType: 'http.Request',
|
|
1089
|
+
},
|
|
1090
|
+
],
|
|
1091
|
+
results: [],
|
|
1092
|
+
}),
|
|
1093
|
+
'http.HandlerFunc',
|
|
1094
|
+
{
|
|
1095
|
+
kind: $.TypeKind.Function,
|
|
1096
|
+
name: 'http.HandlerFunc',
|
|
1097
|
+
params: [
|
|
1098
|
+
'http.ResponseWriter',
|
|
1099
|
+
{
|
|
1100
|
+
kind: $.TypeKind.Pointer,
|
|
1101
|
+
elemType: 'http.Request',
|
|
1102
|
+
},
|
|
1103
|
+
],
|
|
1104
|
+
results: [],
|
|
1105
|
+
},
|
|
1106
|
+
),
|
|
1107
|
+
'http.HandlerFunc',
|
|
1108
|
+
{
|
|
1109
|
+
ServeHTTP: (receiver, ...args) =>
|
|
1110
|
+
HandlerFunc_ServeHTTP(
|
|
1111
|
+
$.isVarRef(receiver) ? receiver.value : receiver,
|
|
1112
|
+
...(args as [ResponseWriter | null, Request | null]),
|
|
1113
|
+
),
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
kind: $.TypeKind.Function,
|
|
1117
|
+
name: 'http.HandlerFunc',
|
|
1118
|
+
params: [
|
|
1119
|
+
'http.ResponseWriter',
|
|
1120
|
+
{
|
|
1121
|
+
kind: $.TypeKind.Pointer,
|
|
1122
|
+
elemType: 'http.Request',
|
|
1123
|
+
},
|
|
1124
|
+
],
|
|
1125
|
+
results: [],
|
|
1126
|
+
},
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
const [asserted, ok] = $.typeAssertTuple<{
|
|
1130
|
+
ServeHTTP(w: ResponseWriter | null, r: Request | null): void
|
|
1131
|
+
}>(handler, 'http.Handler')
|
|
1132
|
+
|
|
1133
|
+
expect(ok).toBe(true)
|
|
1134
|
+
expect(asserted).toBe(handler)
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
it('rejects named method wrappers with incompatible interface arity', () => {
|
|
1138
|
+
const handler = $.namedValueInterfaceValue(
|
|
1139
|
+
$.namedFunction(
|
|
1140
|
+
$.functionValue(() => undefined, {
|
|
1141
|
+
kind: $.TypeKind.Function,
|
|
1142
|
+
params: [],
|
|
1143
|
+
results: [],
|
|
1144
|
+
}),
|
|
1145
|
+
'http.HandlerFunc',
|
|
1146
|
+
{
|
|
1147
|
+
kind: $.TypeKind.Function,
|
|
1148
|
+
name: 'http.HandlerFunc',
|
|
1149
|
+
params: [],
|
|
1150
|
+
results: [],
|
|
1151
|
+
},
|
|
1152
|
+
),
|
|
1153
|
+
'http.HandlerFunc',
|
|
1154
|
+
{
|
|
1155
|
+
ServeHTTP: (receiver, ...args) =>
|
|
1156
|
+
HandlerFunc_ServeHTTP(
|
|
1157
|
+
$.isVarRef(receiver) ? receiver.value : receiver,
|
|
1158
|
+
...(args as [ResponseWriter | null, Request | null]),
|
|
1159
|
+
),
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
kind: $.TypeKind.Function,
|
|
1163
|
+
name: 'http.HandlerFunc',
|
|
1164
|
+
params: [],
|
|
1165
|
+
results: [],
|
|
1166
|
+
},
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
const [, ok] = $.typeAssertTuple<{
|
|
1170
|
+
ServeHTTP(w: ResponseWriter | null, r: Request | null): void
|
|
1171
|
+
}>(handler, 'http.Handler')
|
|
1172
|
+
|
|
1173
|
+
expect(ok).toBe(false)
|
|
1174
|
+
})
|
|
1175
|
+
|
|
752
1176
|
it('routes through default mux and handler helper exports', () => {
|
|
753
1177
|
const writes: string[] = []
|
|
754
1178
|
const writer: ResponseWriter = {
|
|
@@ -778,7 +1202,7 @@ describe('net/http override', () => {
|
|
|
778
1202
|
])
|
|
779
1203
|
})
|
|
780
1204
|
|
|
781
|
-
it('formats Set-Cookie headers for browser bootstrap routes', () => {
|
|
1205
|
+
it('formats Set-Cookie headers for browser bootstrap routes', async () => {
|
|
782
1206
|
const header = new Header()
|
|
783
1207
|
const writer: ResponseWriter = {
|
|
784
1208
|
Header: () => header,
|
|
@@ -786,15 +1210,18 @@ describe('net/http override', () => {
|
|
|
786
1210
|
WriteHeader: () => undefined,
|
|
787
1211
|
}
|
|
788
1212
|
|
|
789
|
-
SetCookie(
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1213
|
+
await SetCookie(
|
|
1214
|
+
writer,
|
|
1215
|
+
new Cookie({
|
|
1216
|
+
Name: 'spacewave_local_capability',
|
|
1217
|
+
Value: 'token',
|
|
1218
|
+
Path: '/',
|
|
1219
|
+
MaxAge: 300,
|
|
1220
|
+
HttpOnly: true,
|
|
1221
|
+
Secure: true,
|
|
1222
|
+
SameSite: SameSiteStrictMode,
|
|
1223
|
+
}),
|
|
1224
|
+
)
|
|
798
1225
|
|
|
799
1226
|
expect(Array.from(header.get('Set-Cookie') ?? [])).toEqual([
|
|
800
1227
|
'spacewave_local_capability=token; Path=/; Max-Age=300; HttpOnly; Secure; SameSite=Strict',
|
|
@@ -806,29 +1233,46 @@ describe('net/http override', () => {
|
|
|
806
1233
|
|
|
807
1234
|
expect(err).toBeNull()
|
|
808
1235
|
expect(parsed.Unix()).toBe(784111777)
|
|
809
|
-
expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe(
|
|
1236
|
+
expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe(
|
|
1237
|
+
'text/html; charset=utf-8',
|
|
1238
|
+
)
|
|
810
1239
|
expect(
|
|
811
|
-
DetectContentType(
|
|
1240
|
+
DetectContentType(
|
|
1241
|
+
new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
1242
|
+
),
|
|
812
1243
|
).toBe('image/png')
|
|
813
|
-
expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe(
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
expect(DetectContentType($.stringToBytes('
|
|
1244
|
+
expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe(
|
|
1245
|
+
'image/jpeg',
|
|
1246
|
+
)
|
|
1247
|
+
expect(DetectContentType($.stringToBytes('%PDF-1.7'))).toBe(
|
|
1248
|
+
'application/pdf',
|
|
1249
|
+
)
|
|
1250
|
+
expect(DetectContentType($.stringToBytes('RIFFxxxxWEBPVP'))).toBe(
|
|
1251
|
+
'image/webp',
|
|
1252
|
+
)
|
|
1253
|
+
expect(DetectContentType($.stringToBytes('FORMxxxxAIFF'))).toBe(
|
|
1254
|
+
'audio/aiff',
|
|
1255
|
+
)
|
|
817
1256
|
expect(DetectContentType($.stringToBytes('ID3payload'))).toBe('audio/mpeg')
|
|
818
1257
|
expect(DetectContentType($.stringToBytes('wOFFpayload'))).toBe('font/woff')
|
|
819
|
-
expect(
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
expect(DetectContentType(new Uint8Array())).toBe(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1258
|
+
expect(
|
|
1259
|
+
DetectContentType(
|
|
1260
|
+
new Uint8Array([
|
|
1261
|
+
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f,
|
|
1262
|
+
0x6d, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x70, 0x34, 0x31, 0x00, 0x00,
|
|
1263
|
+
0x00, 0x00,
|
|
1264
|
+
]),
|
|
1265
|
+
),
|
|
1266
|
+
).toBe('video/mp4')
|
|
1267
|
+
expect(DetectContentType(new Uint8Array([0x00, 0x01, 0x02]))).toBe(
|
|
1268
|
+
'application/octet-stream',
|
|
1269
|
+
)
|
|
1270
|
+
expect(DetectContentType(new Uint8Array())).toBe(
|
|
1271
|
+
'text/plain; charset=utf-8',
|
|
1272
|
+
)
|
|
1273
|
+
})
|
|
1274
|
+
|
|
1275
|
+
it('exports file server interface shapes', async () => {
|
|
832
1276
|
const file: File = {
|
|
833
1277
|
Close: () => null,
|
|
834
1278
|
Read: (p) => [p?.length ?? 0, null],
|
|
@@ -837,11 +1281,15 @@ describe('net/http override', () => {
|
|
|
837
1281
|
Stat: () => [null, null],
|
|
838
1282
|
}
|
|
839
1283
|
const fsys: FileSystem = {
|
|
840
|
-
Open: (name) =>
|
|
1284
|
+
Open: (name) =>
|
|
1285
|
+
name === 'ok' ? [file, null] : [null, new Error('missing')],
|
|
841
1286
|
}
|
|
842
1287
|
|
|
843
|
-
|
|
844
|
-
|
|
1288
|
+
const [okFile] = await fsys.Open('ok')
|
|
1289
|
+
const [, missingErr] = await fsys.Open('missing')
|
|
1290
|
+
|
|
1291
|
+
expect(okFile).toBe(file)
|
|
1292
|
+
expect(missingErr?.message).toBe('missing')
|
|
845
1293
|
})
|
|
846
1294
|
|
|
847
1295
|
it('serves files and omits HEAD response bodies', async () => {
|
|
@@ -853,23 +1301,26 @@ describe('net/http override', () => {
|
|
|
853
1301
|
Read: (p: Uint8Array) => reader.Read(p),
|
|
854
1302
|
Seek: (offset: number, whence: number) => reader.Seek(offset, whence),
|
|
855
1303
|
Readdir: () => [null, null] as [null, null],
|
|
856
|
-
Stat: () =>
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1304
|
+
Stat: () =>
|
|
1305
|
+
[
|
|
1306
|
+
{
|
|
1307
|
+
IsDir: () => false,
|
|
1308
|
+
ModTime: () => null as never,
|
|
1309
|
+
Mode: () => 0,
|
|
1310
|
+
Name: () => 'file.txt',
|
|
1311
|
+
Size: () => 5,
|
|
1312
|
+
Sys: () => null,
|
|
1313
|
+
},
|
|
1314
|
+
null,
|
|
1315
|
+
] as const,
|
|
867
1316
|
}
|
|
868
1317
|
}
|
|
869
1318
|
const root = FS({
|
|
870
1319
|
Open: (name) => {
|
|
871
1320
|
opened.push(name)
|
|
872
|
-
return name === 'file.txt' ?
|
|
1321
|
+
return name === 'file.txt' ?
|
|
1322
|
+
[makeFile(), null]
|
|
1323
|
+
: [null, new Error('missing')]
|
|
873
1324
|
},
|
|
874
1325
|
})
|
|
875
1326
|
const writes: string[] = []
|
|
@@ -883,17 +1334,26 @@ describe('net/http override', () => {
|
|
|
883
1334
|
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
884
1335
|
}
|
|
885
1336
|
const handler = FileServer(root)
|
|
886
|
-
const [getReq] = NewRequest(
|
|
1337
|
+
const [getReq] = NewRequest(
|
|
1338
|
+
MethodGet,
|
|
1339
|
+
'http://example.invalid/../file.txt',
|
|
1340
|
+
null,
|
|
1341
|
+
)
|
|
887
1342
|
|
|
888
1343
|
await handler.ServeHTTP(writer, getReq)
|
|
889
1344
|
|
|
890
1345
|
expect(opened).toEqual(['file.txt'])
|
|
891
1346
|
expect(writes).toEqual(['status:200', 'hello'])
|
|
892
1347
|
expect(Header_Get(header, 'Content-Length')).toBe('5')
|
|
1348
|
+
expect(Header_Get(header, 'Content-Type')).toBe('text/plain; charset=utf-8')
|
|
893
1349
|
|
|
894
1350
|
writes.length = 0
|
|
895
1351
|
opened.length = 0
|
|
896
|
-
const [headReq] = NewRequest(
|
|
1352
|
+
const [headReq] = NewRequest(
|
|
1353
|
+
MethodHead,
|
|
1354
|
+
'http://example.invalid/file.txt',
|
|
1355
|
+
null,
|
|
1356
|
+
)
|
|
897
1357
|
|
|
898
1358
|
await handler.ServeHTTP(writer, headReq)
|
|
899
1359
|
|
|
@@ -901,6 +1361,249 @@ describe('net/http override', () => {
|
|
|
901
1361
|
expect(writes).toEqual(['status:200'])
|
|
902
1362
|
})
|
|
903
1363
|
|
|
1364
|
+
it('serves files from async file systems and file methods', async () => {
|
|
1365
|
+
const closeCalls: string[] = []
|
|
1366
|
+
const root = {
|
|
1367
|
+
async Open(name: string) {
|
|
1368
|
+
if (name !== 'async.txt') {
|
|
1369
|
+
return [null, new Error('missing')]
|
|
1370
|
+
}
|
|
1371
|
+
let offset = 0
|
|
1372
|
+
const file = {
|
|
1373
|
+
async Close() {
|
|
1374
|
+
closeCalls.push(name)
|
|
1375
|
+
return null
|
|
1376
|
+
},
|
|
1377
|
+
async Read(p) {
|
|
1378
|
+
const data = $.stringToBytes('async-body')
|
|
1379
|
+
if (offset >= data.length) {
|
|
1380
|
+
return [0, io.EOF]
|
|
1381
|
+
}
|
|
1382
|
+
const n = Math.min(p.length, data.length - offset)
|
|
1383
|
+
p.set(data.subarray(offset, offset + n), 0)
|
|
1384
|
+
offset += n
|
|
1385
|
+
return [n, null]
|
|
1386
|
+
},
|
|
1387
|
+
async Seek(seekOffset) {
|
|
1388
|
+
offset = Number(seekOffset)
|
|
1389
|
+
return [offset, null]
|
|
1390
|
+
},
|
|
1391
|
+
async Readdir() {
|
|
1392
|
+
return [null, null]
|
|
1393
|
+
},
|
|
1394
|
+
async Stat() {
|
|
1395
|
+
return [
|
|
1396
|
+
{
|
|
1397
|
+
IsDir: () => false,
|
|
1398
|
+
ModTime: () => null as never,
|
|
1399
|
+
Mode: () => 0,
|
|
1400
|
+
Name: () => name,
|
|
1401
|
+
Size: () => 10,
|
|
1402
|
+
Sys: () => null,
|
|
1403
|
+
},
|
|
1404
|
+
null,
|
|
1405
|
+
]
|
|
1406
|
+
},
|
|
1407
|
+
}
|
|
1408
|
+
return [file, null]
|
|
1409
|
+
},
|
|
1410
|
+
}
|
|
1411
|
+
const writes: string[] = []
|
|
1412
|
+
const header = new Header()
|
|
1413
|
+
const writer: ResponseWriter = {
|
|
1414
|
+
Header: () => header,
|
|
1415
|
+
Write: (p) => {
|
|
1416
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
1417
|
+
return [p?.length ?? 0, null]
|
|
1418
|
+
},
|
|
1419
|
+
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
1420
|
+
}
|
|
1421
|
+
const [req] = NewRequest(
|
|
1422
|
+
MethodGet,
|
|
1423
|
+
'http://example.invalid/async.txt',
|
|
1424
|
+
null,
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
await FileServer(root).ServeHTTP(writer, req)
|
|
1428
|
+
|
|
1429
|
+
expect(writes).toEqual(['status:200', 'async-body'])
|
|
1430
|
+
expect(Header_Get(header, 'Content-Length')).toBe('10')
|
|
1431
|
+
expect(closeCalls).toEqual(['async.txt'])
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1434
|
+
it('serves files through FS adapters backed by async io/fs Open', async () => {
|
|
1435
|
+
const opened: string[] = []
|
|
1436
|
+
const root = {
|
|
1437
|
+
async Open(name: string) {
|
|
1438
|
+
opened.push(name)
|
|
1439
|
+
const data = $.stringToBytes('fs-adapter')
|
|
1440
|
+
let offset = 0
|
|
1441
|
+
return [
|
|
1442
|
+
{
|
|
1443
|
+
Close: async () => null,
|
|
1444
|
+
Read: async (p: $.Slice<number>) => {
|
|
1445
|
+
if (offset >= data.length) {
|
|
1446
|
+
return [0, io.EOF] as [number, $.GoError]
|
|
1447
|
+
}
|
|
1448
|
+
const n = Math.min(p?.length ?? 0, data.length - offset)
|
|
1449
|
+
p?.set(data.subarray(offset, offset + n), 0)
|
|
1450
|
+
offset += n
|
|
1451
|
+
return [n, null] as [number, $.GoError]
|
|
1452
|
+
},
|
|
1453
|
+
Stat: async () => [
|
|
1454
|
+
{
|
|
1455
|
+
Name: () => name,
|
|
1456
|
+
Size: () => data.length,
|
|
1457
|
+
Mode: () => 0,
|
|
1458
|
+
ModTime: () => new time.Time(),
|
|
1459
|
+
IsDir: () => false,
|
|
1460
|
+
Sys: () => null,
|
|
1461
|
+
},
|
|
1462
|
+
null,
|
|
1463
|
+
],
|
|
1464
|
+
},
|
|
1465
|
+
null,
|
|
1466
|
+
]
|
|
1467
|
+
},
|
|
1468
|
+
}
|
|
1469
|
+
const writes: string[] = []
|
|
1470
|
+
const header = new Header()
|
|
1471
|
+
const writer: ResponseWriter = {
|
|
1472
|
+
Header: () => header,
|
|
1473
|
+
Write: (p) => {
|
|
1474
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
1475
|
+
return [p?.length ?? 0, null]
|
|
1476
|
+
},
|
|
1477
|
+
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
1478
|
+
}
|
|
1479
|
+
const [req] = NewRequest(
|
|
1480
|
+
MethodGet,
|
|
1481
|
+
'http://example.invalid/dir/fs-adapter.txt',
|
|
1482
|
+
null,
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
await FileServerFS(root).ServeHTTP(writer, req)
|
|
1486
|
+
|
|
1487
|
+
expect(opened).toEqual(['dir/fs-adapter.txt'])
|
|
1488
|
+
expect(writes).toEqual(['status:200', 'fs-adapter'])
|
|
1489
|
+
expect(Header_Get(header, 'Content-Length')).toBe('10')
|
|
1490
|
+
})
|
|
1491
|
+
|
|
1492
|
+
it('serves files through async response writer adapters', async () => {
|
|
1493
|
+
const root = {
|
|
1494
|
+
async Open(name: string) {
|
|
1495
|
+
const data = $.stringToBytes('async-adapter')
|
|
1496
|
+
return [
|
|
1497
|
+
{
|
|
1498
|
+
Read: (p: $.Slice<number>) => {
|
|
1499
|
+
p?.set(data.subarray(0, p.length))
|
|
1500
|
+
return [data.length, io.EOF] as [number, $.GoError]
|
|
1501
|
+
},
|
|
1502
|
+
Close: async () => null,
|
|
1503
|
+
Stat: async () => [
|
|
1504
|
+
{
|
|
1505
|
+
Name: () => name,
|
|
1506
|
+
Size: () => data.length,
|
|
1507
|
+
Mode: () => 0,
|
|
1508
|
+
ModTime: () => new time.Time(),
|
|
1509
|
+
IsDir: () => false,
|
|
1510
|
+
Sys: () => null,
|
|
1511
|
+
},
|
|
1512
|
+
null,
|
|
1513
|
+
],
|
|
1514
|
+
},
|
|
1515
|
+
null,
|
|
1516
|
+
] as [File, $.GoError]
|
|
1517
|
+
},
|
|
1518
|
+
}
|
|
1519
|
+
const writes: string[] = []
|
|
1520
|
+
const header = new Header()
|
|
1521
|
+
const writer: ResponseWriter = {
|
|
1522
|
+
Header: async () => header,
|
|
1523
|
+
Write: async (p) => {
|
|
1524
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
1525
|
+
return [p?.length ?? 0, null]
|
|
1526
|
+
},
|
|
1527
|
+
WriteHeader: async (code) => {
|
|
1528
|
+
writes.push(`status:${code}`)
|
|
1529
|
+
},
|
|
1530
|
+
}
|
|
1531
|
+
const [req] = NewRequest(
|
|
1532
|
+
MethodGet,
|
|
1533
|
+
'http://example.invalid/async-adapter.txt',
|
|
1534
|
+
null,
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
await FileServer(root).ServeHTTP(writer, req)
|
|
1538
|
+
|
|
1539
|
+
expect(writes).toEqual(['status:200', 'async-adapter'])
|
|
1540
|
+
expect(Header_Get(header, 'Content-Length')).toBe('13')
|
|
1541
|
+
})
|
|
1542
|
+
|
|
1543
|
+
it('writes file server headers before reading response bodies', async () => {
|
|
1544
|
+
let headerWritten = false
|
|
1545
|
+
let readSawHeader = false
|
|
1546
|
+
let offset = 0
|
|
1547
|
+
const data = $.stringToBytes('streamed-body')
|
|
1548
|
+
const root = {
|
|
1549
|
+
async Open(name: string) {
|
|
1550
|
+
return [
|
|
1551
|
+
{
|
|
1552
|
+
Close: async () => null,
|
|
1553
|
+
Read: async (p: $.Slice<number>) => {
|
|
1554
|
+
readSawHeader = headerWritten
|
|
1555
|
+
if (offset >= data.length) {
|
|
1556
|
+
return [0, io.EOF] as [number, $.GoError]
|
|
1557
|
+
}
|
|
1558
|
+
const n = Math.min(p?.length ?? 0, data.length - offset)
|
|
1559
|
+
p?.set(data.subarray(offset, offset + n), 0)
|
|
1560
|
+
offset += n
|
|
1561
|
+
return [n, null] as [number, $.GoError]
|
|
1562
|
+
},
|
|
1563
|
+
Seek: async () => [0, null] as [number, $.GoError],
|
|
1564
|
+
Readdir: async () => [null, null] as [null, $.GoError],
|
|
1565
|
+
Stat: async () => [
|
|
1566
|
+
{
|
|
1567
|
+
Name: () => name,
|
|
1568
|
+
Size: () => data.length,
|
|
1569
|
+
Mode: () => 0,
|
|
1570
|
+
ModTime: () => null as never,
|
|
1571
|
+
IsDir: () => false,
|
|
1572
|
+
Sys: () => null,
|
|
1573
|
+
},
|
|
1574
|
+
null,
|
|
1575
|
+
],
|
|
1576
|
+
},
|
|
1577
|
+
null,
|
|
1578
|
+
] as [File, $.GoError]
|
|
1579
|
+
},
|
|
1580
|
+
}
|
|
1581
|
+
const writes: string[] = []
|
|
1582
|
+
const header = new Header()
|
|
1583
|
+
const writer: ResponseWriter = {
|
|
1584
|
+
Header: () => header,
|
|
1585
|
+
Write: (p) => {
|
|
1586
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
1587
|
+
return [p?.length ?? 0, null]
|
|
1588
|
+
},
|
|
1589
|
+
WriteHeader: (code) => {
|
|
1590
|
+
headerWritten = true
|
|
1591
|
+
writes.push(`status:${code}`)
|
|
1592
|
+
},
|
|
1593
|
+
}
|
|
1594
|
+
const [req] = NewRequest(
|
|
1595
|
+
MethodGet,
|
|
1596
|
+
'http://example.invalid/streamed.mjs',
|
|
1597
|
+
null,
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
await FileServer(root).ServeHTTP(writer, req)
|
|
1601
|
+
|
|
1602
|
+
expect(readSawHeader).toBe(true)
|
|
1603
|
+
expect(writes).toEqual(['status:200', 'streamed-body'])
|
|
1604
|
+
expect(Header_Get(header, 'Content-Type')).toBe('text/javascript; charset=utf-8')
|
|
1605
|
+
})
|
|
1606
|
+
|
|
904
1607
|
it('awaits ServeContent writes before returning', async () => {
|
|
905
1608
|
const writes: string[] = []
|
|
906
1609
|
const writer: ResponseWriter = {
|
|
@@ -911,15 +1614,35 @@ describe('net/http override', () => {
|
|
|
911
1614
|
},
|
|
912
1615
|
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
913
1616
|
}
|
|
914
|
-
const [req] = NewRequest(
|
|
1617
|
+
const [req] = NewRequest(
|
|
1618
|
+
MethodGet,
|
|
1619
|
+
'http://example.invalid/content.txt',
|
|
1620
|
+
null,
|
|
1621
|
+
)
|
|
915
1622
|
|
|
916
|
-
await ServeContent(
|
|
1623
|
+
await ServeContent(
|
|
1624
|
+
writer,
|
|
1625
|
+
req,
|
|
1626
|
+
'content.txt',
|
|
1627
|
+
null as never,
|
|
1628
|
+
bytes.NewReader($.stringToBytes('served')),
|
|
1629
|
+
)
|
|
917
1630
|
|
|
918
1631
|
expect(writes).toEqual(['status:200', 'served'])
|
|
919
1632
|
|
|
920
1633
|
writes.length = 0
|
|
921
|
-
const [headReq] = NewRequest(
|
|
922
|
-
|
|
1634
|
+
const [headReq] = NewRequest(
|
|
1635
|
+
MethodHead,
|
|
1636
|
+
'http://example.invalid/content.txt',
|
|
1637
|
+
null,
|
|
1638
|
+
)
|
|
1639
|
+
await ServeContent(
|
|
1640
|
+
writer,
|
|
1641
|
+
headReq,
|
|
1642
|
+
'content.txt',
|
|
1643
|
+
null as never,
|
|
1644
|
+
bytes.NewReader($.stringToBytes('hidden')),
|
|
1645
|
+
)
|
|
923
1646
|
|
|
924
1647
|
expect(writes).toEqual(['status:200'])
|
|
925
1648
|
})
|
|
@@ -954,7 +1677,11 @@ describe('net/http override', () => {
|
|
|
954
1677
|
},
|
|
955
1678
|
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
956
1679
|
}
|
|
957
|
-
const [req] = NewRequest(
|
|
1680
|
+
const [req] = NewRequest(
|
|
1681
|
+
MethodGet,
|
|
1682
|
+
'http://example.invalid/eval/missing.js',
|
|
1683
|
+
null,
|
|
1684
|
+
)
|
|
958
1685
|
|
|
959
1686
|
ServeFile(writer, req, '/host-only/missing.js')
|
|
960
1687
|
|