From 05df3fcd0743ec7674dcd80099405e543932c200 Mon Sep 17 00:00:00 2001 From: Kevin Tang <73975146+vt128@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:58:22 +0800 Subject: [PATCH] [test] web server handler httptest coverage (web 0% -> 50%) (STAR-53) The web-server request mode had zero coverage and had never been exercised end-to-end (the v0.1.0 cost-price audit's top P0 gap). Extract the per-request handler out of Start into a named handler() (behaviour-preserving) so it can be driven with httptest without binding a port, and add the suite: - the script's response (status + body) is written back to the client - a runtime error (fail()) becomes a 500 carrying the error text - request fields (method, body) reach the script via the injected `request` Start() keeps only the mux + ListenAndServe wiring (the untestable port bind + log.Fatalw remain uncovered). web/ coverage 0% -> 50%. Full -race green; Docker golang:1.25 floor green; gofmt/vet clean. --- web/webserver.go | 19 ++++++++--- web/webserver_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 web/webserver_test.go diff --git a/web/webserver.go b/web/webserver.go index bfe6f97..e00044e 100644 --- a/web/webserver.go +++ b/web/webserver.go @@ -10,10 +10,13 @@ import ( "go.uber.org/zap" ) -// Start starts a web server on the given port, builds and runs a Starbox instance for each request. -func Start(port uint16, builder func() *starbox.RunnerConfig) error { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +// handler builds the per-request HTTP handler. Each request builds and runs a +// fresh Starbox (via builder) with `request` and `response` injected as globals; +// the script populates `response`, which is then written back. A runtime error +// becomes a 500 carrying the error text. It is split out of Start so it can be +// exercised with httptest without binding a port. +func handler(builder func() *starbox.RunnerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { // prepare envs resp := shttp.NewServerResponse() mac := builder().KeyValueMap(starlet.StringAnyMap{ @@ -41,7 +44,13 @@ func Start(port uint16, builder func() *starbox.RunnerConfig) error { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) } - }) + } +} + +// Start starts a web server on the given port, builds and runs a Starbox instance for each request. +func Start(port uint16, builder func() *starbox.RunnerConfig) error { + mux := http.NewServeMux() + mux.HandleFunc("/", handler(builder)) log.Infow("web server started", "port", port) err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux) diff --git a/web/webserver_test.go b/web/webserver_test.go new file mode 100644 index 0000000..0ed68b5 --- /dev/null +++ b/web/webserver_test.go @@ -0,0 +1,79 @@ +package web + +// Behavior tests for the web-server request handler, exercised through httptest +// (no port binding). Each request builds a fresh Starbox whose script is given +// the injected `request` / `response` globals. +// +// Sections: +// - the script's response (status + body) is written back +// - a runtime error becomes a 500 carrying the error text +// - request fields (method, body, …) reach the script + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/1set/starbox" +) + +// builderFor returns a per-request builder whose Starbox runs the given script. +// handler() injects `request` and `response` as globals before executing it. +func builderFor(script string) func() *starbox.RunnerConfig { + return func() *starbox.RunnerConfig { + return starbox.New("webtest").CreateRunConfig().Script(script) + } +} + +func TestHandler_WritesScriptResponse(t *testing.T) { + h := handler(builderFor(` +response.set_status(201) +response.set_text("hello " + request.method) +`)) + rec := httptest.NewRecorder() + h(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + + res := rec.Result() + defer res.Body.Close() + if res.StatusCode != 201 { + t.Errorf("status = %d, want 201", res.StatusCode) + } + body, _ := io.ReadAll(res.Body) + if got := string(body); got != "hello GET" { + t.Errorf("body = %q, want %q", got, "hello GET") + } +} + +func TestHandler_RuntimeErrorIs500(t *testing.T) { + h := handler(builderFor(`fail("boom")`)) + rec := httptest.NewRecorder() + h(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + + res := rec.Result() + defer res.Body.Close() + if res.StatusCode != http.StatusInternalServerError { + t.Errorf("status = %d, want 500", res.StatusCode) + } + body, _ := io.ReadAll(res.Body) + if s := string(body); !strings.Contains(s, "Runtime Error") || !strings.Contains(s, "boom") { + t.Errorf("body = %q, want it to report the runtime error and 'boom'", s) + } +} + +func TestHandler_InjectsRequestFields(t *testing.T) { + h := handler(builderFor(`response.set_text(request.method + " " + request.body)`)) + rec := httptest.NewRecorder() + h(rec, httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader("payload"))) + + res := rec.Result() + defer res.Body.Close() + if res.StatusCode != 200 { + t.Errorf("status = %d, want 200", res.StatusCode) + } + body, _ := io.ReadAll(res.Body) + if got := string(body); got != "POST payload" { + t.Errorf("body = %q, want %q (request method+body should reach the script)", got, "POST payload") + } +}