goscript 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/cmd/go_js_wasm_exec/main.go +201 -0
- package/cmd/go_js_wasm_exec/main_test.go +83 -0
- package/cmd/goscript/{cmd_compile.go → cmd-compile.go} +7 -0
- package/cmd/goscript/cmd-test.go +14 -0
- package/cmd/goscript/cmd-test_test.go +1 -1
- package/compiler/compile-request.go +12 -9
- package/compiler/compliance_test.go +0 -1
- package/compiler/config.go +2 -0
- package/compiler/gotest/request.go +28 -0
- package/compiler/gotest/runner.go +353 -27
- package/compiler/gotest/runner_test.go +273 -1
- package/compiler/gotest/testdata/browserapi/browserapi_test.go +20 -0
- package/compiler/gotest/testdata/browserapi/go.mod +3 -0
- package/compiler/lowered-program.go +24 -17
- package/compiler/lowering.go +392 -127
- package/compiler/lowering_bench_test.go +41 -27
- package/compiler/override-facts.go +15 -0
- package/compiler/override-parity-verifier.go +450 -0
- package/compiler/override-parity.go +122 -0
- package/compiler/override-registry_test.go +559 -0
- package/compiler/protobuf-ts-binding.go +514 -0
- package/compiler/protobuf-ts-binding_test.go +172 -0
- package/compiler/semantic-model-types.go +9 -4
- package/compiler/semantic-model.go +282 -70
- package/compiler/semantic-model_test.go +82 -1
- package/compiler/service.go +20 -1
- package/compiler/skeleton_test.go +62 -8
- package/compiler/typescript-emitter.go +128 -13
- package/dist/gs/builtin/slice.d.ts +2 -1
- package/dist/gs/builtin/slice.js +29 -4
- package/dist/gs/builtin/slice.js.map +1 -1
- package/dist/gs/builtin/type.d.ts +13 -5
- package/dist/gs/builtin/type.js +153 -60
- package/dist/gs/builtin/type.js.map +1 -1
- package/dist/gs/builtin/varRef.d.ts +11 -0
- package/dist/gs/builtin/varRef.js +57 -2
- package/dist/gs/builtin/varRef.js.map +1 -1
- package/dist/gs/bytes/buffer.gs.js +1 -1
- package/dist/gs/bytes/buffer.gs.js.map +1 -1
- package/dist/gs/bytes/reader.gs.js +1 -1
- package/dist/gs/bytes/reader.gs.js.map +1 -1
- package/dist/gs/compress/zlib/index.d.ts +10 -3
- package/dist/gs/compress/zlib/index.js +50 -16
- package/dist/gs/compress/zlib/index.js.map +1 -1
- package/dist/gs/encoding/json/index.d.ts +114 -0
- package/dist/gs/encoding/json/index.js +544 -36
- package/dist/gs/encoding/json/index.js.map +1 -1
- package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +100 -0
- package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +564 -0
- package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
- package/dist/gs/github.com/pkg/errors/errors.js +54 -30
- package/dist/gs/github.com/pkg/errors/errors.js.map +1 -1
- package/dist/gs/go/scanner/index.d.ts +2 -0
- package/dist/gs/go/scanner/index.js +29 -5
- package/dist/gs/go/scanner/index.js.map +1 -1
- package/dist/gs/go/token/index.js +22 -6
- package/dist/gs/go/token/index.js.map +1 -1
- package/dist/gs/hash/index.d.ts +6 -0
- package/dist/gs/hash/index.js +20 -0
- package/dist/gs/hash/index.js.map +1 -1
- package/dist/gs/internal/goarch/index.d.ts +43 -3
- package/dist/gs/internal/goarch/index.js +42 -10
- package/dist/gs/internal/goarch/index.js.map +1 -1
- package/dist/gs/io/fs/fs.js +26 -14
- package/dist/gs/io/fs/fs.js.map +1 -1
- package/dist/gs/io/fs/readdir.js +4 -2
- package/dist/gs/io/fs/readdir.js.map +1 -1
- package/dist/gs/io/fs/sub.js +8 -1
- package/dist/gs/io/fs/sub.js.map +1 -1
- package/dist/gs/io/io.d.ts +2 -0
- package/dist/gs/io/io.js.map +1 -1
- package/dist/gs/math/bits/index.d.ts +5 -0
- package/dist/gs/math/bits/index.js +16 -4
- package/dist/gs/math/bits/index.js.map +1 -1
- package/dist/gs/mime/index.d.ts +16 -0
- package/dist/gs/mime/index.js +315 -6
- package/dist/gs/mime/index.js.map +1 -1
- package/dist/gs/net/http/httptest/index.d.ts +12 -0
- package/dist/gs/net/http/httptest/index.js +85 -6
- package/dist/gs/net/http/httptest/index.js.map +1 -1
- package/dist/gs/net/http/index.d.ts +300 -5
- package/dist/gs/net/http/index.js +1598 -58
- package/dist/gs/net/http/index.js.map +1 -1
- package/dist/gs/os/dir_unix.gs.js +1 -1
- package/dist/gs/os/dir_unix.gs.js.map +1 -1
- package/dist/gs/os/error.gs.js +1 -1
- package/dist/gs/os/error.gs.js.map +1 -1
- package/dist/gs/os/exec.gs.d.ts +1 -0
- package/dist/gs/os/exec.gs.js +4 -8
- package/dist/gs/os/exec.gs.js.map +1 -1
- package/dist/gs/os/exec_posix.gs.js +1 -1
- package/dist/gs/os/exec_posix.gs.js.map +1 -1
- package/dist/gs/os/index.d.ts +1 -1
- package/dist/gs/os/index.js +1 -1
- package/dist/gs/os/index.js.map +1 -1
- package/dist/gs/os/proc.gs.d.ts +4 -0
- package/dist/gs/os/proc.gs.js +12 -6
- package/dist/gs/os/proc.gs.js.map +1 -1
- package/dist/gs/os/root_js.gs.js +1 -1
- package/dist/gs/os/root_js.gs.js.map +1 -1
- package/dist/gs/os/types.gs.js +1 -1
- package/dist/gs/os/types.gs.js.map +1 -1
- package/dist/gs/os/types_js.gs.js +1 -1
- package/dist/gs/os/types_js.gs.js.map +1 -1
- package/dist/gs/os/types_unix.gs.js +1 -1
- package/dist/gs/os/types_unix.gs.js.map +1 -1
- package/dist/gs/path/path.js +11 -7
- package/dist/gs/path/path.js.map +1 -1
- package/dist/gs/reflect/index.d.ts +5 -4
- package/dist/gs/reflect/index.js +4 -3
- package/dist/gs/reflect/index.js.map +1 -1
- package/dist/gs/reflect/map.js +15 -0
- package/dist/gs/reflect/map.js.map +1 -1
- package/dist/gs/reflect/type.d.ts +25 -6
- package/dist/gs/reflect/type.js +1418 -228
- package/dist/gs/reflect/type.js.map +1 -1
- package/dist/gs/reflect/types.d.ts +14 -6
- package/dist/gs/reflect/types.js +35 -1
- package/dist/gs/reflect/types.js.map +1 -1
- package/dist/gs/reflect/value.d.ts +1 -0
- package/dist/gs/reflect/value.js +83 -41
- package/dist/gs/reflect/value.js.map +1 -1
- package/dist/gs/reflect/visiblefields.js +4 -140
- package/dist/gs/reflect/visiblefields.js.map +1 -1
- package/dist/gs/runtime/pprof/index.d.ts +8 -2
- package/dist/gs/runtime/pprof/index.js +50 -30
- package/dist/gs/runtime/pprof/index.js.map +1 -1
- package/dist/gs/runtime/runtime.js +5 -4
- package/dist/gs/runtime/runtime.js.map +1 -1
- package/dist/gs/runtime/trace/index.js +5 -19
- package/dist/gs/runtime/trace/index.js.map +1 -1
- package/dist/gs/strconv/atoi.gs.js +1 -1
- package/dist/gs/strconv/atoi.gs.js.map +1 -1
- package/dist/gs/strconv/complex.gs.d.ts +3 -0
- package/dist/gs/strconv/complex.gs.js +148 -0
- package/dist/gs/strconv/complex.gs.js.map +1 -0
- package/dist/gs/strconv/index.d.ts +1 -0
- package/dist/gs/strconv/index.js +1 -0
- package/dist/gs/strconv/index.js.map +1 -1
- package/dist/gs/strings/builder.js +1 -1
- package/dist/gs/strings/reader.js +9 -5
- package/dist/gs/strings/reader.js.map +1 -1
- package/dist/gs/strings/replace.js +15 -7
- package/dist/gs/strings/replace.js.map +1 -1
- package/dist/gs/strings/strings.d.ts +5 -0
- package/dist/gs/strings/strings.js +57 -5
- package/dist/gs/strings/strings.js.map +1 -1
- package/dist/gs/sync/atomic/type.gs.js +9 -9
- package/dist/gs/sync/atomic/type.gs.js.map +1 -1
- package/dist/gs/sync/atomic/value.gs.js +2 -2
- package/dist/gs/sync/atomic/value.gs.js.map +1 -1
- package/dist/gs/syscall/env.js +22 -14
- package/dist/gs/syscall/env.js.map +1 -1
- package/dist/gs/testing/testing.js +55 -13
- package/dist/gs/testing/testing.js.map +1 -1
- package/dist/gs/time/time.d.ts +24 -1
- package/dist/gs/time/time.js +43 -3
- package/dist/gs/time/time.js.map +1 -1
- package/dist/gs/unique/index.js +7 -1
- package/dist/gs/unique/index.js.map +1 -1
- package/go.mod +3 -3
- package/go.sum +16 -0
- package/gs/builtin/runtime-contract.test.ts +218 -21
- package/gs/builtin/slice.ts +44 -4
- package/gs/builtin/type.ts +226 -59
- package/gs/builtin/varRef.ts +85 -2
- package/gs/bytes/buffer.gs.ts +1 -1
- package/gs/bytes/reader.gs.ts +1 -1
- package/gs/compress/zlib/index.test.ts +62 -1
- package/gs/compress/zlib/index.ts +53 -16
- package/gs/compress/zlib/parity.json +51 -0
- package/gs/encoding/json/index.test.ts +360 -6
- package/gs/encoding/json/index.ts +679 -38
- package/gs/encoding/json/parity.json +81 -0
- package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +211 -3
- package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +857 -1
- package/gs/github.com/pkg/errors/errors.ts +54 -30
- package/gs/go/scanner/index.test.ts +39 -56
- package/gs/go/scanner/index.ts +33 -5
- package/gs/go/scanner/parity.json +27 -0
- package/gs/go/token/index.ts +22 -6
- package/gs/hash/index.test.ts +20 -33
- package/gs/hash/index.ts +28 -0
- package/gs/hash/parity.json +21 -0
- package/gs/internal/goarch/index.test.ts +32 -0
- package/gs/internal/goarch/index.ts +45 -13
- package/gs/internal/goarch/parity.json +144 -0
- package/gs/io/fs/fs.ts +26 -14
- package/gs/io/fs/readdir.ts +4 -4
- package/gs/io/fs/sub.ts +8 -1
- package/gs/io/io.ts +1 -0
- package/gs/io/parity.json +162 -0
- package/gs/math/bits/index.test.ts +14 -1
- package/gs/math/bits/index.ts +23 -4
- package/gs/math/bits/parity.json +156 -0
- package/gs/mime/index.test.ts +90 -0
- package/gs/mime/index.ts +369 -6
- package/gs/mime/parity.json +36 -0
- package/gs/net/http/httptest/index.test.ts +98 -2
- package/gs/net/http/httptest/index.ts +101 -6
- package/gs/net/http/httptest/parity.json +15 -0
- package/gs/net/http/index.test.ts +781 -12
- package/gs/net/http/index.ts +1860 -139
- package/gs/net/http/meta.json +16 -1
- package/gs/net/http/parity.json +193 -0
- package/gs/os/dir_unix.gs.ts +1 -1
- package/gs/os/error.gs.ts +1 -1
- package/gs/os/exec.gs.ts +4 -8
- package/gs/os/exec_posix.gs.ts +1 -1
- package/gs/os/index.test.ts +9 -0
- package/gs/os/index.ts +1 -0
- package/gs/os/parity.json +9 -0
- package/gs/os/proc.gs.ts +18 -5
- package/gs/os/proc.test.ts +26 -0
- package/gs/os/root_js.gs.ts +1 -1
- package/gs/os/types.gs.ts +1 -1
- package/gs/os/types_js.gs.ts +1 -1
- package/gs/os/types_unix.gs.ts +1 -1
- package/gs/path/path.ts +11 -7
- package/gs/reflect/field.test.ts +37 -15
- package/gs/reflect/function-types.test.ts +518 -22
- package/gs/reflect/index.ts +8 -6
- package/gs/reflect/map.ts +20 -0
- package/gs/reflect/meta.json +6 -4
- package/gs/reflect/parity.json +234 -0
- package/gs/reflect/sliceat.test.ts +156 -0
- package/gs/reflect/structof.test.ts +401 -0
- package/gs/reflect/type.ts +1897 -317
- package/gs/reflect/typefor.test.ts +510 -10
- package/gs/reflect/types.ts +43 -18
- package/gs/reflect/value.ts +105 -45
- package/gs/reflect/visiblefields.ts +5 -168
- package/gs/runtime/parity.json +24 -0
- package/gs/runtime/pprof/index.test.ts +29 -7
- package/gs/runtime/pprof/index.ts +56 -30
- package/gs/runtime/pprof/parity.json +27 -0
- package/gs/runtime/runtime.test.ts +3 -1
- package/gs/runtime/runtime.ts +4 -3
- package/gs/runtime/trace/index.test.ts +5 -3
- package/gs/runtime/trace/index.ts +8 -20
- package/gs/runtime/trace/parity.json +36 -0
- package/gs/strconv/atoi.gs.ts +1 -1
- package/gs/strconv/complex.gs.ts +174 -0
- package/gs/strconv/complex.test.ts +65 -0
- package/gs/strconv/index.ts +1 -0
- package/gs/strconv/parity.json +120 -0
- package/gs/strings/builder.ts +1 -1
- package/gs/strings/parity.json +186 -0
- package/gs/strings/reader.ts +9 -5
- package/gs/strings/replace.ts +15 -7
- package/gs/strings/strings.test.ts +22 -2
- package/gs/strings/strings.ts +64 -6
- package/gs/sync/atomic/type.gs.ts +9 -9
- package/gs/sync/atomic/value.gs.ts +2 -2
- package/gs/syscall/env.ts +29 -14
- package/gs/testing/testing.test.ts +67 -0
- package/gs/testing/testing.ts +87 -19
- package/gs/time/parity.json +225 -0
- package/gs/time/time.test.ts +20 -2
- package/gs/time/time.ts +49 -7
- package/gs/unique/index.ts +7 -1
- package/package.json +4 -2
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.d.ts +0 -217
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +0 -926
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +0 -1
- package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +0 -38
- package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +0 -1361
- package/gs/github.com/aperturerobotics/starpc/srpc/meta.json +0 -46
- /package/compiler/{wasm_api.go → wasm-api.go} +0 -0
|
@@ -1,25 +1,72 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { varRef } from '../../builtin/varRef.js'
|
|
4
|
+
import * as $ from '../../builtin/index.js'
|
|
5
|
+
import * as bytes from '../../bytes/index.js'
|
|
6
|
+
import * as context from '../../context/index.js'
|
|
7
|
+
import * as io from '../../io/index.js'
|
|
8
|
+
import * as strings from '../../strings/index.js'
|
|
4
9
|
import {
|
|
10
|
+
CanonicalHeaderKey,
|
|
5
11
|
Client,
|
|
12
|
+
Cookie,
|
|
13
|
+
DefaultClient,
|
|
14
|
+
DefaultMaxHeaderBytes,
|
|
15
|
+
DefaultMaxIdleConnsPerHost,
|
|
16
|
+
DefaultServeMux,
|
|
6
17
|
DefaultTransport,
|
|
18
|
+
DetectContentType,
|
|
19
|
+
ErrNotSupported,
|
|
20
|
+
ErrServerClosed,
|
|
7
21
|
File,
|
|
22
|
+
FileServer,
|
|
8
23
|
FileSystem,
|
|
24
|
+
FS,
|
|
25
|
+
Get,
|
|
26
|
+
Handle,
|
|
9
27
|
Header,
|
|
10
28
|
Header_Add,
|
|
29
|
+
Header_Clone,
|
|
11
30
|
Header_Del,
|
|
12
31
|
Header_Get,
|
|
13
32
|
Header_Set,
|
|
33
|
+
Header_Values,
|
|
34
|
+
Header_Write,
|
|
14
35
|
HandlerFunc_ServeHTTP,
|
|
15
|
-
|
|
36
|
+
ListenAndServe,
|
|
37
|
+
MaxBytesError,
|
|
38
|
+
MaxBytesHandler,
|
|
39
|
+
MaxBytesReader,
|
|
40
|
+
NewFileTransport,
|
|
41
|
+
MethodGet,
|
|
42
|
+
MethodHead,
|
|
43
|
+
MethodOptions,
|
|
16
44
|
MethodPost,
|
|
17
45
|
MethodDelete,
|
|
46
|
+
MethodPatch,
|
|
47
|
+
MethodPut,
|
|
48
|
+
NewCrossOriginProtection,
|
|
18
49
|
NewRequest,
|
|
50
|
+
NewRequestWithContext,
|
|
51
|
+
NewResponseController,
|
|
52
|
+
NoBody,
|
|
19
53
|
NotFound,
|
|
54
|
+
NotFoundHandler,
|
|
55
|
+
ParseCookie,
|
|
56
|
+
ParseHTTPVersion,
|
|
57
|
+
ParseSetCookie,
|
|
20
58
|
ParseTime,
|
|
59
|
+
PostForm,
|
|
60
|
+
Protocols,
|
|
61
|
+
RegisterInProcessServer,
|
|
62
|
+
SameSiteStrictMode,
|
|
63
|
+
SetCookie,
|
|
64
|
+
StatusBadGateway,
|
|
65
|
+
Request,
|
|
21
66
|
Response,
|
|
22
67
|
ResponseWriter,
|
|
68
|
+
ServeContent,
|
|
69
|
+
ServeFile,
|
|
23
70
|
Server,
|
|
24
71
|
StatusCreated,
|
|
25
72
|
StatusForbidden,
|
|
@@ -32,9 +79,42 @@ import {
|
|
|
32
79
|
StatusTooManyRequests,
|
|
33
80
|
StatusUnauthorized,
|
|
34
81
|
StatusUnsupportedMediaType,
|
|
82
|
+
StatusNetworkAuthenticationRequired,
|
|
83
|
+
TimeFormat,
|
|
84
|
+
TrailerPrefix,
|
|
85
|
+
Transport,
|
|
86
|
+
UnregisterInProcessServer,
|
|
35
87
|
} from './index.js'
|
|
36
88
|
|
|
89
|
+
const originalFetch = globalThis.fetch
|
|
90
|
+
|
|
91
|
+
class testResponseWriter implements ResponseWriter {
|
|
92
|
+
public Code = 0
|
|
93
|
+
public Body = new bytes.Buffer()
|
|
94
|
+
private headerMap = new Header()
|
|
95
|
+
|
|
96
|
+
public Header(): Header {
|
|
97
|
+
return this.headerMap
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public Write(p: $.Slice<number>): [number, $.GoError] {
|
|
101
|
+
return this.Body.Write(p)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public WriteHeader(statusCode: number): void {
|
|
105
|
+
this.Code = statusCode
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
37
109
|
describe('net/http override', () => {
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: originalFetch,
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
38
118
|
it('exports response status helpers', () => {
|
|
39
119
|
const resp = new Response({ StatusCode: StatusOK })
|
|
40
120
|
|
|
@@ -47,29 +127,312 @@ describe('net/http override', () => {
|
|
|
47
127
|
expect(StatusText(StatusUnsupportedMediaType)).toBe('Unsupported Media Type')
|
|
48
128
|
expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
|
|
49
129
|
expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
|
|
130
|
+
expect(StatusText(StatusBadGateway)).toBe('Bad Gateway')
|
|
50
131
|
expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
|
|
132
|
+
expect(StatusText(StatusNetworkAuthenticationRequired)).toBe('Network Authentication Required')
|
|
51
133
|
expect(StatusText(599)).toBe('')
|
|
134
|
+
Header_Set(resp.Header, 'X-Test', 'ok')
|
|
135
|
+
resp.Body = io.NopCloser(bytes.NewReader($.stringToBytes('body')))
|
|
136
|
+
const written = new bytes.Buffer()
|
|
137
|
+
expect(resp.Write(written)).toBeNull()
|
|
138
|
+
expect(Buffer.from(written.Bytes()).toString('utf8')).toBe(
|
|
139
|
+
'HTTP/1.1 200 OK\r\nX-Test: ok\r\n\r\nbody',
|
|
140
|
+
)
|
|
141
|
+
expect(MethodGet).toBe('GET')
|
|
142
|
+
expect(MethodHead).toBe('HEAD')
|
|
52
143
|
expect(MethodPost).toBe('POST')
|
|
144
|
+
expect(MethodPut).toBe('PUT')
|
|
145
|
+
expect(MethodPatch).toBe('PATCH')
|
|
53
146
|
expect(MethodDelete).toBe('DELETE')
|
|
54
147
|
expect(StatusCreated).toBe(201)
|
|
148
|
+
expect(DefaultMaxHeaderBytes).toBe(1 << 20)
|
|
149
|
+
expect(DefaultMaxIdleConnsPerHost).toBe(2)
|
|
150
|
+
expect(TimeFormat).toBe('Mon, 02 Jan 2006 15:04:05 GMT')
|
|
151
|
+
expect(TrailerPrefix).toBe('Trailer:')
|
|
152
|
+
expect(ErrServerClosed.Error()).toBe('http: Server closed')
|
|
55
153
|
})
|
|
56
154
|
|
|
57
|
-
it('
|
|
58
|
-
const
|
|
155
|
+
it('exports common header and protocol utility surfaces', () => {
|
|
156
|
+
const header = new Header()
|
|
157
|
+
Header_Add(header, 'content-type', 'text/plain')
|
|
158
|
+
Header_Add(header, 'Content-Type', 'charset=utf-8')
|
|
159
|
+
const cloned = Header_Clone(header)
|
|
160
|
+
Header_Add(cloned, 'Content-Type', 'copy')
|
|
59
161
|
|
|
60
|
-
expect(
|
|
61
|
-
expect(
|
|
162
|
+
expect(CanonicalHeaderKey('content-type')).toBe('Content-Type')
|
|
163
|
+
expect(Array.from(Header_Values(header, 'CONTENT-TYPE') ?? [])).toEqual([
|
|
164
|
+
'text/plain',
|
|
165
|
+
'charset=utf-8',
|
|
166
|
+
])
|
|
167
|
+
expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain('copy')
|
|
168
|
+
expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).not.toContain('copy')
|
|
169
|
+
|
|
170
|
+
const written = new bytes.Buffer()
|
|
171
|
+
expect(Header_Write(header, written)).toBeNull()
|
|
172
|
+
expect(Buffer.from(written.Bytes()).toString('utf8')).toContain('Content-Type: text/plain\r\n')
|
|
173
|
+
|
|
174
|
+
expect(ParseHTTPVersion('HTTP/2.0')).toEqual([2, 0, true])
|
|
175
|
+
expect(ParseHTTPVersion('h2')).toEqual([0, 0, false])
|
|
176
|
+
|
|
177
|
+
const protocols = new Protocols()
|
|
178
|
+
protocols.SetHTTP1(true)
|
|
179
|
+
protocols.SetUnencryptedHTTP2(true)
|
|
180
|
+
expect(protocols.HTTP1()).toBe(true)
|
|
181
|
+
expect(protocols.HTTP2()).toBe(false)
|
|
182
|
+
expect(protocols.String()).toBe('{HTTP1,UnencryptedHTTP2}')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('validates outgoing request construction', () => {
|
|
186
|
+
const [req, reqErr] = NewRequestWithContext(
|
|
187
|
+
context.Background(),
|
|
188
|
+
'',
|
|
189
|
+
'https://example.invalid/path?q=1',
|
|
190
|
+
null,
|
|
191
|
+
)
|
|
192
|
+
expect(reqErr).toBeNull()
|
|
193
|
+
expect(req?.Method).toBe(MethodGet)
|
|
194
|
+
expect(req?.Host).toBe('example.invalid')
|
|
195
|
+
expect(req?.URL?.Path).toBe('/path')
|
|
196
|
+
expect(req?.URL?.RawQuery).toBe('q=1')
|
|
197
|
+
|
|
198
|
+
expect(NewRequestWithContext(context.Background(), 'bad method', 'https://example.invalid/', null)[1]?.Error()).toBe(
|
|
199
|
+
'net/http: invalid method "bad method"',
|
|
200
|
+
)
|
|
201
|
+
expect(NewRequestWithContext(null, MethodGet, 'https://example.invalid/', null)[1]?.Error()).toBe(
|
|
202
|
+
'net/http: nil Context',
|
|
203
|
+
)
|
|
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
|
+
expect(bodyReqErr).toBeNull()
|
|
209
|
+
expect(bodyReq?.ContentLength).toBe(3)
|
|
210
|
+
const [stringReq, stringReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', strings.NewReader('abcd'))
|
|
211
|
+
expect(stringReqErr).toBeNull()
|
|
212
|
+
expect(stringReq?.ContentLength).toBe(4)
|
|
213
|
+
const [noBodyReq, noBodyErr] = NewRequest(MethodPost, 'https://example.invalid/upload', NoBody)
|
|
214
|
+
expect(noBodyErr).toBeNull()
|
|
215
|
+
expect(noBodyReq?.ContentLength).toBe(0)
|
|
216
|
+
expect(noBodyReq?.Body).toBe(NoBody)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('applies cross-origin protection checks', () => {
|
|
220
|
+
const protection = NewCrossOriginProtection()
|
|
221
|
+
const [sameOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
|
|
222
|
+
Header_Set(sameOrigin!.Header, 'Sec-Fetch-Site', 'same-origin')
|
|
223
|
+
expect(protection.Check(sameOrigin)).toBeNull()
|
|
224
|
+
|
|
225
|
+
const [noHeaders] = NewRequest(MethodPost, 'https://example.invalid/update', null)
|
|
226
|
+
expect(protection.Check(noHeaders)).toBeNull()
|
|
227
|
+
|
|
228
|
+
const [safe] = NewRequest(MethodOptions, 'https://example.invalid/update', null)
|
|
229
|
+
Header_Set(safe!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
230
|
+
expect(protection.Check(safe)).toBeNull()
|
|
231
|
+
|
|
232
|
+
const [matchingOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
|
|
233
|
+
Header_Set(matchingOrigin!.Header, 'Origin', 'https://example.invalid')
|
|
234
|
+
expect(protection.Check(matchingOrigin)).toBeNull()
|
|
235
|
+
|
|
236
|
+
const [crossSite] = NewRequest(MethodPost, 'https://example.invalid/update', null)
|
|
237
|
+
Header_Set(crossSite!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
238
|
+
expect(protection.Check(crossSite)?.Error()).toContain('Sec-Fetch-Site')
|
|
239
|
+
|
|
240
|
+
const [oldBrowser] = NewRequest(MethodPost, 'https://example.invalid/update', null)
|
|
241
|
+
Header_Set(oldBrowser!.Header, 'Origin', 'https://attacker.invalid')
|
|
242
|
+
expect(protection.Check(oldBrowser)?.Error()).toContain('Origin does not match Host')
|
|
243
|
+
|
|
244
|
+
expect(protection.AddTrustedOrigin('https://trusted.invalid')).toBeNull()
|
|
245
|
+
expect(protection.AddTrustedOrigin('https://trusted.invalid/')).not.toBeNull()
|
|
246
|
+
const [trusted] = NewRequest(MethodPost, 'https://example.invalid/update', null)
|
|
247
|
+
Header_Set(trusted!.Header, 'Origin', 'https://trusted.invalid')
|
|
248
|
+
Header_Set(trusted!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
249
|
+
expect(protection.Check(trusted)).toBeNull()
|
|
250
|
+
|
|
251
|
+
protection.AddInsecureBypassPattern('/bypass/')
|
|
252
|
+
protection.AddInsecureBypassPattern('POST /post-only/')
|
|
253
|
+
const [bypass] = NewRequest(MethodPost, 'https://example.invalid/bypass/ok', null)
|
|
254
|
+
Header_Set(bypass!.Header, 'Origin', 'https://attacker.invalid')
|
|
255
|
+
Header_Set(bypass!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
256
|
+
expect(protection.Check(bypass)).toBeNull()
|
|
257
|
+
|
|
258
|
+
const [methodBypass] = NewRequest(MethodPost, 'https://example.invalid/post-only/', null)
|
|
259
|
+
Header_Set(methodBypass!.Header, 'Origin', 'https://attacker.invalid')
|
|
260
|
+
expect(protection.Check(methodBypass)).toBeNull()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('routes cross-origin protection handlers through deny and success paths', () => {
|
|
264
|
+
const protection = NewCrossOriginProtection()
|
|
265
|
+
let served = false
|
|
266
|
+
const handler = protection.Handler({
|
|
267
|
+
ServeHTTP(w) {
|
|
268
|
+
served = true
|
|
269
|
+
w?.WriteHeader(StatusOK)
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
const [blocked] = NewRequest(MethodPost, 'https://example.invalid/update', null)
|
|
273
|
+
Header_Set(blocked!.Header, 'Sec-Fetch-Site', 'cross-site')
|
|
274
|
+
const blockedWriter = new testResponseWriter()
|
|
275
|
+
|
|
276
|
+
handler.ServeHTTP(blockedWriter, blocked)
|
|
277
|
+
|
|
278
|
+
expect(served).toBe(false)
|
|
279
|
+
expect(blockedWriter.Code).toBe(StatusForbidden)
|
|
280
|
+
|
|
281
|
+
protection.SetDenyHandler({
|
|
282
|
+
ServeHTTP(w) {
|
|
283
|
+
w?.WriteHeader(StatusTeapot)
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
const deniedWriter = new testResponseWriter()
|
|
287
|
+
handler.ServeHTTP(deniedWriter, blocked)
|
|
288
|
+
expect(deniedWriter.Code).toBe(StatusTeapot)
|
|
289
|
+
|
|
290
|
+
const [safe] = NewRequest(MethodGet, 'https://example.invalid/update', null)
|
|
291
|
+
const allowedWriter = new testResponseWriter()
|
|
292
|
+
handler.ServeHTTP(allowedWriter, safe)
|
|
293
|
+
expect(served).toBe(true)
|
|
294
|
+
expect(allowedWriter.Code).toBe(StatusOK)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('parses cookies and reports syntax errors', () => {
|
|
298
|
+
const [cookies, cookieErr] = ParseCookie('Cookie-1="v$1"; c2=v2')
|
|
299
|
+
expect(cookieErr).toBeNull()
|
|
300
|
+
expect(cookies?.[0]?.Name).toBe('Cookie-1')
|
|
301
|
+
expect(cookies?.[0]?.Value).toBe('v$1')
|
|
302
|
+
expect(cookies?.[0]?.Quoted).toBe(true)
|
|
303
|
+
expect(cookies?.[1]?.Name).toBe('c2')
|
|
304
|
+
expect(cookies?.[1]?.Value).toBe('v2')
|
|
305
|
+
|
|
306
|
+
expect(ParseCookie('')[1]?.Error()).toBe('http: blank cookie')
|
|
307
|
+
expect(ParseCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
|
|
308
|
+
expect(ParseCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
|
|
309
|
+
expect(ParseCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
|
|
310
|
+
|
|
311
|
+
const [setCookie, setErr] = ParseSetCookie(
|
|
312
|
+
'sid=abc; Path=/app; Domain=example.invalid; HttpOnly; Secure; SameSite=Strict; Max-Age=60; Partitioned',
|
|
313
|
+
)
|
|
314
|
+
expect(setErr).toBeNull()
|
|
315
|
+
expect(setCookie?.Name).toBe('sid')
|
|
316
|
+
expect(setCookie?.Value).toBe('abc')
|
|
317
|
+
expect(setCookie?.Path).toBe('/app')
|
|
318
|
+
expect(setCookie?.Domain).toBe('example.invalid')
|
|
319
|
+
expect(setCookie?.HttpOnly).toBe(true)
|
|
320
|
+
expect(setCookie?.Secure).toBe(true)
|
|
321
|
+
expect(setCookie?.SameSite).toBe(SameSiteStrictMode)
|
|
322
|
+
expect(setCookie?.MaxAge).toBe(60)
|
|
323
|
+
expect(setCookie?.Partitioned).toBe(true)
|
|
324
|
+
expect(setCookie?.Raw).toContain('sid=abc')
|
|
325
|
+
|
|
326
|
+
const [spaced, spacedErr] = ParseSetCookie('special-9 =","')
|
|
327
|
+
expect(spacedErr).toBeNull()
|
|
328
|
+
expect(spaced?.Name).toBe('special-9')
|
|
329
|
+
expect(spaced?.Value).toBe(',')
|
|
330
|
+
expect(spaced?.Quoted).toBe(true)
|
|
331
|
+
|
|
332
|
+
expect(ParseSetCookie('')[1]?.Error()).toBe('http: blank cookie')
|
|
333
|
+
expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
|
|
334
|
+
expect(ParseSetCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
|
|
335
|
+
expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('exports no-body, limit-reader, and unsupported controller surfaces', async () => {
|
|
339
|
+
const empty = new Uint8Array(1)
|
|
340
|
+
const [n, err] = NoBody.Read(empty)
|
|
341
|
+
expect(n).toBe(0)
|
|
342
|
+
expect(err).toBe(io.EOF)
|
|
343
|
+
expect(NoBody.Close()).toBeNull()
|
|
344
|
+
|
|
345
|
+
const limited = MaxBytesReader(null, {
|
|
346
|
+
Read: (p: Uint8Array) => {
|
|
347
|
+
p[0] = 1
|
|
348
|
+
if (p.length > 1) {
|
|
349
|
+
p[1] = 2
|
|
350
|
+
}
|
|
351
|
+
return [1, null]
|
|
352
|
+
},
|
|
353
|
+
Close: () => null,
|
|
354
|
+
}, 1)
|
|
355
|
+
const limitedBuf = new Uint8Array(2)
|
|
356
|
+
expect(limited.Read(limitedBuf)).toEqual([1, null])
|
|
357
|
+
expect(limitedBuf[0]).toBe(1)
|
|
358
|
+
const [, limitErr] = limited.Read(new Uint8Array(1))
|
|
359
|
+
expect(limitErr).toBeInstanceOf(MaxBytesError)
|
|
360
|
+
|
|
361
|
+
const exactReader = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('ok'))), 2)
|
|
362
|
+
const [exactData, exactErr] = await io.ReadAll(exactReader)
|
|
363
|
+
expect(exactErr).toBeNull()
|
|
364
|
+
expect(Buffer.from(exactData ?? []).toString('utf8')).toBe('ok')
|
|
365
|
+
|
|
366
|
+
const tooLarge = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('toolarge'))), 2)
|
|
367
|
+
const tooLargeBuf = new Uint8Array(8)
|
|
368
|
+
const [tooLargeN, tooLargeErr] = tooLarge.Read(tooLargeBuf)
|
|
369
|
+
expect(tooLargeN).toBe(2)
|
|
370
|
+
expect(tooLargeErr).toBeInstanceOf(MaxBytesError)
|
|
371
|
+
expect((tooLargeErr as MaxBytesError).Limit).toBe(2)
|
|
372
|
+
expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe('to')
|
|
373
|
+
|
|
374
|
+
const controller = NewResponseController(null)
|
|
375
|
+
expect(controller.Hijack()[2]).toBe(ErrNotSupported)
|
|
376
|
+
expect(controller.SetReadDeadline({} as any)).toBe(ErrNotSupported)
|
|
377
|
+
expect(ListenAndServe(':0', null)).toBe(ErrNotSupported)
|
|
378
|
+
expect(new Transport().Clone()).toBeInstanceOf(Transport)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('wraps a cloned request body in MaxBytesHandler', () => {
|
|
382
|
+
const body = io.NopCloser(bytes.NewReader($.stringToBytes('ok')))
|
|
383
|
+
const [req] = NewRequest(MethodPost, 'http://example.invalid/upload', body)
|
|
384
|
+
let servedReq: any = null
|
|
385
|
+
const handler = MaxBytesHandler({
|
|
386
|
+
ServeHTTP(_w, r) {
|
|
387
|
+
servedReq = $.pointerValue(r)
|
|
388
|
+
Header_Set(servedReq.Header, 'X-Shared', 'true')
|
|
389
|
+
},
|
|
390
|
+
}, 1)
|
|
391
|
+
|
|
392
|
+
handler.ServeHTTP(null, req)
|
|
393
|
+
|
|
394
|
+
expect(servedReq).not.toBe(req)
|
|
395
|
+
expect(servedReq.Body).not.toBe(body)
|
|
396
|
+
expect(req!.Body).toBe(body)
|
|
397
|
+
expect(Header_Get(req!.Header, 'X-Shared')).toBe('true')
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('routes Get through fetch-backed DefaultTransport', async () => {
|
|
401
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
402
|
+
configurable: true,
|
|
403
|
+
writable: true,
|
|
404
|
+
value: async () =>
|
|
405
|
+
new globalThis.Response('hello', {
|
|
406
|
+
status: StatusOK,
|
|
407
|
+
statusText: 'OK',
|
|
408
|
+
headers: { 'Content-Length': '5', 'X-Test': 'ok' },
|
|
409
|
+
}),
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
const [resp, err] = await Get('https://example.invalid')
|
|
413
|
+
|
|
414
|
+
expect(err).toBeNull()
|
|
415
|
+
expect(resp?.StatusCode).toBe(StatusOK)
|
|
416
|
+
expect(Header_Get(resp!.Header, 'x-test')).toBe('ok')
|
|
62
417
|
})
|
|
63
418
|
|
|
64
419
|
it('accepts VarRef requests for client calls', async () => {
|
|
420
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
421
|
+
configurable: true,
|
|
422
|
+
writable: true,
|
|
423
|
+
value: async () => {
|
|
424
|
+
throw new Error('network down')
|
|
425
|
+
},
|
|
426
|
+
})
|
|
65
427
|
const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid', null)
|
|
66
428
|
expect(reqErr).toBeNull()
|
|
67
429
|
expect((req!.URL as any).Path).toBe('/')
|
|
68
|
-
expect(req!.
|
|
430
|
+
expect(req!.Host).toBe('example.invalid')
|
|
431
|
+
expect(req!.RequestURI).toBe('')
|
|
69
432
|
|
|
70
433
|
const [resp, err] = await new Client().Do(varRef(req!))
|
|
71
434
|
expect(resp).toBeNull()
|
|
72
|
-
expect(err?.Error()).
|
|
435
|
+
expect(err?.Error()).toContain('network down')
|
|
73
436
|
})
|
|
74
437
|
|
|
75
438
|
it('wraps request body readers and keeps response metadata', () => {
|
|
@@ -88,13 +451,197 @@ describe('net/http override', () => {
|
|
|
88
451
|
expect((resp.Request as any).value).toBe(req)
|
|
89
452
|
})
|
|
90
453
|
|
|
91
|
-
it('exports
|
|
92
|
-
|
|
454
|
+
it('exports fetch-backed default transport surface', async () => {
|
|
455
|
+
let requestBodyClosed = false
|
|
456
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
457
|
+
configurable: true,
|
|
458
|
+
writable: true,
|
|
459
|
+
value: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
460
|
+
expect(String(input)).toBe('https://example.invalid/upload')
|
|
461
|
+
expect(init?.method).toBe(MethodPost)
|
|
462
|
+
const headers = init?.headers as Headers
|
|
463
|
+
expect(headers.get('Range')).toBe('bytes=0-9')
|
|
464
|
+
expect(headers.get('Authorization')).toBe('Bearer test')
|
|
465
|
+
expect(Buffer.from((init?.body as Uint8Array) ?? []).toString('utf8')).toBe('payload')
|
|
466
|
+
return new globalThis.Response('accepted', {
|
|
467
|
+
status: StatusCreated,
|
|
468
|
+
statusText: 'Created',
|
|
469
|
+
headers: { 'Content-Length': '8', 'X-Reply': 'yes' },
|
|
470
|
+
})
|
|
471
|
+
},
|
|
472
|
+
})
|
|
473
|
+
const payload = bytes.NewReader($.stringToBytes('payload'))
|
|
474
|
+
const requestBody = {
|
|
475
|
+
Read: payload.Read.bind(payload),
|
|
476
|
+
Close: () => {
|
|
477
|
+
requestBodyClosed = true
|
|
478
|
+
return null
|
|
479
|
+
},
|
|
480
|
+
}
|
|
481
|
+
const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', requestBody)
|
|
482
|
+
Header_Set(req!.Header, 'Range', 'bytes=0-9')
|
|
483
|
+
Header_Set(req!.Header, 'Authorization', 'Bearer test')
|
|
484
|
+
|
|
485
|
+
const [resp, err] = await DefaultTransport.RoundTrip(req)
|
|
486
|
+
|
|
487
|
+
expect(err).toBeNull()
|
|
488
|
+
expect(resp?.StatusCode).toBe(StatusCreated)
|
|
489
|
+
expect(resp?.ContentLength).toBe(8)
|
|
490
|
+
expect(Header_Get(resp!.Header, 'x-reply')).toBe('yes')
|
|
491
|
+
expect(requestBodyClosed).toBe(true)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('closes request bodies when fetch body reads fail', async () => {
|
|
495
|
+
let requestBodyClosed = false
|
|
496
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
497
|
+
configurable: true,
|
|
498
|
+
writable: true,
|
|
499
|
+
value: async () => {
|
|
500
|
+
throw new Error('fetch should not run')
|
|
501
|
+
},
|
|
502
|
+
})
|
|
503
|
+
const readErr = $.newError('read failed')
|
|
504
|
+
const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', {
|
|
505
|
+
Read: () => [0, readErr],
|
|
506
|
+
Close: () => {
|
|
507
|
+
requestBodyClosed = true
|
|
508
|
+
return null
|
|
509
|
+
},
|
|
510
|
+
})
|
|
93
511
|
|
|
94
512
|
const [resp, err] = await DefaultTransport.RoundTrip(req)
|
|
95
513
|
|
|
96
514
|
expect(resp).toBeNull()
|
|
97
|
-
expect(err
|
|
515
|
+
expect(err).toBe(readErr)
|
|
516
|
+
expect(requestBodyClosed).toBe(true)
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('closes request bodies before unsupported and canceled requests return', async () => {
|
|
520
|
+
let unsupportedClosed = false
|
|
521
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
522
|
+
configurable: true,
|
|
523
|
+
writable: true,
|
|
524
|
+
value: undefined,
|
|
525
|
+
})
|
|
526
|
+
const [unsupportedReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
|
|
527
|
+
Read: () => [0, null],
|
|
528
|
+
Close: () => {
|
|
529
|
+
unsupportedClosed = true
|
|
530
|
+
return null
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const [unsupportedResp, unsupportedErr] = await DefaultTransport.RoundTrip(unsupportedReq)
|
|
535
|
+
|
|
536
|
+
expect(unsupportedResp).toBeNull()
|
|
537
|
+
expect(unsupportedErr?.Error()).toContain('Client.Do is not implemented')
|
|
538
|
+
expect(unsupportedClosed).toBe(true)
|
|
539
|
+
|
|
540
|
+
let canceledClosed = false
|
|
541
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
542
|
+
configurable: true,
|
|
543
|
+
writable: true,
|
|
544
|
+
value: async () => {
|
|
545
|
+
throw new Error('fetch should not run')
|
|
546
|
+
},
|
|
547
|
+
})
|
|
548
|
+
const [ctx, cancel] = context.WithCancel(context.Background())
|
|
549
|
+
cancel?.()
|
|
550
|
+
const [canceledReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
|
|
551
|
+
Read: () => [0, null],
|
|
552
|
+
Close: () => {
|
|
553
|
+
canceledClosed = true
|
|
554
|
+
return null
|
|
555
|
+
},
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(canceledReq!.WithContext(ctx))
|
|
559
|
+
|
|
560
|
+
expect(canceledResp).toBeNull()
|
|
561
|
+
expect(canceledErr).toBe(context.Canceled)
|
|
562
|
+
expect(canceledClosed).toBe(true)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('closes request bodies for methods that do not send a fetch body', async () => {
|
|
566
|
+
let requestBodyClosed = false
|
|
567
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
568
|
+
configurable: true,
|
|
569
|
+
writable: true,
|
|
570
|
+
value: async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
571
|
+
expect(init?.method).toBe(MethodGet)
|
|
572
|
+
expect(init?.body).toBeUndefined()
|
|
573
|
+
return new globalThis.Response('ok', { status: StatusOK })
|
|
574
|
+
},
|
|
575
|
+
})
|
|
576
|
+
const [req] = NewRequest(MethodGet, 'https://example.invalid/read', {
|
|
577
|
+
Read: () => {
|
|
578
|
+
throw new Error('GET body should not be read')
|
|
579
|
+
},
|
|
580
|
+
Close: () => {
|
|
581
|
+
requestBodyClosed = true
|
|
582
|
+
return null
|
|
583
|
+
},
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
const [resp, err] = await DefaultTransport.RoundTrip(req)
|
|
587
|
+
|
|
588
|
+
expect(err).toBeNull()
|
|
589
|
+
expect(resp?.StatusCode).toBe(StatusOK)
|
|
590
|
+
expect(requestBodyClosed).toBe(true)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('closes request bodies after in-process handlers return', async () => {
|
|
594
|
+
let handlerSawBody = false
|
|
595
|
+
let requestBodyClosed = false
|
|
596
|
+
const url = RegisterInProcessServer({
|
|
597
|
+
ServeHTTP: (w, r) => {
|
|
598
|
+
const req = $.pointerValue(r)
|
|
599
|
+
handlerSawBody = req?.Body != null
|
|
600
|
+
expect(req?.RequestURI).toBe('/close?q=1')
|
|
601
|
+
expect(req?.URL.Host).toBe('')
|
|
602
|
+
expect(req?.URL.Scheme).toBe('')
|
|
603
|
+
w?.WriteHeader(StatusOK)
|
|
604
|
+
},
|
|
605
|
+
})
|
|
606
|
+
try {
|
|
607
|
+
const [req] = NewRequest(MethodPost, url + '/close?q=1', {
|
|
608
|
+
Read: () => [0, null],
|
|
609
|
+
Close: () => {
|
|
610
|
+
requestBodyClosed = true
|
|
611
|
+
return null
|
|
612
|
+
},
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
const [resp, err] = await DefaultTransport.RoundTrip(req)
|
|
616
|
+
|
|
617
|
+
expect(err).toBeNull()
|
|
618
|
+
expect(resp?.StatusCode).toBe(StatusOK)
|
|
619
|
+
expect(handlerSawBody).toBe(true)
|
|
620
|
+
expect(requestBodyClosed).toBe(true)
|
|
621
|
+
} finally {
|
|
622
|
+
UnregisterInProcessServer(url)
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it('suppresses in-process response bodies for HEAD requests', async () => {
|
|
627
|
+
const url = RegisterInProcessServer({
|
|
628
|
+
ServeHTTP(w) {
|
|
629
|
+
w?.WriteHeader(StatusOK)
|
|
630
|
+
w?.Write($.stringToBytes('hidden'))
|
|
631
|
+
},
|
|
632
|
+
})
|
|
633
|
+
try {
|
|
634
|
+
const [req] = NewRequest(MethodHead, url + '/head', null)
|
|
635
|
+
|
|
636
|
+
const [resp, err] = await new Transport().RoundTrip(req)
|
|
637
|
+
|
|
638
|
+
expect(err).toBeNull()
|
|
639
|
+
const [n, readErr] = resp!.Body!.Read(new Uint8Array(8))
|
|
640
|
+
expect(n).toBe(0)
|
|
641
|
+
expect(readErr).toBe(io.EOF)
|
|
642
|
+
} finally {
|
|
643
|
+
UnregisterInProcessServer(url)
|
|
644
|
+
}
|
|
98
645
|
})
|
|
99
646
|
|
|
100
647
|
it('delegates client calls through RoundTripper implementations', async () => {
|
|
@@ -109,7 +656,7 @@ describe('net/http override', () => {
|
|
|
109
656
|
const request = (got as any).value ?? got
|
|
110
657
|
expect(request.UserAgent()).toBe('goscript-test')
|
|
111
658
|
expect(request.RemoteAddr).toBe('127.0.0.1:1234')
|
|
112
|
-
expect(request.RequestURI).toBe('
|
|
659
|
+
expect(request.RequestURI).toBe('')
|
|
113
660
|
expect(request.ContentLength).toBe(42)
|
|
114
661
|
return [new Response({ StatusCode: StatusOK }), null]
|
|
115
662
|
},
|
|
@@ -121,6 +668,39 @@ describe('net/http override', () => {
|
|
|
121
668
|
expect(resp?.StatusCode).toBe(StatusOK)
|
|
122
669
|
})
|
|
123
670
|
|
|
671
|
+
it('posts URL-encoded forms through clients and package helper', async () => {
|
|
672
|
+
const transport = {
|
|
673
|
+
async RoundTrip(got: Request | $.VarRef<Request> | null): Promise<[Response | null, $.GoError]> {
|
|
674
|
+
const request = $.pointerValue<Request>(got)
|
|
675
|
+
expect(request.Method).toBe(MethodPost)
|
|
676
|
+
expect(Header_Get(request.Header, 'Content-Type')).toBe('application/x-www-form-urlencoded')
|
|
677
|
+
const [data, err] = await io.ReadAll(request.Body!)
|
|
678
|
+
expect(err).toBeNull()
|
|
679
|
+
expect($.bytesToString(data)).toBe('a=one&a=two&space=x+y')
|
|
680
|
+
return [new Response({ StatusCode: StatusOK }), null]
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
const form = new Map<string, $.Slice<string>>([
|
|
684
|
+
['space', $.arrayToSlice(['x y'])],
|
|
685
|
+
['a', $.arrayToSlice(['one', 'two'])],
|
|
686
|
+
])
|
|
687
|
+
const client = new Client({ Transport: transport })
|
|
688
|
+
|
|
689
|
+
const [clientResp, clientErr] = await client.PostForm('https://example.invalid/form', form)
|
|
690
|
+
expect(clientErr).toBeNull()
|
|
691
|
+
expect(clientResp?.StatusCode).toBe(StatusOK)
|
|
692
|
+
|
|
693
|
+
const oldTransport = DefaultClient.Transport
|
|
694
|
+
DefaultClient.Transport = transport
|
|
695
|
+
try {
|
|
696
|
+
const [resp, err] = await PostForm('https://example.invalid/form', form)
|
|
697
|
+
expect(err).toBeNull()
|
|
698
|
+
expect(resp?.StatusCode).toBe(StatusOK)
|
|
699
|
+
} finally {
|
|
700
|
+
DefaultClient.Transport = oldTransport
|
|
701
|
+
}
|
|
702
|
+
})
|
|
703
|
+
|
|
124
704
|
it('canonicalizes header keys for case-insensitive lookup', () => {
|
|
125
705
|
const header = new Header()
|
|
126
706
|
|
|
@@ -169,11 +749,83 @@ describe('net/http override', () => {
|
|
|
169
749
|
expect(writes).toEqual(['status:404', '404 page not found\n'])
|
|
170
750
|
})
|
|
171
751
|
|
|
752
|
+
it('routes through default mux and handler helper exports', () => {
|
|
753
|
+
const writes: string[] = []
|
|
754
|
+
const writer: ResponseWriter = {
|
|
755
|
+
Header: () => new Header(),
|
|
756
|
+
Write: (p) => {
|
|
757
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
758
|
+
return [p?.length ?? 0, null]
|
|
759
|
+
},
|
|
760
|
+
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
761
|
+
}
|
|
762
|
+
const [req] = NewRequest(MethodGet, 'https://example.invalid/default', null)
|
|
763
|
+
Handle('/default', {
|
|
764
|
+
ServeHTTP(w) {
|
|
765
|
+
w!.WriteHeader(StatusOK)
|
|
766
|
+
w!.Write($.stringToBytes('default mux'))
|
|
767
|
+
},
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
DefaultServeMux.ServeHTTP(writer, req)
|
|
771
|
+
NotFoundHandler().ServeHTTP(writer, req)
|
|
772
|
+
|
|
773
|
+
expect(writes).toEqual([
|
|
774
|
+
'status:200',
|
|
775
|
+
'default mux',
|
|
776
|
+
'status:404',
|
|
777
|
+
'404 page not found\n',
|
|
778
|
+
])
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('formats Set-Cookie headers for browser bootstrap routes', () => {
|
|
782
|
+
const header = new Header()
|
|
783
|
+
const writer: ResponseWriter = {
|
|
784
|
+
Header: () => header,
|
|
785
|
+
Write: (p) => [p?.length ?? 0, null],
|
|
786
|
+
WriteHeader: () => undefined,
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
SetCookie(writer, new Cookie({
|
|
790
|
+
Name: 'spacewave_local_capability',
|
|
791
|
+
Value: 'token',
|
|
792
|
+
Path: '/',
|
|
793
|
+
MaxAge: 300,
|
|
794
|
+
HttpOnly: true,
|
|
795
|
+
Secure: true,
|
|
796
|
+
SameSite: SameSiteStrictMode,
|
|
797
|
+
}))
|
|
798
|
+
|
|
799
|
+
expect(Array.from(header.get('Set-Cookie') ?? [])).toEqual([
|
|
800
|
+
'spacewave_local_capability=token; Path=/; Max-Age=300; HttpOnly; Secure; SameSite=Strict',
|
|
801
|
+
])
|
|
802
|
+
})
|
|
803
|
+
|
|
172
804
|
it('parses HTTP dates', () => {
|
|
173
805
|
const [parsed, err] = ParseTime('Sun, 06 Nov 1994 08:49:37 GMT')
|
|
174
806
|
|
|
175
807
|
expect(err).toBeNull()
|
|
176
808
|
expect(parsed.Unix()).toBe(784111777)
|
|
809
|
+
expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe('text/html; charset=utf-8')
|
|
810
|
+
expect(
|
|
811
|
+
DetectContentType(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
|
|
812
|
+
).toBe('image/png')
|
|
813
|
+
expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe('image/jpeg')
|
|
814
|
+
expect(DetectContentType($.stringToBytes('%PDF-1.7'))).toBe('application/pdf')
|
|
815
|
+
expect(DetectContentType($.stringToBytes('RIFFxxxxWEBPVP'))).toBe('image/webp')
|
|
816
|
+
expect(DetectContentType($.stringToBytes('FORMxxxxAIFF'))).toBe('audio/aiff')
|
|
817
|
+
expect(DetectContentType($.stringToBytes('ID3payload'))).toBe('audio/mpeg')
|
|
818
|
+
expect(DetectContentType($.stringToBytes('wOFFpayload'))).toBe('font/woff')
|
|
819
|
+
expect(DetectContentType(new Uint8Array([
|
|
820
|
+
0x00, 0x00, 0x00, 0x18,
|
|
821
|
+
0x66, 0x74, 0x79, 0x70,
|
|
822
|
+
0x69, 0x73, 0x6f, 0x6d,
|
|
823
|
+
0x00, 0x00, 0x00, 0x00,
|
|
824
|
+
0x6d, 0x70, 0x34, 0x31,
|
|
825
|
+
0x00, 0x00, 0x00, 0x00,
|
|
826
|
+
]))).toBe('video/mp4')
|
|
827
|
+
expect(DetectContentType(new Uint8Array([0x00, 0x01, 0x02]))).toBe('application/octet-stream')
|
|
828
|
+
expect(DetectContentType(new Uint8Array())).toBe('text/plain; charset=utf-8')
|
|
177
829
|
})
|
|
178
830
|
|
|
179
831
|
it('exports file server interface shapes', () => {
|
|
@@ -191,4 +843,121 @@ describe('net/http override', () => {
|
|
|
191
843
|
expect(fsys.Open('ok')[0]).toBe(file)
|
|
192
844
|
expect(fsys.Open('missing')[1]?.message).toBe('missing')
|
|
193
845
|
})
|
|
846
|
+
|
|
847
|
+
it('serves files and omits HEAD response bodies', async () => {
|
|
848
|
+
const opened: string[] = []
|
|
849
|
+
const makeFile = () => {
|
|
850
|
+
const reader = bytes.NewReader($.stringToBytes('hello'))
|
|
851
|
+
return {
|
|
852
|
+
Close: () => null,
|
|
853
|
+
Read: (p: Uint8Array) => reader.Read(p),
|
|
854
|
+
Seek: (offset: number, whence: number) => reader.Seek(offset, whence),
|
|
855
|
+
Readdir: () => [null, null] as [null, null],
|
|
856
|
+
Stat: () => [
|
|
857
|
+
{
|
|
858
|
+
IsDir: () => false,
|
|
859
|
+
ModTime: () => null as never,
|
|
860
|
+
Mode: () => 0,
|
|
861
|
+
Name: () => 'file.txt',
|
|
862
|
+
Size: () => 5,
|
|
863
|
+
Sys: () => null,
|
|
864
|
+
},
|
|
865
|
+
null,
|
|
866
|
+
] as const,
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const root = FS({
|
|
870
|
+
Open: (name) => {
|
|
871
|
+
opened.push(name)
|
|
872
|
+
return name === 'file.txt' ? [makeFile(), null] : [null, new Error('missing')]
|
|
873
|
+
},
|
|
874
|
+
})
|
|
875
|
+
const writes: string[] = []
|
|
876
|
+
const header = new Header()
|
|
877
|
+
const writer: ResponseWriter = {
|
|
878
|
+
Header: () => header,
|
|
879
|
+
Write: (p) => {
|
|
880
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
881
|
+
return [p?.length ?? 0, null]
|
|
882
|
+
},
|
|
883
|
+
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
884
|
+
}
|
|
885
|
+
const handler = FileServer(root)
|
|
886
|
+
const [getReq] = NewRequest(MethodGet, 'http://example.invalid/../file.txt', null)
|
|
887
|
+
|
|
888
|
+
await handler.ServeHTTP(writer, getReq)
|
|
889
|
+
|
|
890
|
+
expect(opened).toEqual(['file.txt'])
|
|
891
|
+
expect(writes).toEqual(['status:200', 'hello'])
|
|
892
|
+
expect(Header_Get(header, 'Content-Length')).toBe('5')
|
|
893
|
+
|
|
894
|
+
writes.length = 0
|
|
895
|
+
opened.length = 0
|
|
896
|
+
const [headReq] = NewRequest(MethodHead, 'http://example.invalid/file.txt', null)
|
|
897
|
+
|
|
898
|
+
await handler.ServeHTTP(writer, headReq)
|
|
899
|
+
|
|
900
|
+
expect(opened).toEqual(['file.txt'])
|
|
901
|
+
expect(writes).toEqual(['status:200'])
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it('awaits ServeContent writes before returning', async () => {
|
|
905
|
+
const writes: string[] = []
|
|
906
|
+
const writer: ResponseWriter = {
|
|
907
|
+
Header: () => new Header(),
|
|
908
|
+
Write: (p) => {
|
|
909
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
910
|
+
return [p?.length ?? 0, null]
|
|
911
|
+
},
|
|
912
|
+
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
913
|
+
}
|
|
914
|
+
const [req] = NewRequest(MethodGet, 'http://example.invalid/content.txt', null)
|
|
915
|
+
|
|
916
|
+
await ServeContent(writer, req, 'content.txt', null as never, bytes.NewReader($.stringToBytes('served')))
|
|
917
|
+
|
|
918
|
+
expect(writes).toEqual(['status:200', 'served'])
|
|
919
|
+
|
|
920
|
+
writes.length = 0
|
|
921
|
+
const [headReq] = NewRequest(MethodHead, 'http://example.invalid/content.txt', null)
|
|
922
|
+
await ServeContent(writer, headReq, 'content.txt', null as never, bytes.NewReader($.stringToBytes('hidden')))
|
|
923
|
+
|
|
924
|
+
expect(writes).toEqual(['status:200'])
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
it('closes request bodies after file transport requests', async () => {
|
|
928
|
+
const root = FS({
|
|
929
|
+
Open: () => [null, new Error('missing')],
|
|
930
|
+
})
|
|
931
|
+
let closed = false
|
|
932
|
+
const [req] = NewRequest(MethodGet, 'file:///missing.txt', {
|
|
933
|
+
Read: () => [0, io.EOF],
|
|
934
|
+
Close: () => {
|
|
935
|
+
closed = true
|
|
936
|
+
return null
|
|
937
|
+
},
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
const [resp, err] = await NewFileTransport(root).RoundTrip(req)
|
|
941
|
+
|
|
942
|
+
expect(err).toBeNull()
|
|
943
|
+
expect(resp?.StatusCode).toBe(StatusNotFound)
|
|
944
|
+
expect(closed).toBe(true)
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
it('exports ServeFile for browser builds without local filesystem access', () => {
|
|
948
|
+
const writes: string[] = []
|
|
949
|
+
const writer: ResponseWriter = {
|
|
950
|
+
Header: () => new Header(),
|
|
951
|
+
Write: (p) => {
|
|
952
|
+
writes.push(Buffer.from(p ?? []).toString('utf8'))
|
|
953
|
+
return [p?.length ?? 0, null]
|
|
954
|
+
},
|
|
955
|
+
WriteHeader: (code) => writes.push(`status:${code}`),
|
|
956
|
+
}
|
|
957
|
+
const [req] = NewRequest(MethodGet, 'http://example.invalid/eval/missing.js', null)
|
|
958
|
+
|
|
959
|
+
ServeFile(writer, req, '/host-only/missing.js')
|
|
960
|
+
|
|
961
|
+
expect(writes[0]).toBe('status:404')
|
|
962
|
+
})
|
|
194
963
|
})
|