Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ jobs:
packages/wasm/go.mod
packages/wasm/go.sum

- name: Set up Node
uses: actions/setup-node@v7
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
node-version: "24"

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
Expand Down
217 changes: 217 additions & 0 deletions packages/wasm/http_wasm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package main

import (
"ballerina/platform/pal"
"context"
"io"
"net/http"
"reflect"
"syscall/js"
"testing"
)

func useTestRunContext(t *testing.T) *runContext {
t.Helper()

previous := activeRun
ctx := &runContext{listeners: make(map[string]http.Handler)}
activeRun = ctx
t.Cleanup(func() {
activeRun = previous
})
return ctx
}

func registerTestHandler(t *testing.T, ctx *runContext, cfg pal.ServerConfig, handler http.Handler) {
t.Helper()

if _, err := ctx.registerListener(cfg, handler); err != nil {
t.Fatalf("register listener: %v", err)
}
}

func TestExecuteLocalRequest(t *testing.T) {
ctx := useTestRunContext(t)
cfg := pal.ServerConfig{Host: "0.0.0.0", Port: 9090}
var received *http.Request
var receivedBody []byte
registerTestHandler(t, ctx, cfg, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r
receivedBody, _ = io.ReadAll(r.Body)
w.Header().Add("X-Response", "one")
w.Header().Add("X-Response", "two")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte("response body"))
}))

client := &fetchHTTPClient{cfg: pal.ClientConfig{ResponseLimits: pal.ResponseLimitConfig{MaxEntityBodySize: -1}}}
status, headers, body, handled, err := client.executeLocalRequest(
context.Background(),
http.MethodPost,
"http://localhost:9090/resource?name=Jane",
[]byte("request body"),
"application/json",
map[string][]string{
"Content-Type": {"text/plain"},
"X-Request": {"one", "two"},
},
)
if err != nil {
t.Fatalf("execute local request: %v", err)
}
if !handled {
t.Fatal("expected loopback request to be handled")
}
if status != http.StatusCreated {
t.Errorf("status = %d, want %d", status, http.StatusCreated)
}
if got, want := headers["X-Response"], []string{"one", "two"}; !reflect.DeepEqual(got, want) {
t.Errorf("response headers = %v, want %v", got, want)
}
gotBody, err := io.ReadAll(body)
if err != nil {
t.Fatalf("read response body: %v", err)
}
if got, want := string(gotBody), "response body"; got != want {
t.Errorf("response body = %q, want %q", got, want)
}

if received == nil {
t.Fatal("handler did not receive a request")
}
if got, want := received.Method, http.MethodPost; got != want {
t.Errorf("method = %q, want %q", got, want)
}
if got, want := received.RequestURI, "/resource?name=Jane"; got != want {
t.Errorf("request URI = %q, want %q", got, want)
}
if got, want := received.Host, "localhost:9090"; got != want {
t.Errorf("host = %q, want %q", got, want)
}
if got, want := received.Header.Values("X-Request"), []string{"one", "two"}; !reflect.DeepEqual(got, want) {
t.Errorf("request headers = %v, want %v", got, want)
}
if got, want := received.Header.Get("Content-Type"), "application/json"; got != want {
t.Errorf("content type = %q, want %q", got, want)
}
if got, want := string(receivedBody), "request body"; got != want {
t.Errorf("request body = %q, want %q", got, want)
}
}

func TestExecuteLocalRequestResolvesLoopbackAliases(t *testing.T) {
ctx := useTestRunContext(t)
registerTestHandler(t, ctx, pal.ServerConfig{Host: "0.0.0.0", Port: 9090}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ipv4"))
}))
registerTestHandler(t, ctx, pal.ServerConfig{Host: "::1", Port: 9091}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ipv6"))
}))

client := &fetchHTTPClient{cfg: pal.ClientConfig{ResponseLimits: pal.ResponseLimitConfig{MaxEntityBodySize: -1}}}
for _, tc := range []struct {
url string
want string
}{
{url: "http://localhost:9090", want: "ipv4"},
{url: "http://127.0.0.1:9090", want: "ipv4"},
{url: "http://[::1]:9091", want: "ipv6"},
} {
t.Run(tc.url, func(t *testing.T) {
_, _, body, handled, err := client.executeLocalRequest(context.Background(), http.MethodGet, tc.url, nil, "", nil)
if err != nil {
t.Fatalf("execute local request: %v", err)
}
if !handled {
t.Fatal("expected loopback request to be handled")
}
got, err := io.ReadAll(body)
if err != nil {
t.Fatalf("read response body: %v", err)
}
if string(got) != tc.want {
t.Errorf("response body = %q, want %q", got, tc.want)
}
})
}
}

func TestExecuteLocalRequestFallsBackWhenNotHandled(t *testing.T) {
client := &fetchHTTPClient{cfg: pal.ClientConfig{ResponseLimits: pal.ResponseLimitConfig{MaxEntityBodySize: -1}}}
for _, tc := range []struct {
name string
url string
}{
{name: "unregistered loopback host", url: "http://localhost:9090"},
{name: "non-loopback host", url: "https://example.com"},
} {
t.Run(tc.name, func(t *testing.T) {
_, _, _, handled, err := client.executeLocalRequest(context.Background(), http.MethodGet, tc.url, nil, "", nil)
if err != nil {
t.Fatalf("execute local request: %v", err)
}
if handled {
t.Fatal("expected request to fall back to browser fetch")
}
})
}
}

func TestExecuteLocalRequestRejectsOversizedResponse(t *testing.T) {
ctx := useTestRunContext(t)
registerTestHandler(t, ctx, pal.ServerConfig{Host: "localhost", Port: 9090}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("too large"))
}))

client := &fetchHTTPClient{cfg: pal.ClientConfig{ResponseLimits: pal.ResponseLimitConfig{MaxEntityBodySize: 3}}}
_, _, _, handled, err := client.executeLocalRequest(context.Background(), http.MethodGet, "http://localhost:9090", nil, "", nil)
if !handled {
t.Fatal("expected loopback request to be handled")
}
if err == nil {
t.Fatal("expected oversized response to return an error")
}
}

func TestHTTPRequestsFromJS(t *testing.T) {
req, err := httpRequestFromJS(js.ValueOf(map[string]any{
"method": "post",
"host": "localhost:9090",
"path": "albums/Kind%20of%20Blue",
"query": "?genre=jazz",
"body": "request body",
"headers": map[string]any{
"X-Request": []any{"one", "two"},
"Content-Type": "text/plain",
},
}))
if err != nil {
t.Fatalf("create HTTP request: %v", err)
}

if got, want := req.Method, http.MethodPost; got != want {
t.Errorf("method = %q, want %q", got, want)
}
if got, want := req.URL.Path, "/albums/Kind of Blue"; got != want {
t.Errorf("path = %q, want %q", got, want)
}
if got, want := req.URL.EscapedPath(), "/albums/Kind%20of%20Blue"; got != want {
t.Errorf("escaped path = %q, want %q", got, want)
}
if got, want := req.URL.RawQuery, "genre=jazz"; got != want {
t.Errorf("query = %q, want %q", got, want)
}
if got, want := req.Header.Values("X-Request"), []string{"one", "two"}; !reflect.DeepEqual(got, want) {
t.Errorf("request headers = %v, want %v", got, want)
}
if got, want := req.Header.Get("Content-Type"), "text/plain"; got != want {
t.Errorf("content type = %q, want %q", got, want)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Fatalf("read request body: %v", err)
}
if got, want := string(body), "request body"; got != want {
t.Errorf("body = %q, want %q", got, want)
}
}
1 change: 1 addition & 0 deletions packages/wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"scripts": {
"build": "GOOS=js GOARCH=wasm go build -o dist/ballerina.wasm . && ./scripts/gen-meta.sh",
"test": "GOOS=js GOARCH=wasm go test -exec=\"$(go env GOROOT)/lib/wasm/go_js_wasm_exec\" .",
"clean": "git clean -xdf .turbo dist"
}
}
79 changes: 79 additions & 0 deletions packages/wasm/run_context_wasm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package main

import (
"ballerina/platform/pal"
"context"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)

func TestRunContextListenerLifecycle(t *testing.T) {
ctx := useTestRunContext(t)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we run these in parallel

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to run the tests in parallel

cfg := pal.ServerConfig{Host: "localhost", Port: 9090}
firstHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("first handler"))
})
secondHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("second handler"))
})

handle, err := ctx.registerListener(cfg, firstHandler)
if err != nil {
t.Fatalf("register listener: %v", err)
}
if _, err := ctx.registerListener(cfg, secondHandler); err == nil {
t.Fatal("expected duplicate listener registration to fail")
}
registeredHandler, ok := ctx.getHandler("localhost:9090")
if !ok {
t.Fatal("registered listener was not returned")
}
recorder := httptest.NewRecorder()
registeredHandler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
if got, want := recorder.Body.String(), "first handler"; got != want {
t.Errorf("registered handler response = %q, want %q", got, want)
}

if err := handle.Close(); err != nil {
t.Fatalf("close listener: %v", err)
}
if _, ok := ctx.getHandler("localhost:9090"); ok {
t.Error("closed listener is still registered")
}

handle, err = ctx.registerListener(cfg, firstHandler)
if err != nil {
t.Fatalf("register listener again: %v", err)
}
if err := handle.Shutdown(context.Background()); err != nil {
t.Fatalf("shutdown listener: %v", err)
}
if _, ok := ctx.getHandler("localhost:9090"); ok {
t.Error("shutdown listener is still registered")
}
}

func TestRunContextHostsAreSorted(t *testing.T) {
ctx := useTestRunContext(t)
for _, cfg := range []pal.ServerConfig{
{Host: "localhost", Port: 9091},
{Host: "localhost", Port: 9090},
{Host: "127.0.0.1", Port: 9090},
} {
registerTestHandler(t, ctx, cfg, http.NotFoundHandler())
}

want := []any{"127.0.0.1:9090", "localhost:9090", "localhost:9091"}
if got := ctx.hosts(); !reflect.DeepEqual(got, want) {
t.Errorf("hosts = %v, want %v", got, want)
}
}

func TestListenerHostFormatsIPv6(t *testing.T) {
cfg := pal.ServerConfig{Host: "::1", Port: 9090}
if got, want := listenerHost(cfg), "[::1]:9090"; got != want {
t.Errorf("listener host = %q, want %q", got, want)
}
}
Loading