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") + } +}