aboutsummaryrefslogtreecommitdiff
path: root/lib/test
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2024-03-09 05:02:47 +0700
committerShulhan <ms@kilabit.info>2024-03-14 23:51:34 +0700
commit8d9c5ff8958b6adcd01e86f9a48f256df388b9db (patch)
tree57dfb7644b2db6e295ec77698366449b6a5da9f6 /lib/test
parent29343294a596fd74f6f9cb917c8aac74ccb78094 (diff)
downloadpakakeh.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].
Diffstat (limited to 'lib/test')
-rw-r--r--lib/test/httptest/httptest.go59
-rw-r--r--lib/test/httptest/httptest_test.go94
-rw-r--r--lib/test/httptest/simulate_request.go40
-rw-r--r--lib/test/httptest/simulate_result.go85
-rw-r--r--lib/test/httptest/simulate_result_test.go122
5 files changed, 400 insertions, 0 deletions
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!
+}