goscript 0.1.4 → 0.2.1

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.
Files changed (295) hide show
  1. package/README.md +5 -2
  2. package/cmd/go_js_wasm_exec/main.go +201 -0
  3. package/cmd/go_js_wasm_exec/main_test.go +83 -0
  4. package/cmd/goscript/{cmd_compile.go → cmd-compile.go} +7 -0
  5. package/cmd/goscript/cmd-test.go +14 -0
  6. package/cmd/goscript/cmd-test_test.go +1 -1
  7. package/cmd/goscript-wasm/main.go +38 -6
  8. package/compiler/compile-request.go +12 -9
  9. package/compiler/compliance_test.go +0 -1
  10. package/compiler/config.go +2 -0
  11. package/compiler/diagnostic.go +104 -12
  12. package/compiler/diagnostic_test.go +106 -0
  13. package/compiler/gotest/request.go +28 -0
  14. package/compiler/gotest/runner.go +354 -44
  15. package/compiler/gotest/runner_test.go +293 -1
  16. package/compiler/gotest/testdata/browserapi/browserapi_test.go +20 -0
  17. package/compiler/gotest/testdata/browserapi/go.mod +3 -0
  18. package/compiler/index.test.ts +23 -0
  19. package/compiler/lowered-program.go +33 -24
  20. package/compiler/lowering.go +746 -194
  21. package/compiler/lowering_bench_test.go +42 -27
  22. package/compiler/lowering_internal_test.go +18 -0
  23. package/compiler/override-facts.go +15 -0
  24. package/compiler/override-parity-verifier.go +450 -0
  25. package/compiler/override-parity.go +122 -0
  26. package/compiler/override-registry_test.go +559 -0
  27. package/compiler/protobuf-ts-binding.go +567 -0
  28. package/compiler/protobuf-ts-binding_test.go +402 -0
  29. package/compiler/runtime-contract.go +4 -0
  30. package/compiler/runtime-contract_test.go +2 -0
  31. package/compiler/semantic-model-types.go +9 -4
  32. package/compiler/semantic-model.go +282 -70
  33. package/compiler/semantic-model_test.go +82 -1
  34. package/compiler/service.go +21 -1
  35. package/compiler/skeleton_test.go +118 -10
  36. package/compiler/typescript-emitter.go +128 -13
  37. package/compiler/wasm/compile_test.go +37 -4
  38. package/compiler/{wasm_api.go → wasm-api.go} +57 -7
  39. package/dist/gs/builtin/hostio.js +5 -0
  40. package/dist/gs/builtin/hostio.js.map +1 -1
  41. package/dist/gs/builtin/slice.d.ts +13 -2
  42. package/dist/gs/builtin/slice.js +187 -6
  43. package/dist/gs/builtin/slice.js.map +1 -1
  44. package/dist/gs/builtin/type.d.ts +13 -5
  45. package/dist/gs/builtin/type.js +153 -60
  46. package/dist/gs/builtin/type.js.map +1 -1
  47. package/dist/gs/builtin/varRef.d.ts +11 -0
  48. package/dist/gs/builtin/varRef.js +57 -2
  49. package/dist/gs/builtin/varRef.js.map +1 -1
  50. package/dist/gs/bytes/buffer.gs.js +1 -1
  51. package/dist/gs/bytes/buffer.gs.js.map +1 -1
  52. package/dist/gs/bytes/reader.gs.js +1 -1
  53. package/dist/gs/bytes/reader.gs.js.map +1 -1
  54. package/dist/gs/compress/zlib/index.d.ts +10 -3
  55. package/dist/gs/compress/zlib/index.js +50 -16
  56. package/dist/gs/compress/zlib/index.js.map +1 -1
  57. package/dist/gs/encoding/json/index.d.ts +114 -0
  58. package/dist/gs/encoding/json/index.js +544 -36
  59. package/dist/gs/encoding/json/index.js.map +1 -1
  60. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +101 -0
  61. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +589 -0
  62. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
  63. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.d.ts +1 -0
  64. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.js +17 -11
  65. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.js.map +1 -1
  66. package/dist/gs/github.com/pkg/errors/errors.js +54 -30
  67. package/dist/gs/github.com/pkg/errors/errors.js.map +1 -1
  68. package/dist/gs/go/scanner/index.d.ts +2 -0
  69. package/dist/gs/go/scanner/index.js +29 -5
  70. package/dist/gs/go/scanner/index.js.map +1 -1
  71. package/dist/gs/go/token/index.js +22 -6
  72. package/dist/gs/go/token/index.js.map +1 -1
  73. package/dist/gs/hash/index.d.ts +6 -0
  74. package/dist/gs/hash/index.js +20 -0
  75. package/dist/gs/hash/index.js.map +1 -1
  76. package/dist/gs/internal/byteorder/index.js +2 -2
  77. package/dist/gs/internal/byteorder/index.js.map +1 -1
  78. package/dist/gs/internal/goarch/index.d.ts +43 -3
  79. package/dist/gs/internal/goarch/index.js +42 -10
  80. package/dist/gs/internal/goarch/index.js.map +1 -1
  81. package/dist/gs/io/fs/fs.js +26 -14
  82. package/dist/gs/io/fs/fs.js.map +1 -1
  83. package/dist/gs/io/fs/readdir.js +4 -2
  84. package/dist/gs/io/fs/readdir.js.map +1 -1
  85. package/dist/gs/io/fs/sub.js +8 -1
  86. package/dist/gs/io/fs/sub.js.map +1 -1
  87. package/dist/gs/io/io.d.ts +2 -0
  88. package/dist/gs/io/io.js.map +1 -1
  89. package/dist/gs/math/bits/index.d.ts +5 -0
  90. package/dist/gs/math/bits/index.js +16 -4
  91. package/dist/gs/math/bits/index.js.map +1 -1
  92. package/dist/gs/mime/index.d.ts +16 -0
  93. package/dist/gs/mime/index.js +315 -6
  94. package/dist/gs/mime/index.js.map +1 -1
  95. package/dist/gs/net/http/httptest/index.d.ts +12 -0
  96. package/dist/gs/net/http/httptest/index.js +85 -6
  97. package/dist/gs/net/http/httptest/index.js.map +1 -1
  98. package/dist/gs/net/http/index.d.ts +300 -5
  99. package/dist/gs/net/http/index.js +1598 -58
  100. package/dist/gs/net/http/index.js.map +1 -1
  101. package/dist/gs/os/dir_unix.gs.js +1 -1
  102. package/dist/gs/os/dir_unix.gs.js.map +1 -1
  103. package/dist/gs/os/error.gs.js +1 -1
  104. package/dist/gs/os/error.gs.js.map +1 -1
  105. package/dist/gs/os/exec.gs.d.ts +1 -0
  106. package/dist/gs/os/exec.gs.js +4 -8
  107. package/dist/gs/os/exec.gs.js.map +1 -1
  108. package/dist/gs/os/exec_posix.gs.js +1 -1
  109. package/dist/gs/os/exec_posix.gs.js.map +1 -1
  110. package/dist/gs/os/index.d.ts +1 -1
  111. package/dist/gs/os/index.js +1 -1
  112. package/dist/gs/os/index.js.map +1 -1
  113. package/dist/gs/os/proc.gs.d.ts +4 -0
  114. package/dist/gs/os/proc.gs.js +12 -6
  115. package/dist/gs/os/proc.gs.js.map +1 -1
  116. package/dist/gs/os/root_js.gs.js +1 -1
  117. package/dist/gs/os/root_js.gs.js.map +1 -1
  118. package/dist/gs/os/types.gs.js +1 -1
  119. package/dist/gs/os/types.gs.js.map +1 -1
  120. package/dist/gs/os/types_js.gs.js +1 -1
  121. package/dist/gs/os/types_js.gs.js.map +1 -1
  122. package/dist/gs/os/types_unix.gs.js +1 -1
  123. package/dist/gs/os/types_unix.gs.js.map +1 -1
  124. package/dist/gs/path/path.js +11 -7
  125. package/dist/gs/path/path.js.map +1 -1
  126. package/dist/gs/reflect/index.d.ts +5 -4
  127. package/dist/gs/reflect/index.js +4 -3
  128. package/dist/gs/reflect/index.js.map +1 -1
  129. package/dist/gs/reflect/map.js +15 -0
  130. package/dist/gs/reflect/map.js.map +1 -1
  131. package/dist/gs/reflect/type.d.ts +25 -6
  132. package/dist/gs/reflect/type.js +1475 -228
  133. package/dist/gs/reflect/type.js.map +1 -1
  134. package/dist/gs/reflect/types.d.ts +14 -6
  135. package/dist/gs/reflect/types.js +35 -1
  136. package/dist/gs/reflect/types.js.map +1 -1
  137. package/dist/gs/reflect/value.d.ts +1 -0
  138. package/dist/gs/reflect/value.js +83 -41
  139. package/dist/gs/reflect/value.js.map +1 -1
  140. package/dist/gs/reflect/visiblefields.js +4 -140
  141. package/dist/gs/reflect/visiblefields.js.map +1 -1
  142. package/dist/gs/runtime/pprof/index.d.ts +8 -2
  143. package/dist/gs/runtime/pprof/index.js +50 -30
  144. package/dist/gs/runtime/pprof/index.js.map +1 -1
  145. package/dist/gs/runtime/runtime.js +5 -4
  146. package/dist/gs/runtime/runtime.js.map +1 -1
  147. package/dist/gs/runtime/trace/index.js +5 -19
  148. package/dist/gs/runtime/trace/index.js.map +1 -1
  149. package/dist/gs/strconv/atoi.gs.js +1 -1
  150. package/dist/gs/strconv/atoi.gs.js.map +1 -1
  151. package/dist/gs/strconv/complex.gs.d.ts +3 -0
  152. package/dist/gs/strconv/complex.gs.js +148 -0
  153. package/dist/gs/strconv/complex.gs.js.map +1 -0
  154. package/dist/gs/strconv/index.d.ts +1 -0
  155. package/dist/gs/strconv/index.js +1 -0
  156. package/dist/gs/strconv/index.js.map +1 -1
  157. package/dist/gs/strings/builder.js +1 -1
  158. package/dist/gs/strings/reader.js +9 -5
  159. package/dist/gs/strings/reader.js.map +1 -1
  160. package/dist/gs/strings/replace.js +15 -7
  161. package/dist/gs/strings/replace.js.map +1 -1
  162. package/dist/gs/strings/strings.d.ts +5 -0
  163. package/dist/gs/strings/strings.js +57 -5
  164. package/dist/gs/strings/strings.js.map +1 -1
  165. package/dist/gs/sync/atomic/doc_64.gs.js +7 -6
  166. package/dist/gs/sync/atomic/doc_64.gs.js.map +1 -1
  167. package/dist/gs/sync/atomic/type.gs.js +9 -9
  168. package/dist/gs/sync/atomic/type.gs.js.map +1 -1
  169. package/dist/gs/sync/atomic/value.gs.js +2 -2
  170. package/dist/gs/sync/atomic/value.gs.js.map +1 -1
  171. package/dist/gs/syscall/env.js +22 -14
  172. package/dist/gs/syscall/env.js.map +1 -1
  173. package/dist/gs/testing/testing.js +55 -13
  174. package/dist/gs/testing/testing.js.map +1 -1
  175. package/dist/gs/time/time.d.ts +24 -1
  176. package/dist/gs/time/time.js +43 -3
  177. package/dist/gs/time/time.js.map +1 -1
  178. package/dist/gs/unique/index.js +7 -1
  179. package/dist/gs/unique/index.js.map +1 -1
  180. package/go.mod +3 -3
  181. package/go.sum +16 -0
  182. package/gs/builtin/hostio.test.ts +16 -0
  183. package/gs/builtin/hostio.ts +7 -0
  184. package/gs/builtin/runtime-contract.test.ts +246 -21
  185. package/gs/builtin/slice.ts +269 -24
  186. package/gs/builtin/type.ts +226 -59
  187. package/gs/builtin/varRef.ts +85 -2
  188. package/gs/bytes/buffer.gs.ts +1 -1
  189. package/gs/bytes/reader.gs.ts +1 -1
  190. package/gs/compress/zlib/index.test.ts +62 -1
  191. package/gs/compress/zlib/index.ts +53 -16
  192. package/gs/compress/zlib/parity.json +51 -0
  193. package/gs/encoding/json/index.test.ts +360 -6
  194. package/gs/encoding/json/index.ts +679 -38
  195. package/gs/encoding/json/parity.json +81 -0
  196. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +373 -3
  197. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +893 -1
  198. package/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.test.ts +18 -0
  199. package/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.ts +17 -11
  200. package/gs/github.com/pkg/errors/errors.ts +54 -30
  201. package/gs/go/scanner/index.test.ts +39 -56
  202. package/gs/go/scanner/index.ts +33 -5
  203. package/gs/go/scanner/parity.json +27 -0
  204. package/gs/go/token/index.ts +22 -6
  205. package/gs/hash/index.test.ts +20 -33
  206. package/gs/hash/index.ts +28 -0
  207. package/gs/hash/parity.json +21 -0
  208. package/gs/internal/byteorder/index.test.ts +2 -2
  209. package/gs/internal/byteorder/index.ts +2 -2
  210. package/gs/internal/goarch/index.test.ts +32 -0
  211. package/gs/internal/goarch/index.ts +45 -13
  212. package/gs/internal/goarch/parity.json +144 -0
  213. package/gs/io/fs/fs.ts +26 -14
  214. package/gs/io/fs/readdir.ts +4 -4
  215. package/gs/io/fs/sub.ts +8 -1
  216. package/gs/io/io.ts +1 -0
  217. package/gs/io/parity.json +162 -0
  218. package/gs/math/bits/index.test.ts +14 -1
  219. package/gs/math/bits/index.ts +23 -4
  220. package/gs/math/bits/parity.json +156 -0
  221. package/gs/mime/index.test.ts +90 -0
  222. package/gs/mime/index.ts +369 -6
  223. package/gs/mime/parity.json +36 -0
  224. package/gs/net/http/httptest/index.test.ts +98 -2
  225. package/gs/net/http/httptest/index.ts +101 -6
  226. package/gs/net/http/httptest/parity.json +15 -0
  227. package/gs/net/http/index.test.ts +781 -12
  228. package/gs/net/http/index.ts +1860 -139
  229. package/gs/net/http/meta.json +16 -1
  230. package/gs/net/http/parity.json +193 -0
  231. package/gs/os/dir_unix.gs.ts +1 -1
  232. package/gs/os/error.gs.ts +1 -1
  233. package/gs/os/exec.gs.ts +4 -8
  234. package/gs/os/exec_posix.gs.ts +1 -1
  235. package/gs/os/index.test.ts +9 -0
  236. package/gs/os/index.ts +1 -0
  237. package/gs/os/parity.json +9 -0
  238. package/gs/os/proc.gs.ts +18 -5
  239. package/gs/os/proc.test.ts +26 -0
  240. package/gs/os/root_js.gs.ts +1 -1
  241. package/gs/os/types.gs.ts +1 -1
  242. package/gs/os/types_js.gs.ts +1 -1
  243. package/gs/os/types_unix.gs.ts +1 -1
  244. package/gs/path/path.ts +11 -7
  245. package/gs/reflect/field.test.ts +37 -15
  246. package/gs/reflect/function-types.test.ts +518 -22
  247. package/gs/reflect/index.ts +8 -6
  248. package/gs/reflect/map.ts +20 -0
  249. package/gs/reflect/meta.json +6 -4
  250. package/gs/reflect/parity.json +234 -0
  251. package/gs/reflect/sliceat.test.ts +156 -0
  252. package/gs/reflect/structof.test.ts +401 -0
  253. package/gs/reflect/type.ts +1961 -317
  254. package/gs/reflect/typefor.test.ts +530 -10
  255. package/gs/reflect/types.ts +43 -18
  256. package/gs/reflect/value.ts +105 -45
  257. package/gs/reflect/visiblefields.ts +5 -168
  258. package/gs/runtime/parity.json +24 -0
  259. package/gs/runtime/pprof/index.test.ts +29 -7
  260. package/gs/runtime/pprof/index.ts +56 -30
  261. package/gs/runtime/pprof/parity.json +27 -0
  262. package/gs/runtime/runtime.test.ts +3 -1
  263. package/gs/runtime/runtime.ts +4 -3
  264. package/gs/runtime/trace/index.test.ts +5 -3
  265. package/gs/runtime/trace/index.ts +8 -20
  266. package/gs/runtime/trace/parity.json +36 -0
  267. package/gs/strconv/atoi.gs.ts +1 -1
  268. package/gs/strconv/complex.gs.ts +174 -0
  269. package/gs/strconv/complex.test.ts +65 -0
  270. package/gs/strconv/index.ts +1 -0
  271. package/gs/strconv/parity.json +120 -0
  272. package/gs/strings/builder.ts +1 -1
  273. package/gs/strings/parity.json +186 -0
  274. package/gs/strings/reader.ts +9 -5
  275. package/gs/strings/replace.ts +15 -7
  276. package/gs/strings/strings.test.ts +22 -2
  277. package/gs/strings/strings.ts +64 -6
  278. package/gs/sync/atomic/doc_64.gs.ts +6 -7
  279. package/gs/sync/atomic/doc_64.test.ts +43 -0
  280. package/gs/sync/atomic/type.gs.ts +9 -9
  281. package/gs/sync/atomic/value.gs.ts +2 -2
  282. package/gs/syscall/env.ts +29 -14
  283. package/gs/testing/testing.test.ts +67 -0
  284. package/gs/testing/testing.ts +87 -19
  285. package/gs/time/parity.json +225 -0
  286. package/gs/time/time.test.ts +20 -2
  287. package/gs/time/time.ts +49 -7
  288. package/gs/unique/index.ts +7 -1
  289. package/package.json +4 -2
  290. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.d.ts +0 -217
  291. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +0 -926
  292. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +0 -1
  293. package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +0 -38
  294. package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +0 -1361
  295. package/gs/github.com/aperturerobotics/starpc/srpc/meta.json +0 -46
@@ -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
- Get,
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('returns an explicit unsupported error for Get', () => {
58
- const [resp, err] = Get('https://example.invalid')
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(resp).toBeNull()
61
- expect(err?.Error()).toBe('net/http: Get is not implemented in GoScript')
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!.RequestURI).toBe('/')
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()).toBe('net/http: Client.Do is not implemented in GoScript')
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 the default transport surface', async () => {
92
- const [req] = NewRequest(MethodPost, 'https://example.invalid', null)
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?.Error()).toBe('net/http: Client.Do is not implemented in GoScript')
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('/path')
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
  })