diff options
| author | Shulhan <ms@kilabit.info> | 2024-03-09 05:02:47 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2024-03-14 23:51:34 +0700 |
| commit | 8d9c5ff8958b6adcd01e86f9a48f256df388b9db (patch) | |
| tree | 57dfb7644b2db6e295ec77698366449b6a5da9f6 | |
| parent | 29343294a596fd74f6f9cb917c8aac74ccb78094 (diff) | |
| download | pakakeh.go-8d9c5ff8958b6adcd01e86f9a48f256df388b9db.tar.xz | |
test/httptest: new helper for testing HTTP server handler
The Simulate function simulate HTTP server handler by generating
[http.Request] from fields in [SimulateRequest]; and then call
[http.HandlerFunc].
The HTTP response from serve along with its raw body and original HTTP
request then returned in [*SimulateResult].
| -rw-r--r-- | Makefile | 1 | ||||
| -rw-r--r-- | lib/test/httptest/httptest.go | 59 | ||||
| -rw-r--r-- | lib/test/httptest/httptest_test.go | 94 | ||||
| -rw-r--r-- | lib/test/httptest/simulate_request.go | 40 | ||||
| -rw-r--r-- | lib/test/httptest/simulate_result.go | 85 | ||||
| -rw-r--r-- | lib/test/httptest/simulate_result_test.go | 122 |
6 files changed, 401 insertions, 0 deletions
@@ -42,6 +42,7 @@ lint: --presets bugs,metalinter,performance,unused \ --disable exhaustive \ --disable musttag \ + --disable bodyclose \ ./... $(CIIGO): diff --git a/lib/test/httptest/httptest.go b/lib/test/httptest/httptest.go new file mode 100644 index 00000000..ff5b4c2d --- /dev/null +++ b/lib/test/httptest/httptest.go @@ -0,0 +1,59 @@ +// Copyright 2024, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package httptest implement testing HTTP package. +package httptest + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" +) + +// Simulate HTTP server handler by generating [http.Request] from +// fields in [SimulateRequest]; and then call [http.HandlerFunc]. +// The HTTP response from serve along with its raw body and original HTTP +// request then returned in [*SimulateResult]. +func Simulate(serve http.HandlerFunc, req *SimulateRequest) (result *SimulateResult, err error) { + var logp = `Simulate` + + result = &SimulateResult{ + Request: req.toHTTPRequest(), + } + + var httpWriter = httptest.NewRecorder() + + serve(httpWriter, result.Request) + + result.Response = httpWriter.Result() //nolint:bodyclose + + result.ResponseBody, err = io.ReadAll(result.Response.Body) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + err = result.Response.Body.Close() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + if len(req.JSONIndentResponse) != 0 { + var dst bytes.Buffer + err = json.Indent(&dst, result.ResponseBody, ``, req.JSONIndentResponse) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + result.ResponseBody = dst.Bytes() + } + + // Recreate the bodies to prevent panic when user trying to inspect + // request or response body. + + result.Request.Body = io.NopCloser(bytes.NewReader(req.Body)) + result.Response.Body = io.NopCloser(bytes.NewReader(result.ResponseBody)) + + return result, nil +} diff --git a/lib/test/httptest/httptest_test.go b/lib/test/httptest/httptest_test.go new file mode 100644 index 00000000..28bf031d --- /dev/null +++ b/lib/test/httptest/httptest_test.go @@ -0,0 +1,94 @@ +package httptest_test + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test/httptest" +) + +func ExampleSimulate() { + http.HandleFunc(`/a/b/c`, func(w http.ResponseWriter, req *http.Request) { + _ = req.ParseForm() + var ( + rawjson []byte + err error + ) + rawjson, err = json.Marshal(req.PostForm) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set(`Content-Type`, `application/json`) + _, _ = w.Write(rawjson) + }) + + var simreq = &httptest.SimulateRequest{ + Method: http.MethodPost, + Path: `/a/b/c`, + Header: http.Header{ + `Content-Type`: []string{`application/x-www-form-urlencoded`}, + }, + Body: []byte(`id=1&name=go`), + JSONIndentResponse: ` `, + } + + var ( + result *httptest.SimulateResult + err error + ) + + result, err = httptest.Simulate(http.DefaultServeMux.ServeHTTP, simreq) + if err != nil { + log.Fatal(err) + } + + var dump []byte + + dump, err = result.DumpRequest(nil) + if err != nil { + log.Fatal(err) + } + fmt.Printf("<<< RequestDump:\n%s\n\n", dump) + + dump, err = result.DumpResponse(nil) + if err != nil { + log.Fatal(err) + } + fmt.Printf("<<< ResponseDump:\n%s\n\n", dump) + fmt.Printf("<<< ResponseBody:\n%s\n", result.ResponseBody) + + // Output: + // <<< RequestDump: + // POST /a/b/c HTTP/1.1 + // Host: example.com + // Content-Type: application/x-www-form-urlencoded + // + // id=1&name=go + // + // <<< ResponseDump: + // HTTP/1.1 200 OK + // Connection: close + // Content-Type: application/json + // + // { + // "id": [ + // "1" + // ], + // "name": [ + // "go" + // ] + // } + // + // <<< ResponseBody: + // { + // "id": [ + // "1" + // ], + // "name": [ + // "go" + // ] + // } +} diff --git a/lib/test/httptest/simulate_request.go b/lib/test/httptest/simulate_request.go new file mode 100644 index 00000000..fe931ce9 --- /dev/null +++ b/lib/test/httptest/simulate_request.go @@ -0,0 +1,40 @@ +package httptest + +import ( + "bytes" + "net/http" + "net/http/httptest" +) + +// SimulateRequest request to simulate [http.ServeHTTP]. +type SimulateRequest struct { + Method string + Path string + + // JSONIndentResponse if not empty, the response body will be + // indented using [json.Indent]. + JSONIndentResponse string + + Header http.Header + Body []byte +} + +func (simreq *SimulateRequest) toHTTPRequest() (req *http.Request) { + var body bytes.Buffer + + body.Write(simreq.Body) + + req = httptest.NewRequest(simreq.Method, simreq.Path, &body) + + var ( + key string + val string + vals []string + ) + for key, vals = range simreq.Header { + for _, val = range vals { + req.Header.Add(key, val) + } + } + return req +} diff --git a/lib/test/httptest/simulate_result.go b/lib/test/httptest/simulate_result.go new file mode 100644 index 00000000..a37aa623 --- /dev/null +++ b/lib/test/httptest/simulate_result.go @@ -0,0 +1,85 @@ +package httptest + +import ( + "bytes" + "fmt" + "maps" + "net/http" + "net/http/httputil" +) + +// SimulateResult contains [http.Request] and [http.Response] from calling +// [http.ServeHTTP]. +type SimulateResult struct { + Request *http.Request `json:"-"` + Response *http.Response `json:"-"` + + RequestDump []byte + ResponseDump []byte + ResponseBody []byte +} + +// DumpRequest convert [SimulateResult.Request] with its body to stream of +// bytes using [httputil.DumpRequest]. +// +// The returned bytes have CRLF ("\r\n") replaced with single LF ("\n"). +// +// Any request headers that match with excludeHeaders will be deleted before +// dumping. +func (result *SimulateResult) DumpRequest(excludeHeaders []string) (raw []byte, err error) { + if result.RequestDump != nil { + return result.RequestDump, nil + } + + var ( + logp = `DumpRequest` + orgHeader = maps.Clone(result.Request.Header) + header string + ) + + for _, header = range excludeHeaders { + result.Request.Header.Del(header) + } + + raw, err = httputil.DumpRequest(result.Request, true) + result.Request.Header = orgHeader + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + result.RequestDump = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'}) + + return result.RequestDump, nil +} + +// DumpResponse convert [SimulateResult.Response] with its body to stream of +// bytes using [httputil.DumpResponse]. +// +// The returned bytes have CRLF ("\r\n") replaced with single LF ("\n"). +// +// Any response headers that match with excludeHeaders will be deleted +// before dumping. +func (result *SimulateResult) DumpResponse(excludeHeaders []string) (raw []byte, err error) { + if result.ResponseDump != nil { + return result.ResponseDump, nil + } + + var ( + logp = `DumpResponse` + orgHeader = maps.Clone(result.Response.Header) + header string + ) + for _, header = range excludeHeaders { + result.Response.Header.Del(header) + } + + raw, err = httputil.DumpResponse(result.Response, true) + result.Response.Header = orgHeader + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + result.ResponseDump = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'}) + + return result.ResponseDump, nil +} diff --git a/lib/test/httptest/simulate_result_test.go b/lib/test/httptest/simulate_result_test.go new file mode 100644 index 00000000..297bdc06 --- /dev/null +++ b/lib/test/httptest/simulate_result_test.go @@ -0,0 +1,122 @@ +package httptest_test + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + + libhttptest "git.sr.ht/~shulhan/pakakeh.go/lib/test/httptest" +) + +func ExampleSimulateResult_DumpRequest() { + var ( + req *http.Request + err error + ) + + req, err = http.NewRequest(http.MethodGet, `/a/b/c`, nil) + if err != nil { + log.Fatal(err) + } + req.Header.Set(`h1`, `v1`) + req.Header.Set(`h2`, `v2`) + req.Header.Set(`h3`, `v3`) + + var sim = libhttptest.SimulateResult{ + Request: req, + } + + var got []byte + + got, err = sim.DumpRequest([]string{`h1`}) + if err != nil { + log.Fatal(err) + } + fmt.Printf("DumpRequest:\n%s", got) + + sim.RequestDump = nil + + got, err = sim.DumpRequest([]string{`h3`}) + if err != nil { + log.Fatal(err) + } + fmt.Printf("DumpRequest:\n%s", got) + + // Output: + // DumpRequest: + // GET /a/b/c HTTP/1.1 + // H2: v2 + // H3: v3 + // + // + // DumpRequest: + // GET /a/b/c HTTP/1.1 + // H1: v1 + // H2: v2 +} + +func ExampleSimulateResult_DumpResponse() { + var handler = func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set(`h1`, `v1`) + w.Header().Set(`h2`, `v2`) + w.Header().Set(`h3`, `v3`) + _, _ = io.WriteString(w, `Hello world!`) + } + + var ( + ctx = context.Background() + + req *http.Request + err error + ) + + req, err = http.NewRequestWithContext(ctx, http.MethodGet, `/a/b/c`, nil) + if err != nil { + log.Fatal(err) + } + + var recorder = httptest.NewRecorder() + + handler(recorder, req) + + var result = libhttptest.SimulateResult{ + Response: recorder.Result(), + } + + var got []byte + + got, err = result.DumpResponse([]string{`h1`}) + if err != nil { + log.Fatal(`DumpResponse #1:`, err) + } + fmt.Printf("<<< DumpResponse:\n%s\n", got) + + result.ResponseDump = nil + + got, err = result.DumpResponse([]string{`h3`}) + if err != nil { + log.Fatal(`DumpResponse #2:`, err) + } + fmt.Printf("<<< DumpResponse:\n%s", got) + + // Output: + // <<< DumpResponse: + // HTTP/1.1 200 OK + // Connection: close + // Content-Type: text/plain; charset=utf-8 + // H2: v2 + // H3: v3 + // + // Hello world! + // <<< DumpResponse: + // HTTP/1.1 200 OK + // Connection: close + // Content-Type: text/plain; charset=utf-8 + // H1: v1 + // H2: v2 + // + // Hello world! +} |
