├── .circleci
└── config.yml
├── .gitignore
├── LICENSE
├── README.md
├── assertions.go
├── assertions_test.go
├── client.go
├── client_test.go
├── fixtures
└── gopher.png
├── go.mod
├── go.sum
├── internal
└── tls.go
├── request.go
├── request_helpers.go
├── request_helpers_test.go
├── request_test.go
├── server_1_8.go
├── server_1_9.go
├── server_test.go
└── transport.go
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Golang CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-go/ for more details
4 | version: 2.1
5 | defaults: &defaults
6 | #### TEMPLATE_NOTE: go expects specific checkout path representing url
7 | #### expecting it in the form of
8 | #### /go/src/github.com/circleci/go-tool
9 | #### /go/src/bitbucket.org/circleci/go-tool
10 | working_directory: github.com/dolab/httptesting
11 | parallelism: 2
12 | default_steps: &default_steps
13 | steps:
14 | - checkout # checkout source code
15 |
16 | # specify any bash command here prefixed with `run: `
17 | - run: git submodule update --init
18 | - run: go test -v -timeout 30s github.com/dolab/httptesting
19 | - run: go test -v -race -timeout 30s github.com/dolab/httptesting
20 | jobs:
21 | go1-24:
22 | <<: *defaults
23 | docker:
24 | # specify the version
25 | - image: cimg/go:1.24
26 |
27 | # Specify service dependencies here if necessary
28 | # CircleCI maintains a library of pre-built images
29 | # documented at https://circleci.com/docs/2.0/circleci-images/
30 | # - image: circleci/postgres:9.4
31 | <<: *default_steps
32 | go1-19:
33 | <<: *defaults
34 | docker:
35 | # specify the version
36 | - image: cimg/go:1.19
37 |
38 | # Specify service dependencies here if necessary
39 | # CircleCI maintains a library of pre-built images
40 | # documented at https://circleci.com/docs/2.0/circleci-images/
41 | # - image: circleci/postgres:9.4
42 | <<: *default_steps
43 |
44 | workflows:
45 | version: 2.1
46 | build_and_test:
47 | jobs:
48 | - go1-24
49 | #- go1-19
50 |
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
26 | # IDE
27 | .idea
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # httptesting
2 |
3 | [](https://circleci.com/gh/dolab/httptesting/tree/master)
4 |
5 | Golang HTTP testing client for human.
6 |
7 | ## Installation
8 |
9 | ```bash
10 | $ go get github.com/dolab/httptesting
11 | ```
12 |
13 | ## Getting Started
14 |
15 | ```go
16 | package httptesting
17 |
18 | import (
19 | "net/http"
20 | "testing"
21 |
22 | "github.com/dolab/httptesting"
23 | )
24 |
25 | // Testing with httptesting.Request
26 | func Test_Request(t *testing.T) {
27 | host := "https://example.com"
28 | client := httptesting.New(host, true)
29 |
30 | request := client.New(t)
31 | request.Get("/")
32 |
33 | // verify http response status
34 | if request.AssertOK() {
35 | // verify http response header
36 | request.AssertExistHeader("Content-Length")
37 |
38 | // verify http response body
39 | request.AssertNotEmpty()
40 | }
41 | }
42 | ```
43 |
44 | ### Connected with `httptest.Server`
45 |
46 | ```go
47 | package httptesting
48 |
49 | import (
50 | "net/http"
51 | "testing"
52 |
53 | "github.com/dolab/httptesting"
54 | )
55 |
56 | type mockServer struct {
57 | method string
58 | path string
59 | assertion func(w http.ResponseWriter, r *http.Request)
60 | }
61 |
62 | func (mock *mockServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
63 | mock.assertion(w, r)
64 | }
65 |
66 | func Test_Server(t *testing.T) {
67 | method := "GET"
68 | uri := "/server/https"
69 | server := &mockServer{
70 | method: method,
71 | path: uri,
72 | assertion: func(w http.ResponseWriter, r *http.Request) {
73 | w.Header().Set("x-request-method", r.Method)
74 |
75 | w.WriteHeader(http.StatusOK)
76 | w.Write([]byte("TLS"))
77 | },
78 | }
79 |
80 | // return default client connected with httptest.Server
81 | ts := httptesting.NewServer(server, true)
82 | defer ts.Close()
83 |
84 | request := ts.New(t)
85 | request.Get("/server/https")
86 |
87 | // verify http response status
88 | if request.AssertOK() {
89 | // verify http response header
90 | request.AssertExistHeader("Content-Length")
91 |
92 | // verify http response body
93 | request.AssertContains("TLS")
94 | }
95 | }
96 | ```
97 |
98 | ### Advantage Usage
99 |
100 | ```go
101 | package main
102 |
103 | import (
104 | "testing"
105 | )
106 |
107 | func Test_Request(t *testing.T) {
108 | host := "https://example.com"
109 | client := httptesting.New(host, true)
110 |
111 | t.Run("GET /api/json", func(t *testing.T) {
112 | request := client.New(t)
113 | request.WithHeader("X-Mock-Client", "httptesting")
114 |
115 | // assume server response with following json data:
116 | // {"user":{"name":"httptesting","age":3},"addresses":[{"name":"china"},{"name":"USA"}]}
117 | request.GetJSON("/api/json", nil)
118 |
119 | // verify http response status
120 | if request.AssertOK() {
121 | // verify http response header
122 | request.AssertHeader("X-Mock-Client", "httptesting")
123 |
124 | // verify http response body with json format
125 | request.AssertContainsJSON("user.name", "httptesting")
126 |
127 | // for array
128 | request.AssertContainsJSON("addresses.1.name", "USA")
129 | request.AssertNotContainsJSON("addresses.2.name")
130 |
131 | // use regexp for custom matcher
132 | request.AssertMatch("user.*")
133 | }
134 | })
135 |
136 | t.Run("POST /api/json", func(t *testing.T) {
137 | request := client.New(t)
138 | request.WithHeader("X-Mock-Client", "httptesting")
139 |
140 | payload := struct {
141 | Name string `json:"name"`
142 | Age int `json:"age"`
143 | }{"httptesting", 3}
144 |
145 | // assume server response with following json data:
146 | // {"data":{"name":"httptesting","age":3},"success":true}
147 | request.PostJSON("/api/json", payload)
148 |
149 | // verify http response status
150 | if request.AssertOK() {
151 | // verify http response header
152 | request.AssertHeader("X-Mock-Client", "httptesting")
153 |
154 | // verify http response body with json format
155 | request.AssertContainsJSON("data.name", "httptesting")
156 | request.AssertContainsJSON("data.age", 3)
157 | request.AssertContainsJSON("success", true)
158 |
159 | // use regexp for custom matcher
160 | request.AssertNotMatch("user.*")
161 | }
162 | })
163 | }
164 | ```
165 |
166 | ## Author
167 |
168 | [Spring MC](https://twitter.com/mcspring)
169 |
170 | ## LICENSE
171 |
172 | ```
173 | The MIT License (MIT)
174 |
175 | Copyright (c) 2016
176 |
177 | Permission is hereby granted, free of charge, to any person obtaining a copy
178 | of this software and associated documentation files (the "Software"), to deal
179 | in the Software without restriction, including without limitation the rights
180 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
181 | copies of the Software, and to permit persons to whom the Software is
182 | furnished to do so, subject to the following conditions:
183 |
184 | The above copyright notice and this permission notice shall be included in all
185 | copies or substantial portions of the Software.
186 |
187 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
188 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
189 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
190 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
191 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
192 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
193 | SOFTWARE.
194 | ```
195 |
--------------------------------------------------------------------------------
/assertions.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/golib/assert"
7 | )
8 |
9 | // AssertStatus asserts that the response status code is equal to value.
10 | func (r *Request) AssertStatus(status int) bool {
11 | return assert.EqualValues(r.t, status, r.Response.StatusCode,
12 | "Expected response status code of %d, but got %d",
13 | status,
14 | r.Response.StatusCode,
15 | )
16 | }
17 |
18 | // AssertOK asserts that the response status code is 200.
19 | func (r *Request) AssertOK() bool {
20 | return r.AssertStatus(http.StatusOK)
21 | }
22 |
23 | // AssertForbidden asserts that the response status code is 403.
24 | func (r *Request) AssertForbidden() bool {
25 | return r.AssertStatus(http.StatusForbidden)
26 | }
27 |
28 | // AssertNotFound asserts that the response status code is 404.
29 | func (r *Request) AssertNotFound() bool {
30 | return r.AssertStatus(http.StatusNotFound)
31 | }
32 |
33 | // AssertInternalError asserts that the response status code is 500.
34 | func (r *Request) AssertInternalError() bool {
35 | return r.AssertStatus(http.StatusInternalServerError)
36 | }
37 |
38 | // AssertHeader asserts that the response includes named header with value.
39 | func (r *Request) AssertHeader(name, value string) bool {
40 | actual := r.Response.Header.Get(name)
41 |
42 | return assert.EqualValues(r.t, value, actual,
43 | "Expected response header contains %s of %s, but got %s",
44 | http.CanonicalHeaderKey(name),
45 | value,
46 | actual,
47 | )
48 | }
49 |
50 | // AssertContentType asserts that the response includes Content-Type header with value.
51 | func (r *Request) AssertContentType(contentType string) bool {
52 | return r.AssertHeader("Content-Type", contentType)
53 | }
54 |
55 | // AssertExistHeader asserts that the response includes named header.
56 | func (r *Request) AssertExistHeader(name string) bool {
57 | name = http.CanonicalHeaderKey(name)
58 |
59 | _, ok := r.Response.Header[name]
60 | if !ok {
61 | assert.Fail(r.t, "Response header: "+name+" (*required)",
62 | "Expected response header includes %s",
63 | name,
64 | )
65 | }
66 |
67 | return ok
68 | }
69 |
70 | // AssertNotExistHeader asserts that the response does not include named header.
71 | func (r *Request) AssertNotExistHeader(name string) bool {
72 | name = http.CanonicalHeaderKey(name)
73 |
74 | _, ok := r.Response.Header[name]
75 | if ok {
76 | assert.Fail(r.t, "Response header: "+name+" (*not required)",
77 | "Expected response header does not include %s",
78 | name,
79 | )
80 | }
81 |
82 | return !ok
83 | }
84 |
85 | // AssertEmpty asserts that the response body is empty.
86 | func (r *Request) AssertEmpty() bool {
87 | return assert.Empty(r.t, string(r.ResponseBody))
88 | }
89 |
90 | // AssertNotEmpty asserts that the response body is not empty.
91 | func (r *Request) AssertNotEmpty() bool {
92 | return assert.NotEmpty(r.t, string(r.ResponseBody))
93 | }
94 |
95 | // AssertContains asserts that the response body contains the string.
96 | func (r *Request) AssertContains(s string) bool {
97 | return assert.Contains(r.t, string(r.ResponseBody), s,
98 | "Expected response body contains %q",
99 | s,
100 | )
101 | }
102 |
103 | // AssertNotContains asserts that the response body does not contain the string.
104 | func (r *Request) AssertNotContains(s string) bool {
105 | return assert.NotContains(r.t, string(r.ResponseBody), s,
106 | "Expected response body does not contain %q",
107 | s,
108 | )
109 | }
110 |
111 | // AssertMatch asserts that the response body matches the regular expression.
112 | func (r *Request) AssertMatch(re string) bool {
113 | return assert.Match(r.t, re, r.ResponseBody,
114 | "Expected response body matches regexp %q",
115 | re,
116 | )
117 | }
118 |
119 | // AssertNotMatch asserts that the response body does not match the regular expression.
120 | func (r *Request) AssertNotMatch(re string) bool {
121 | return assert.NotMatch(r.t, re, r.ResponseBody,
122 | "Expected response body does not match regexp %q",
123 | re,
124 | )
125 | }
126 |
127 | // AssertContainsJSON asserts that the response body contains JSON value of the key.
128 | func (r *Request) AssertContainsJSON(key string, value interface{}) bool {
129 | return assert.ContainsJSON(r.t, string(r.ResponseBody), key, value)
130 | }
131 |
132 | // AssertNotContainsJSON asserts that the response body dose not contain JSON value of the key.
133 | func (r *Request) AssertNotContainsJSON(key string) bool {
134 | return assert.NotContainsJSON(r.t, string(r.ResponseBody), key)
135 | }
136 |
--------------------------------------------------------------------------------
/assertions_test.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "net/url"
7 | "testing"
8 |
9 | "github.com/golib/assert"
10 | )
11 |
12 | func TestRequest_Assertions(t *testing.T) {
13 | it := assert.New(t)
14 | method := "GET"
15 | uri := "/assertions"
16 | params := url.Values{"url-key": []string{"url-value"}}
17 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
18 | it.Equal(method, r.Method)
19 | it.Equal("text/html", r.Header.Get("Content-Type"))
20 | it.Equal("/assertions?url-key=url-value", r.RequestURI)
21 | it.Equal("url-value", r.URL.Query().Get("url-key"))
22 |
23 | w.Header().Set("x-request-method", r.Method)
24 | w.Header().Set("Content-Type", "application/json")
25 | w.WriteHeader(http.StatusOK)
26 | w.Write([]byte(`{"user":{"name":"httptesting","age":3},"addresses":[{"name":"china"},{"name":"USA"}]}`))
27 | })
28 |
29 | ts := httptest.NewServer(server)
30 | defer ts.Close()
31 |
32 | request := New(ts.URL, false).New(t)
33 | request.Get(uri, params)
34 | request.AssertOK()
35 | request.AssertHeader("x-request-method", method)
36 | request.AssertExistHeader("Content-Type")
37 | request.AssertNotExistHeader("x-unknown-header")
38 | request.AssertNotEmpty()
39 | request.AssertContains(`{"name":"china"}`)
40 | request.AssertContainsJSON("user.name", "httptesting")
41 | request.AssertContainsJSON("addresses.1.name", "USA")
42 | request.AssertNotContainsJSON("addresses.0.post")
43 | request.AssertNotContainsJSON("addresses.3.name")
44 | }
45 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "crypto/x509"
5 | "fmt"
6 | "net/http"
7 | "net/http/cookiejar"
8 | "net/http/httptest"
9 | "net/url"
10 | "strings"
11 | "sync"
12 | "testing"
13 |
14 | "golang.org/x/net/websocket"
15 | )
16 |
17 | // Client defines request component of httptesting.
18 | //
19 | // NOTE: Client is not safe for concurrency, please use client.New(t) after initialized.
20 | type Client struct {
21 | mux sync.RWMutex
22 | server *httptest.Server
23 | host string
24 | certs *x509.CertPool
25 | jar *cookiejar.Jar
26 | isTLS bool
27 | }
28 |
29 | // New returns an initialized *Client ready for testing
30 | func New(host string, isTLS bool) *Client {
31 | // adjust host
32 | if strings.HasPrefix(host, "http://") || strings.HasPrefix(host, "https://") {
33 | urlobj, err := url.Parse(host)
34 | if err == nil {
35 | isTLS = strings.HasPrefix(host, "https://")
36 |
37 | host = urlobj.Host
38 | }
39 | }
40 |
41 | jar, err := cookiejar.New(nil)
42 | if err != nil {
43 | panic(fmt.Sprintf("httptesting: New: %v", err))
44 | }
45 |
46 | return &Client{
47 | host: host,
48 | jar: jar,
49 | isTLS: isTLS,
50 | }
51 | }
52 |
53 | // NewWithTLS returns an initialized *Client with custom certificate.
54 | func NewWithTLS(host string, cert *x509.Certificate) *Client {
55 | // adjust host
56 | if strings.HasPrefix(host, "https://") {
57 | urlobj, err := url.Parse(host)
58 | if err == nil {
59 | host = urlobj.Host
60 | }
61 | }
62 |
63 | jar, err := cookiejar.New(nil)
64 | if err != nil {
65 | panic(fmt.Sprintf("httptesting: NewWithTLS: %v", err))
66 | }
67 |
68 | certs := x509.NewCertPool()
69 | certs.AddCert(cert)
70 |
71 | return &Client{
72 | host: host,
73 | certs: certs,
74 | jar: jar,
75 | isTLS: true,
76 | }
77 | }
78 |
79 | // Host returns the host and port of the server, e.g. "127.0.0.1:9090"
80 | func (c *Client) Host() string {
81 | if len(c.host) == 0 {
82 | return ""
83 | }
84 |
85 | if c.host[0] == ':' {
86 | return "127.0.0.1" + c.host
87 | }
88 |
89 | return c.host
90 | }
91 |
92 | // Url returns the abs http/isTLS URL of the resource, e.g. "http://127.0.0.1:9090/status".
93 | // The scheme is set to isTLS if http.ssl is set to true in the configuration.
94 | func (c *Client) Url(urlpath string, params ...url.Values) string {
95 | if len(params) > 0 {
96 | if !strings.Contains(urlpath, "?") {
97 | urlpath += "?"
98 | }
99 |
100 | urlpath += params[0].Encode()
101 | }
102 |
103 | scheme := "http://"
104 | if c.isTLS {
105 | scheme = "https://"
106 | }
107 |
108 | return scheme + c.Host() + urlpath
109 | }
110 |
111 | // WebsocketUrl returns the abs websocket URL of the resource, e.g. "ws://127.0.0.1:9090/status"
112 | func (c *Client) WebsocketUrl(urlpath string, params ...url.Values) string {
113 | if len(params) > 0 {
114 | if !strings.Contains(urlpath, "?") {
115 | urlpath += "?"
116 | }
117 |
118 | urlpath += params[0].Encode()
119 | }
120 |
121 | return "ws://" + c.Host() + urlpath
122 | }
123 |
124 | // Cookies returns jar related to the host
125 | func (c *Client) Cookies() ([]*http.Cookie, error) {
126 | urlobj, err := url.Parse(c.Url("/"))
127 | if err != nil {
128 | return nil, err
129 | }
130 |
131 | return c.jar.Cookies(urlobj), nil
132 | }
133 |
134 | // SetCookies sets jar for the host
135 | func (c *Client) SetCookies(cookies []*http.Cookie) error {
136 | c.mux.Lock()
137 | defer c.mux.Unlock()
138 |
139 | urlobj, err := url.Parse(c.Url("/"))
140 | if err != nil {
141 | return err
142 | }
143 |
144 | c.jar.SetCookies(urlobj, cookies)
145 | return nil
146 | }
147 |
148 | // NewClient creates a http client with cookie and tls for the Client.
149 | func (c *Client) NewClient(filters ...RequestFilter) *http.Client {
150 | client := &http.Client{
151 | Transport: NewFilterTransport(filters, c.certs),
152 | Jar: c.jar,
153 | }
154 |
155 | return client
156 | }
157 |
158 | // NewWebsocket creates a websocket connection to the given path and returns the connection
159 | func (c *Client) NewWebsocket(t *testing.T, path string) *websocket.Conn {
160 | origin := c.WebsocketUrl("/")
161 | target := c.WebsocketUrl(path)
162 |
163 | ws, err := websocket.Dial(target, "", origin)
164 | if err != nil {
165 | t.Fatalf("httptesting: NewWebscoket: connect %s with %v\n", path, err)
166 | }
167 |
168 | return ws
169 | }
170 |
171 | // NewRequest returns a *Request which has more customization!
172 | func (c *Client) NewRequest(t *testing.T) *Request {
173 | return NewRequest(t, c)
174 | }
175 |
176 | // New is alias of NewRequest for shortcut.
177 | func (c *Client) New(t *testing.T) *Request {
178 | return c.NewRequest(t)
179 | }
180 |
181 | // Close tries to
182 | //
183 | // - close *httptest.Server created by NewServer or NewServerWithTLS
184 | func (c *Client) Close() {
185 | c.mux.Lock()
186 | defer c.mux.Unlock()
187 |
188 | if c.server != nil {
189 | c.server.Close()
190 | c.server = nil
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/client_test.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "net/url"
7 | "strconv"
8 | "sync"
9 | "testing"
10 |
11 | "github.com/golib/assert"
12 | )
13 |
14 | type mockServer struct {
15 | method string
16 | path string
17 | it func(w http.ResponseWriter, r *http.Request)
18 | }
19 |
20 | func (mock *mockServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
21 | mock.it(w, r)
22 | }
23 |
24 | var (
25 | newMockServer = func(method, path string, it func(http.ResponseWriter, *http.Request)) *mockServer {
26 | return &mockServer{
27 | method: method,
28 | path: path,
29 | it: it,
30 | }
31 | }
32 | )
33 |
34 | func Test_New(t *testing.T) {
35 | it := assert.New(t)
36 |
37 | host := "www.example.com"
38 | absurl := "https://" + host
39 | ws := "ws://" + host
40 |
41 | client := New(host, true)
42 | it.Nil(client.server)
43 | it.NotEmpty(client.host)
44 | it.True(client.isTLS)
45 | it.Equal(host, client.Host())
46 | it.Equal(absurl, client.Url(""))
47 | it.Equal(ws, client.WebsocketUrl(""))
48 | }
49 |
50 | func Test_NewWithRacy(t *testing.T) {
51 | method := "GET"
52 | uri := "/request/racy"
53 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
54 | w.Header().Set("x-request-method", r.Method)
55 |
56 | w.WriteHeader(http.StatusOK)
57 | w.Write([]byte(r.URL.Query().Get("routine")))
58 | })
59 |
60 | ts := httptest.NewServer(server)
61 | defer ts.Close()
62 |
63 | client := New(ts.URL, false)
64 |
65 | var (
66 | wg sync.WaitGroup
67 |
68 | routines = 3
69 | )
70 | wg.Add(routines)
71 |
72 | for i := 0; i < routines; i++ {
73 | go func(routine int) {
74 | defer wg.Done()
75 |
76 | params := url.Values{}
77 | params.Add("routine", strconv.Itoa(routine+1))
78 |
79 | request := client.New(t)
80 | request.Get(uri, params)
81 | request.AssertOK()
82 | request.AssertContains(strconv.Itoa(routine + 1))
83 | }(i)
84 | }
85 |
86 | wg.Wait()
87 | }
88 |
89 | func TestTesting_New(t *testing.T) {
90 | it := assert.New(t)
91 |
92 | host := "www.example.com"
93 | client := New(host, true)
94 |
95 | request := client.New(t)
96 | it.Equal(client, request.Client)
97 | it.NotNil(request.t)
98 | it.Nil(request.Response)
99 | it.Empty(request.ResponseBody)
100 | it.Nil(request.server)
101 | it.Equal(client.host, request.host)
102 | it.True(request.isTLS)
103 | }
104 |
--------------------------------------------------------------------------------
/fixtures/gopher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolab/httptesting/56e91583b4d7989a8c3bf9be5b377feb6643e5fa/fixtures/gopher.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dolab/httptesting
2 |
3 | go 1.24.1
4 |
5 | require (
6 | github.com/golib/assert v1.7.0
7 | golang.org/x/net v0.37.0
8 | )
9 |
10 | require (
11 | github.com/buger/jsonparser v1.1.1 // indirect
12 | github.com/dolab/types v1.0.0 // indirect
13 | github.com/kr/pretty v0.3.1 // indirect
14 | github.com/kr/text v0.2.0 // indirect
15 | github.com/pmezard/go-difflib v1.0.0 // indirect
16 | github.com/rogpeppe/go-internal v1.14.1 // indirect
17 | )
18 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
2 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
4 | github.com/dolab/types v1.0.0 h1:6Mw2F+OV2O4nbtJuUkJz4SvM680BmnN98+tAKwaS5NM=
5 | github.com/dolab/types v1.0.0/go.mod h1:brm0PbBgIaJU2Bp6XOPSIJ/TeupivTbBK9HQasNfuGc=
6 | github.com/golib/assert v1.7.0 h1:rsJw1nyBS77foXXyOEnTApAn54Wp9s4AEl1cpMdcoEs=
7 | github.com/golib/assert v1.7.0/go.mod h1:NSgHxVL0GnUDmBmb39ed91474lOLh7be5qCXMuIiHow=
8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
12 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
16 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
17 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
18 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
19 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
20 |
--------------------------------------------------------------------------------
/internal/tls.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | var (
4 | // LocalhostCert is a PEM-encoded TLS cert with SAN IPs
5 | // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT.
6 | // generated from src/crypto/tls:
7 | // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
8 | LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
9 | MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS
10 | MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
11 | MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
12 | iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4
13 | iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul
14 | rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO
15 | BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw
16 | AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA
17 | AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9
18 | tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs
19 | h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM
20 | fblo6RBxUQ==
21 | -----END CERTIFICATE-----`)
22 |
23 | // LocalhostKey is the private key for localhostCert.
24 | LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
25 | MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9
26 | SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB
27 | l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB
28 | AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet
29 | 3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb
30 | uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H
31 | qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp
32 | jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY
33 | fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U
34 | fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU
35 | y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX
36 | qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo
37 | f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA==
38 | -----END RSA PRIVATE KEY-----`)
39 | )
40 |
--------------------------------------------------------------------------------
/request.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "mime/multipart"
7 | "net/http"
8 | "os"
9 | "sync"
10 | "testing"
11 | )
12 |
13 | // Request defines http client for human usage.
14 | type Request struct {
15 | *Client
16 |
17 | Response *http.Response
18 | ResponseBody []byte
19 |
20 | mux sync.Mutex
21 | t *testing.T
22 | cookies []*http.Cookie
23 | header http.Header
24 | }
25 |
26 | // NewRequest returns a new *Request with *Client
27 | func NewRequest(t *testing.T, client *Client) *Request {
28 | return &Request{
29 | Client: client,
30 | t: t,
31 | cookies: []*http.Cookie{},
32 | header: http.Header{},
33 | }
34 | }
35 |
36 | // WithHeader sets http header by replace for the request
37 | func (r *Request) WithHeader(key, value string) *Request {
38 | r.mux.Lock()
39 | defer r.mux.Unlock()
40 |
41 | r.header.Set(key, value)
42 |
43 | return r
44 | }
45 |
46 | // WithHttpHeader adds http header for the request
47 | func (r *Request) WithHttpHeader(header http.Header) *Request {
48 | r.mux.Lock()
49 | defer r.mux.Unlock()
50 |
51 | for key, values := range header {
52 | for _, value := range values {
53 | r.header.Add(key, value)
54 | }
55 | }
56 |
57 | return r
58 | }
59 |
60 | // WithCookies sets jar for client by replace for the request
61 | func (r *Request) WithCookies(cookies []*http.Cookie) *Request {
62 | r.mux.Lock()
63 | defer r.mux.Unlock()
64 |
65 | r.cookies = append(r.cookies, cookies...)
66 |
67 | return r
68 | }
69 |
70 | // NewRequest issues any request and read the response.
71 | // If successful, the caller may examine the Response and ResponseBody properties.
72 | // NOTE: You have to manage session / cookie data manually.
73 | func (r *Request) NewRequest(request *http.Request, filters ...RequestFilter) {
74 | r.mux.Lock()
75 | defer r.mux.Unlock()
76 |
77 | var err error
78 |
79 | r.Response, err = r.NewClient(filters...).Do(request)
80 | if err != nil {
81 | r.t.Fatalf("httptesting: %v\n", err)
82 | }
83 | defer r.Response.Body.Close()
84 |
85 | // Read response body if not empty
86 | r.ResponseBody = []byte{}
87 |
88 | switch r.Response.StatusCode {
89 | case http.StatusNoContent:
90 | // ignore
91 |
92 | default:
93 | r.ResponseBody, err = io.ReadAll(r.Response.Body)
94 | if err != nil {
95 | if err != io.EOF {
96 | r.t.Fatalf("httptesting: NewRequest:%s %s: %v\n", request.Method, request.URL.RequestURI(), err)
97 | }
98 |
99 | r.t.Logf("httptesting: NewRequest:%s %s: Unexptected response body with io.EOF\n", request.Method, request.URL.RequestURI())
100 | }
101 | }
102 | }
103 |
104 | // NewSessionRequest issues any request with session / cookie and read the response.
105 | // If successful, the caller may examine the Response and ResponseBody properties.
106 | // NOTE: Session data will be added to the request jar for requested host.
107 | func (r *Request) NewSessionRequest(request *http.Request, filters ...RequestFilter) {
108 | if cookies, err := r.Cookies(); err == nil {
109 | for _, cookie := range cookies {
110 | request.AddCookie(cookie)
111 | }
112 | }
113 |
114 | for _, cookie := range r.cookies {
115 | request.AddCookie(cookie)
116 | }
117 |
118 | r.NewRequest(request, filters...)
119 | }
120 |
121 | // NewMultipartRequest issues a multipart request for the method & fields given and read the response.
122 | // If successful, the caller may examine the Response and ResponseBody properties.
123 | func (r *Request) NewMultipartRequest(method, path, filename string, file interface{}, fields ...map[string]string) {
124 | var buf bytes.Buffer
125 |
126 | mw := multipart.NewWriter(&buf)
127 |
128 | fw, ferr := mw.CreateFormFile("filename", filename)
129 | if ferr != nil {
130 | r.t.Fatalf("httptesting: NewMultipartRequest:%s %s: %v\n", method, path, ferr)
131 | }
132 |
133 | // apply file
134 | var (
135 | reader io.Reader
136 | err error
137 | )
138 | switch f := file.(type) {
139 | case io.Reader:
140 | reader = f
141 |
142 | case string:
143 | reader, err = os.Open(f)
144 | if err != nil {
145 | r.t.Fatalf("httptesting: NewMultipartRequest:os.Open(%s): %v\n", f, err)
146 | }
147 |
148 | default:
149 | r.t.Fatalf("httptesting: NewMultipartRequest:%T<%v>: Unsupported file type\n", file, file)
150 | }
151 |
152 | if _, err := io.Copy(fw, reader); err != nil {
153 | r.t.Fatalf("httptesting: NewMultipartRequest:io.Copy(%T, %T): %v\n", fw, file, err)
154 | }
155 |
156 | // apply fields
157 | if len(fields) > 0 {
158 | for key, value := range fields[0] {
159 | mw.WriteField(key, value)
160 | }
161 | }
162 |
163 | // adds the terminating boundary
164 | mw.Close()
165 |
166 | request, err := http.NewRequest(method, r.Url(path), &buf)
167 | if err != nil {
168 | r.t.Fatalf("httptesting: NewMultipartRequest:%s %s: %v\n", method, path, err)
169 | }
170 | request.Header.Set("Content-Type", mw.FormDataContentType())
171 |
172 | r.NewRequest(request)
173 | }
174 |
--------------------------------------------------------------------------------
/request_helpers.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "encoding/xml"
7 | "io"
8 | "net/http"
9 | "net/url"
10 | "strconv"
11 | )
12 |
13 | // Get issues a GET request to the given path with Content-Type: text/html header, and
14 | // stores the result in Response and ResponseBody if success.
15 | func (r *Request) Get(path string, params ...url.Values) {
16 | contentType := "text/html"
17 |
18 | if len(params) == 0 {
19 | r.Send("GET", path, contentType)
20 | } else {
21 | r.Send("GET", path, contentType, params[0])
22 | }
23 | }
24 |
25 | // GetJSON issues a GET request to the given path with Content-Type: application/json header, and
26 | // stores the result in Response and ResponseBody if success.
27 | func (r *Request) GetJSON(path string, params ...url.Values) {
28 | contentType := "application/json"
29 |
30 | if len(params) == 0 {
31 | r.Send("GET", path, contentType)
32 | } else {
33 | r.Send("GET", path, contentType, params[0])
34 | }
35 | }
36 |
37 | // GetXML issues a GET request to the given path with Content-Type: text/xml header, and
38 | // stores the result in Response and ResponseBody if success.
39 | func (r *Request) GetXML(path string, params ...url.Values) {
40 | contentType := "text/xml"
41 |
42 | if len(params) == 0 {
43 | r.Send("GET", path, contentType)
44 | } else {
45 | r.Send("GET", path, contentType, params[0])
46 | }
47 | }
48 |
49 | // Head issues a HEAD request to the given path with Content-Type: text/html header, and
50 | // stores the result in Response if success.
51 | func (r *Request) Head(path string, params ...url.Values) {
52 | contentType := "text/html"
53 |
54 | if len(params) == 0 {
55 | r.Send("HEAD", path, contentType)
56 | } else {
57 | r.Send("HEAD", path, contentType, params[0])
58 | }
59 | }
60 |
61 | // Options issues an OPTIONS request to the given path Content-Type: text/html header, and
62 | // stores the result in Response if success.
63 | func (r *Request) Options(path string, params ...url.Values) {
64 | contentType := "text/html"
65 |
66 | if len(params) == 0 {
67 | r.Send("OPTIONS", path, contentType)
68 | } else {
69 | r.Send("OPTIONS", path, contentType, params[0])
70 | }
71 | }
72 |
73 | // Put issues a PUT request to the given path with specified Content-Type header, and
74 | // stores the result in Response and ResponseBody if success.
75 | func (r *Request) Put(path, contentType string, data ...interface{}) {
76 | r.Send("PUT", path, contentType, data...)
77 | }
78 |
79 | // PutForm issues a PUT request to the given path with Content-Type: application/x-www-form-urlencoded header, and
80 | // stores the result in Response and ResponseBody if success.
81 | func (r *Request) PutForm(path string, data interface{}) {
82 | r.Put(path, "application/x-www-form-urlencoded", data)
83 | }
84 |
85 | // PutJSON issues a PUT request to the given path with Content-Type: application/json header, and
86 | // stores the result in Response and ResponseBody if success.
87 | // NOTE: It will encode data by json.Marshal before making request.
88 | func (r *Request) PutJSON(path string, data interface{}) {
89 | b, err := json.Marshal(data)
90 | if err != nil {
91 | r.t.Fatalf("httptesting: PutJSON:json.Marshal(%T): %v", data, err)
92 | }
93 |
94 | r.Put(path, "application/json", b)
95 | }
96 |
97 | // PutXML issues a PUT request to the given path with Content-Type: text/xml header, and
98 | // stores the result in Response and ResponseBody if success.
99 | // NOTE: It will encode data by xml.Marshal before making request.
100 | func (r *Request) PutXML(path string, data interface{}) {
101 | b, err := xml.Marshal(data)
102 | if err != nil {
103 | r.t.Fatalf("httptesting: PutXML:xml.Marshal(%T): %v", data, err)
104 | }
105 |
106 | r.Put(path, "text/xml", b)
107 | }
108 |
109 | // Post issues a POST request to the given path with specified Content-Type header, and
110 | // stores the result in Response and ResponseBody if success.
111 | func (r *Request) Post(path, contentType string, data ...interface{}) {
112 | r.Send("POST", path, contentType, data...)
113 | }
114 |
115 | // PostForm issues a POST request to the given path with Content-Type: application/x-www-form-urlencoded header, and
116 | // stores the result in Response and ResponseBody if success.
117 | func (r *Request) PostForm(path string, data interface{}) {
118 | r.Post(path, "application/x-www-form-urlencoded", data)
119 | }
120 |
121 | // PostJSON issues a POST request to the given path with Content-Type: application/json header, and
122 | // stores the result in Response and ResponseBody if success.
123 | // NOTE: It will encode data by json.Marshal before making request.
124 | func (r *Request) PostJSON(path string, data interface{}) {
125 | b, err := json.Marshal(data)
126 | if err != nil {
127 | r.t.Fatalf("httptesting: PostJSON:json.Marshal(%T): %v", data, err)
128 | }
129 |
130 | r.Post(path, "application/json", b)
131 | }
132 |
133 | // PostXML issues a POST request to the given path with Content-Type: text/xml header, and
134 | // stores the result in Response and ResponseBody if success.
135 | // NOTE: It will encode data by xml.Marshal before making request.
136 | func (r *Request) PostXML(path string, data interface{}) {
137 | b, err := xml.Marshal(data)
138 | if err != nil {
139 | r.t.Fatalf("httptesting: PostXML:xml.Marshal(%T): %v", data, err)
140 | }
141 |
142 | r.Post(path, "text/xml", b)
143 | }
144 |
145 | // Patch issues a PATCH request to the given path with specified Content-Type header, and
146 | // stores the result in Response and ResponseBody if success.
147 | func (r *Request) Patch(path, contentType string, data ...interface{}) {
148 | r.Send("PATCH", path, contentType, data...)
149 | }
150 |
151 | // PatchForm issues a PATCH request to the given path with Content-Type: application/x-www-form-urlencoded header, and
152 | // stores the result in Response and ResponseBody if success.
153 | func (r *Request) PatchForm(path string, data interface{}) {
154 | r.Patch(path, "application/x-www-form-urlencoded", data)
155 | }
156 |
157 | // PatchJSON issues a PATCH request to the given path with with Content-Type: application/json header, and
158 | // stores the result in Response and ResponseBody if success.
159 | // It will encode data by json.Marshal before making request.
160 | func (r *Request) PatchJSON(path string, data interface{}) {
161 | b, err := json.Marshal(data)
162 | if err != nil {
163 | r.t.Fatalf("httptesting: PatchJSON:json.Marshal(%T): %v", data, err)
164 | }
165 |
166 | r.Patch(path, "application/json", b)
167 | }
168 |
169 | // PatchXML issues a PATCH request to the given path with Content-Type: text/xml header, and
170 | // stores the result in Response and ResponseBody if success.
171 | // NOTE: It will encode data by xml.Marshal before making request.
172 | func (r *Request) PatchXML(path string, data interface{}) {
173 | b, err := xml.Marshal(data)
174 | if err != nil {
175 | r.t.Fatalf("httptesting: PatchXML:xml.Marshal(%T): %v", data, err)
176 | }
177 |
178 | r.Patch(path, "text/xml", b)
179 | }
180 |
181 | // Delete issues a DELETE request to the given path, sending request with specified Content-Type header, and
182 | // stores the result in Response and ResponseBody if success.
183 | func (r *Request) Delete(path, contentType string, data ...interface{}) {
184 | r.Send("DELETE", path, contentType, data...)
185 | }
186 |
187 | // DeleteForm issues a DELETE request to the given path with Content-Type: application/x-www-form-urlencoded header, and
188 | // stores the result in Response and ResponseBody if success.
189 | func (r *Request) DeleteForm(path string, data interface{}) {
190 | r.Delete(path, "application/x-www-form-urlencoded", data)
191 | }
192 |
193 | // DeleteJSON issues a DELETE request to the given path with Content-Type: application/json header, and
194 | // stores the result in Response and ResponseBody if success.
195 | // NOTE: It will encode data by json.Marshal before making request.
196 | func (r *Request) DeleteJSON(path string, data interface{}) {
197 | b, err := json.Marshal(data)
198 | if err != nil {
199 | r.t.Fatalf("httptesting: DeleteJSON:json.Marshal(%T): %v", data, err)
200 | }
201 |
202 | r.Delete(path, "application/json", b)
203 | }
204 |
205 | // DeleteXML issues a DELETE request to the given path with Content-Type: text/xml header, and
206 | // stores the result in Response and ResponseBody if success.
207 | // NOTE: It will encode data by xml.Marshal before making request.
208 | func (r *Request) DeleteXML(path string, data interface{}) {
209 | b, err := xml.Marshal(data)
210 | if err != nil {
211 | r.t.Fatalf("httptesting: DeleteXML:xml.Marshal(%T): %v", data, err)
212 | }
213 |
214 | r.Delete(path, "text/xml", b)
215 | }
216 |
217 | // Send issues a HTTP request to the given path with specified method and content type header, and
218 | // stores the result in Response and ResponseBody if success.
219 | // NOTE: It will encode data with json.Marshal for unsupported types and reset content type to application/json for the request.
220 | func (r *Request) Send(method, path, contentType string, data ...interface{}) {
221 | request, err := r.Build(method, path, contentType, data...)
222 | if err != nil {
223 | r.t.Fatalf("httptesting: Send:%s %s: %v\n", method, path, err)
224 | }
225 |
226 | // adjust custom headers
227 | for key, values := range r.header {
228 | // ignore Content-Type and Content-Length headers
229 | switch http.CanonicalHeaderKey(key) {
230 | case "Content-Type", "Content-Length":
231 | // ignore
232 |
233 | default:
234 | for _, value := range values {
235 | request.Header.Add(key, value)
236 | }
237 | }
238 | }
239 |
240 | r.NewSessionRequest(request)
241 | }
242 |
243 | func (r *Request) Build(method, urlpath, contentType string, data ...interface{}) (request *http.Request, err error) {
244 | absurl := r.Url(urlpath)
245 |
246 | var (
247 | buf *bytes.Buffer
248 | )
249 |
250 | if len(data) == 0 {
251 | buf = bytes.NewBuffer(nil)
252 |
253 | request, err = http.NewRequest(method, absurl, nil)
254 | } else {
255 | body := data[0]
256 |
257 | switch typo := body.(type) {
258 | case io.Reader:
259 | buf = bytes.NewBuffer(nil)
260 | io.Copy(buf, typo)
261 |
262 | case string:
263 | buf = bytes.NewBufferString(typo)
264 |
265 | case *string:
266 | buf = bytes.NewBufferString(*typo)
267 |
268 | case []byte:
269 | buf = bytes.NewBuffer(typo)
270 |
271 | case *[]byte:
272 | buf = bytes.NewBuffer(*typo)
273 |
274 | case url.Values:
275 | buf = bytes.NewBufferString(typo.Encode())
276 |
277 | default:
278 | b, _ := json.Marshal(body)
279 |
280 | buf = bytes.NewBuffer(b)
281 | contentType = "application/json"
282 | }
283 |
284 | switch method {
285 | case "GET", "HEAD", "OPTIONS": // apply request params to url
286 | if buf.Len() > 0 {
287 | absurl += "?" + buf.String()
288 |
289 | // clean
290 | buf.Reset()
291 | }
292 |
293 | request, err = http.NewRequest(method, absurl, nil)
294 |
295 | default:
296 | request, err = http.NewRequest(method, absurl, buf)
297 | }
298 | }
299 |
300 | if err != nil {
301 | return
302 | }
303 |
304 | // hijack request headers
305 | request.Header.Set("Content-Type", contentType)
306 | request.Header.Set("Content-Length", strconv.FormatInt(int64(buf.Len()), 10))
307 | request.ContentLength = int64(buf.Len())
308 | return
309 | }
310 |
--------------------------------------------------------------------------------
/request_helpers_test.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "encoding/xml"
5 | "io"
6 | "net/http"
7 | "net/http/httptest"
8 | "net/url"
9 | "testing"
10 |
11 | "github.com/golib/assert"
12 | )
13 |
14 | func TestRequest_Get(t *testing.T) {
15 | it := assert.New(t)
16 | method := "GET"
17 | uri := "/get"
18 | params := url.Values{"url-key": []string{"url-value"}}
19 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
20 | it.Equal(method, r.Method)
21 | it.Equal("text/html", r.Header.Get("Content-Type"))
22 | it.Equal("/get?url-key=url-value", r.RequestURI)
23 | it.Equal("url-value", r.URL.Query().Get("url-key"))
24 |
25 | w.Header().Set("x-request-method", r.Method)
26 | w.WriteHeader(http.StatusOK)
27 | w.Write([]byte(method + " " + uri + " OK!"))
28 | })
29 |
30 | ts := httptest.NewServer(server)
31 | defer ts.Close()
32 |
33 | request := New(ts.URL, false).New(t)
34 | request.Get(uri, params)
35 | request.AssertOK()
36 | request.AssertHeader("x-request-method", method)
37 | request.AssertContains("GET /get OK!")
38 | }
39 |
40 | func TestRequest_GetJSON(t *testing.T) {
41 | it := assert.New(t)
42 | method := "GET"
43 | uri := "/get"
44 | params := url.Values{"url-key": []string{"url-value"}}
45 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
46 | it.Equal(method, r.Method)
47 | it.Equal("application/json", r.Header.Get("Content-Type"))
48 | it.Equal("/get?url-key=url-value", r.RequestURI)
49 | it.Equal("url-value", r.URL.Query().Get("url-key"))
50 |
51 | w.Header().Set("x-request-method", r.Method)
52 | w.WriteHeader(http.StatusOK)
53 | w.Write([]byte(method + " " + uri + " OK!"))
54 | })
55 |
56 | ts := httptest.NewServer(server)
57 | defer ts.Close()
58 |
59 | request := New(ts.URL, false).New(t)
60 | request.GetJSON(uri, params)
61 | request.AssertOK()
62 | request.AssertHeader("x-request-method", method)
63 | request.AssertContains("GET /get OK!")
64 | }
65 |
66 | func TestRequest_GetXML(t *testing.T) {
67 | it := assert.New(t)
68 | method := "GET"
69 | uri := "/get"
70 | params := url.Values{"url-key": []string{"url-value"}}
71 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
72 | it.Equal(method, r.Method)
73 | it.Equal("text/xml", r.Header.Get("Content-Type"))
74 | it.Equal("/get?url-key=url-value", r.RequestURI)
75 | it.Equal("url-value", r.URL.Query().Get("url-key"))
76 |
77 | w.Header().Set("x-request-method", r.Method)
78 | w.WriteHeader(http.StatusOK)
79 | w.Write([]byte(method + " " + uri + " OK!"))
80 | })
81 |
82 | ts := httptest.NewServer(server)
83 | defer ts.Close()
84 |
85 | request := New(ts.URL, false).New(t)
86 | request.GetXML(uri, params)
87 | request.AssertOK()
88 | request.AssertHeader("x-request-method", method)
89 | request.AssertContains("GET /get OK!")
90 | }
91 |
92 | func TestRequest_Head(t *testing.T) {
93 | it := assert.New(t)
94 | method := "HEAD"
95 | uri := "/head?key"
96 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
97 | it.Equal(method, r.Method)
98 | it.Equal(uri, r.RequestURI)
99 |
100 | w.Header().Set("x-request-method", r.Method)
101 | w.WriteHeader(http.StatusOK)
102 | w.Write([]byte(method + " " + uri + " OK!"))
103 | })
104 |
105 | ts := httptest.NewServer(server)
106 | defer ts.Close()
107 |
108 | request := New(ts.URL, false).New(t)
109 | request.Head(uri)
110 | request.AssertOK()
111 | request.AssertHeader("x-request-method", method)
112 | request.AssertEmpty()
113 | }
114 |
115 | func TestRequest_Options(t *testing.T) {
116 | it := assert.New(t)
117 | method := "OPTIONS"
118 | uri := "/options?key"
119 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
120 | it.Equal(method, r.Method)
121 | it.Equal(uri, r.RequestURI)
122 |
123 | w.Header().Set("x-request-method", r.Method)
124 | w.WriteHeader(http.StatusOK)
125 | w.Write([]byte(method + " " + uri + " OK!"))
126 | })
127 |
128 | ts := httptest.NewServer(server)
129 | defer ts.Close()
130 |
131 | request := New(ts.URL, false).New(t)
132 | request.Options(uri)
133 | request.AssertOK()
134 | request.AssertHeader("x-request-method", method)
135 | request.AssertContains("OPTIONS /options?key OK!")
136 | }
137 |
138 | func TestRequest_PutForm(t *testing.T) {
139 | it := assert.New(t)
140 | method := "PUT"
141 | uri := "/put?key"
142 | params := url.Values{"form-key": []string{"form-value"}}
143 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
144 | it.Equal(method, r.Method)
145 | it.Equal("application/x-www-form-urlencoded", r.Header.Get("Content-Type"))
146 | it.Equal(uri, r.RequestURI)
147 | it.Equal("form-value", r.FormValue("form-key"))
148 |
149 | _, ok := r.URL.Query()["key"]
150 | it.True(ok)
151 | it.Empty(r.FormValue("key"))
152 |
153 | w.Header().Set("x-request-method", r.Method)
154 | w.WriteHeader(http.StatusOK)
155 | w.Write([]byte(method + " " + uri + " OK!"))
156 | })
157 |
158 | ts := httptest.NewServer(server)
159 | defer ts.Close()
160 |
161 | request := New(ts.URL, false).New(t)
162 | request.PutForm(uri, params)
163 | request.AssertOK()
164 | request.AssertHeader("x-request-method", method)
165 | request.AssertContains("PUT /put?key OK!")
166 | }
167 |
168 | func TestRequest_PutJSON(t *testing.T) {
169 | it := assert.New(t)
170 | method := "PUT"
171 | uri := "/put?key"
172 | params := struct {
173 | Name string `json:"name"`
174 | Age int `json:"age"`
175 | Married bool `json:"married"`
176 | }{"testing", 1, false}
177 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
178 | it.Equal(method, r.Method)
179 | it.Equal("application/json", r.Header.Get("Content-Type"))
180 | it.Equal(uri, r.RequestURI)
181 |
182 | _, ok := r.URL.Query()["key"]
183 | it.True(ok)
184 | it.Empty(r.FormValue("key"))
185 |
186 | b, err := io.ReadAll(r.Body)
187 | r.Body.Close()
188 | it.Nil(err)
189 | it.Equal(`{"name":"testing","age":1,"married":false}`, string(b))
190 |
191 | w.Header().Set("x-request-method", r.Method)
192 | w.WriteHeader(http.StatusOK)
193 | w.Write([]byte(method + " " + uri + " OK!"))
194 | })
195 |
196 | ts := httptest.NewServer(server)
197 | defer ts.Close()
198 |
199 | request := New(ts.URL, false).New(t)
200 | request.PutJSON(uri, params)
201 | request.AssertOK()
202 | request.AssertHeader("x-request-method", method)
203 | request.AssertContains("PUT /put?key OK!")
204 | }
205 |
206 | func TestRequest_PutXML(t *testing.T) {
207 | type xmlData struct {
208 | XMLName xml.Name `xml:"Person"`
209 | Name string `xml:"Name"`
210 | Age int `xml:"Age"`
211 | Married bool `xml:"Married"`
212 | }
213 |
214 | it := assert.New(t)
215 | method := "PUT"
216 | uri := "/put?key"
217 | params := xmlData{
218 | Name: "testing",
219 | Age: 1,
220 | Married: false,
221 | }
222 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
223 | it.Equal(method, r.Method)
224 | it.Equal("text/xml", r.Header.Get("Content-Type"))
225 | it.Equal(uri, r.RequestURI)
226 |
227 | _, ok := r.URL.Query()["key"]
228 | it.True(ok)
229 | it.Empty(r.FormValue("key"))
230 |
231 | b, err := io.ReadAll(r.Body)
232 | r.Body.Close()
233 | it.Nil(err)
234 | it.Equal(`testing1false`, string(b))
235 |
236 | w.Header().Set("x-request-method", r.Method)
237 | w.WriteHeader(http.StatusOK)
238 | w.Write([]byte(method + " " + uri + " OK!"))
239 | })
240 |
241 | ts := httptest.NewServer(server)
242 | defer ts.Close()
243 |
244 | request := New(ts.URL, false).New(t)
245 | request.PutXML(uri, params)
246 | request.AssertOK()
247 | request.AssertHeader("x-request-method", method)
248 | request.AssertContains("PUT /put?key OK!")
249 | }
250 |
251 | func TestRequest_PostForm(t *testing.T) {
252 | it := assert.New(t)
253 | method := "POST"
254 | uri := "/post?key"
255 | params := url.Values{"form-key": []string{"form-value"}}
256 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
257 | it.Equal(method, r.Method)
258 | it.Equal("application/x-www-form-urlencoded", r.Header.Get("Content-Type"))
259 | it.Equal(uri, r.RequestURI)
260 | it.Equal("form-value", r.FormValue("form-key"))
261 |
262 | _, ok := r.URL.Query()["key"]
263 | it.True(ok)
264 | it.Empty(r.FormValue("key"))
265 |
266 | w.Header().Set("x-request-method", r.Method)
267 | w.WriteHeader(http.StatusOK)
268 | w.Write([]byte(method + " " + uri + " OK!"))
269 | })
270 |
271 | ts := httptest.NewServer(server)
272 | defer ts.Close()
273 |
274 | request := New(ts.URL, false).New(t)
275 | request.PostForm(uri, params)
276 | request.AssertOK()
277 | request.AssertHeader("x-request-method", method)
278 | request.AssertContains("POST /post?key OK!")
279 | }
280 |
281 | func TestRequest_PostJSON(t *testing.T) {
282 | it := assert.New(t)
283 | method := "POST"
284 | uri := "/post?key"
285 | params := struct {
286 | Name string `json:"name"`
287 | Age int `json:"age"`
288 | Married bool `json:"married"`
289 | }{"testing", 1, false}
290 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
291 | it.Equal(method, r.Method)
292 | it.Equal("application/json", r.Header.Get("Content-Type"))
293 | it.Equal(uri, r.RequestURI)
294 |
295 | _, ok := r.URL.Query()["key"]
296 | it.True(ok)
297 | it.Empty(r.FormValue("key"))
298 |
299 | b, err := io.ReadAll(r.Body)
300 | r.Body.Close()
301 | it.Nil(err)
302 | it.Equal(`{"name":"testing","age":1,"married":false}`, string(b))
303 |
304 | w.Header().Set("x-request-method", r.Method)
305 | w.WriteHeader(http.StatusOK)
306 | w.Write([]byte(method + " " + uri + " OK!"))
307 | })
308 |
309 | ts := httptest.NewServer(server)
310 | defer ts.Close()
311 |
312 | request := New(ts.URL, false).New(t)
313 | request.PostJSON(uri, params)
314 | request.AssertOK()
315 | request.AssertHeader("x-request-method", method)
316 | request.AssertContains("POST /post?key OK!")
317 | }
318 |
319 | func TestRequest_PostXML(t *testing.T) {
320 | type xmlData struct {
321 | XMLName xml.Name `xml:"Person"`
322 | Name string `xml:"Name"`
323 | Age int `xml:"Age"`
324 | Married bool `xml:"Married"`
325 | }
326 |
327 | it := assert.New(t)
328 | method := "POST"
329 | uri := "/post?key"
330 | params := xmlData{
331 | Name: "testing",
332 | Age: 1,
333 | Married: false,
334 | }
335 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
336 | it.Equal(method, r.Method)
337 | it.Equal("text/xml", r.Header.Get("Content-Type"))
338 | it.Equal(uri, r.RequestURI)
339 |
340 | _, ok := r.URL.Query()["key"]
341 | it.True(ok)
342 | it.Empty(r.FormValue("key"))
343 |
344 | b, err := io.ReadAll(r.Body)
345 | r.Body.Close()
346 | it.Nil(err)
347 | it.Equal(`testing1false`, string(b))
348 |
349 | w.Header().Set("x-request-method", r.Method)
350 | w.WriteHeader(http.StatusOK)
351 | w.Write([]byte(method + " " + uri + " OK!"))
352 | })
353 |
354 | ts := httptest.NewServer(server)
355 | defer ts.Close()
356 |
357 | request := New(ts.URL, false).New(t)
358 | request.PostXML(uri, params)
359 | request.AssertOK()
360 | request.AssertHeader("x-request-method", method)
361 | request.AssertContains("POST /post?key OK!")
362 | }
363 |
364 | func TestRequest_PatchForm(t *testing.T) {
365 | it := assert.New(t)
366 | method := "PATCH"
367 | uri := "/patch?key"
368 | params := url.Values{"form-key": []string{"form-value"}}
369 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
370 | it.Equal(method, r.Method)
371 | it.Equal("application/x-www-form-urlencoded", r.Header.Get("Content-Type"))
372 | it.Equal(uri, r.RequestURI)
373 | it.Equal("form-value", r.FormValue("form-key"))
374 |
375 | _, ok := r.URL.Query()["key"]
376 | it.True(ok)
377 | it.Empty(r.FormValue("key"))
378 |
379 | w.Header().Set("x-request-method", r.Method)
380 | w.WriteHeader(http.StatusOK)
381 | w.Write([]byte(method + " " + uri + " OK!"))
382 | })
383 |
384 | ts := httptest.NewServer(server)
385 | defer ts.Close()
386 |
387 | request := New(ts.URL, false).New(t)
388 | request.PatchForm(uri, params)
389 | request.AssertOK()
390 | request.AssertHeader("x-request-method", method)
391 | request.AssertContains("PATCH /patch?key OK!")
392 | }
393 |
394 | func TestRequest_PatchJSON(t *testing.T) {
395 | it := assert.New(t)
396 | method := "PATCH"
397 | uri := "/patch?key"
398 | params := struct {
399 | Name string `json:"name"`
400 | Age int `json:"age"`
401 | Married bool `json:"married"`
402 | }{"testing", 1, false}
403 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
404 | it.Equal(method, r.Method)
405 | it.Equal("application/json", r.Header.Get("Content-Type"))
406 | it.Equal(uri, r.RequestURI)
407 |
408 | _, ok := r.URL.Query()["key"]
409 | it.True(ok)
410 | it.Empty(r.FormValue("key"))
411 |
412 | b, err := io.ReadAll(r.Body)
413 | r.Body.Close()
414 | it.Nil(err)
415 | it.Equal(`{"name":"testing","age":1,"married":false}`, string(b))
416 |
417 | w.Header().Set("x-request-method", r.Method)
418 | w.WriteHeader(http.StatusOK)
419 | w.Write([]byte(method + " " + uri + " OK!"))
420 | })
421 |
422 | ts := httptest.NewServer(server)
423 | defer ts.Close()
424 |
425 | request := New(ts.URL, false).New(t)
426 | request.PatchJSON(uri, params)
427 | request.AssertOK()
428 | request.AssertHeader("x-request-method", method)
429 | request.AssertContains("PATCH /patch?key OK!")
430 | }
431 |
432 | func TestRequest_PatchXML(t *testing.T) {
433 | type xmlData struct {
434 | XMLName xml.Name `xml:"Person"`
435 | Name string `xml:"Name"`
436 | Age int `xml:"Age"`
437 | Married bool `xml:"Married"`
438 | }
439 |
440 | it := assert.New(t)
441 | method := "PATCH"
442 | uri := "/patch?key"
443 | params := xmlData{
444 | Name: "testing",
445 | Age: 1,
446 | Married: false,
447 | }
448 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
449 | it.Equal(method, r.Method)
450 | it.Equal("text/xml", r.Header.Get("Content-Type"))
451 | it.Equal(uri, r.RequestURI)
452 |
453 | _, ok := r.URL.Query()["key"]
454 | it.True(ok)
455 | it.Empty(r.FormValue("key"))
456 |
457 | b, err := io.ReadAll(r.Body)
458 | r.Body.Close()
459 | it.Nil(err)
460 | it.Equal(`testing1false`, string(b))
461 |
462 | w.Header().Set("x-request-method", r.Method)
463 | w.WriteHeader(http.StatusOK)
464 | w.Write([]byte(method + " " + uri + " OK!"))
465 | })
466 |
467 | ts := httptest.NewServer(server)
468 | defer ts.Close()
469 |
470 | request := New(ts.URL, false).New(t)
471 | request.PatchXML(uri, params)
472 | request.AssertOK()
473 | request.AssertHeader("x-request-method", method)
474 | request.AssertContains("PATCH /patch?key OK!")
475 | }
476 |
477 | func TestRequest_DeleteForm(t *testing.T) {
478 | it := assert.New(t)
479 | method := "DELETE"
480 | uri := "/delete?key"
481 | params := url.Values{"form-key": []string{"form-value"}}
482 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
483 | it.Equal(method, r.Method)
484 | it.Equal("application/x-www-form-urlencoded", r.Header.Get("Content-Type"))
485 | it.Equal(uri, r.RequestURI)
486 | it.Empty(r.FormValue("form-key"))
487 |
488 | _, ok := r.URL.Query()["key"]
489 | it.True(ok)
490 | it.Empty(r.FormValue("key"))
491 |
492 | b, err := io.ReadAll(r.Body)
493 | r.Body.Close()
494 | it.Nil(err)
495 | it.Equal(`form-key=form-value`, string(b))
496 |
497 | w.Header().Set("x-request-method", r.Method)
498 | w.WriteHeader(http.StatusOK)
499 | w.Write([]byte(method + " " + uri + " OK!"))
500 | })
501 |
502 | ts := httptest.NewServer(server)
503 | defer ts.Close()
504 |
505 | request := New(ts.URL, false).New(t)
506 | request.DeleteForm(uri, params)
507 | request.AssertOK()
508 | request.AssertHeader("x-request-method", method)
509 | request.AssertContains("DELETE /delete?key OK!")
510 | }
511 |
512 | func TestRequest_DeleteJSON(t *testing.T) {
513 | it := assert.New(t)
514 | method := "DELETE"
515 | uri := "/delete?key"
516 | params := struct {
517 | Name string `json:"name"`
518 | Age int `json:"age"`
519 | Married bool `json:"married"`
520 | }{"testing", 1, false}
521 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
522 | it.Equal(method, r.Method)
523 | it.Equal("application/json", r.Header.Get("Content-Type"))
524 | it.Equal(uri, r.RequestURI)
525 |
526 | _, ok := r.URL.Query()["key"]
527 | it.True(ok)
528 | it.Empty(r.FormValue("key"))
529 |
530 | b, err := io.ReadAll(r.Body)
531 | r.Body.Close()
532 | it.Nil(err)
533 | it.Equal(`{"name":"testing","age":1,"married":false}`, string(b))
534 |
535 | w.Header().Set("x-request-method", r.Method)
536 | w.WriteHeader(http.StatusOK)
537 | w.Write([]byte(method + " " + uri + " OK!"))
538 | })
539 |
540 | ts := httptest.NewServer(server)
541 | defer ts.Close()
542 |
543 | request := New(ts.URL, false).New(t)
544 | request.DeleteJSON(uri, params)
545 | request.AssertOK()
546 | request.AssertHeader("x-request-method", method)
547 | request.AssertContains("DELETE /delete?key OK!")
548 | }
549 |
550 | func TestRequest_DeleteXML(t *testing.T) {
551 | type xmlData struct {
552 | XMLName xml.Name `xml:"Person"`
553 | Name string `xml:"Name"`
554 | Age int `xml:"Age"`
555 | Married bool `xml:"Married"`
556 | }
557 |
558 | it := assert.New(t)
559 | method := "DELETE"
560 | uri := "/delete?key"
561 | params := xmlData{
562 | Name: "testing",
563 | Age: 1,
564 | Married: false,
565 | }
566 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
567 | it.Equal(method, r.Method)
568 | it.Equal("text/xml", r.Header.Get("Content-Type"))
569 | it.Equal(uri, r.RequestURI)
570 |
571 | _, ok := r.URL.Query()["key"]
572 | it.True(ok)
573 | it.Empty(r.FormValue("key"))
574 |
575 | b, err := io.ReadAll(r.Body)
576 | r.Body.Close()
577 | it.Nil(err)
578 | it.Equal(`testing1false`, string(b))
579 |
580 | w.Header().Set("x-request-method", r.Method)
581 | w.WriteHeader(http.StatusOK)
582 | w.Write([]byte(method + " " + uri + " OK!"))
583 | })
584 |
585 | ts := httptest.NewServer(server)
586 | defer ts.Close()
587 |
588 | request := New(ts.URL, false).New(t)
589 | request.DeleteXML(uri, params)
590 | request.AssertOK()
591 | request.AssertHeader("x-request-method", method)
592 | request.AssertContains("DELETE /delete?key OK!")
593 | }
594 |
--------------------------------------------------------------------------------
/request_test.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "sync"
7 | "testing"
8 | "time"
9 |
10 | "github.com/golib/assert"
11 | )
12 |
13 | func TestRequest(t *testing.T) {
14 | it := assert.New(t)
15 |
16 | method := "GET"
17 | uri := "/request/client"
18 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
19 | it.Equal(method, r.Method)
20 | it.Equal("/request/client", r.RequestURI)
21 |
22 | w.WriteHeader(http.StatusOK)
23 | w.Write([]byte(r.Header.Get("X-Mock-Client")))
24 | })
25 |
26 | ts := httptest.NewServer(server)
27 | defer ts.Close()
28 |
29 | client := New(ts.URL, false)
30 |
31 | request := client.New(t)
32 | request.WithHeader("X-Mock-Client", "httptesting")
33 |
34 | request.Get("/request/client", nil)
35 | request.AssertOK()
36 | request.AssertContains("httptesting")
37 | }
38 |
39 | func TestRequestWithConcurrency(t *testing.T) {
40 | it := assert.New(t)
41 |
42 | method := "GET"
43 | uri := "/request/client"
44 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
45 | it.Equal(method, r.Method)
46 | it.Equal("/request/client", r.RequestURI)
47 |
48 | time.Sleep(10 * time.Millisecond)
49 |
50 | w.WriteHeader(http.StatusOK)
51 | w.Write([]byte(r.Header.Get("X-Mock-Client")))
52 | })
53 |
54 | ts := httptest.NewServer(server)
55 | defer ts.Close()
56 |
57 | client := New(ts.URL, false)
58 |
59 | var (
60 | wg sync.WaitGroup
61 |
62 | concurrency = 3
63 | )
64 |
65 | issuedAt := time.Now()
66 |
67 | wg.Add(concurrency)
68 | for i := 0; i < concurrency; i++ {
69 | go func() {
70 | defer wg.Done()
71 |
72 | request := client.New(t)
73 | request.WithHeader("X-Mock-Client", "httptesting")
74 |
75 | request.Get("/request/client", nil)
76 | request.AssertOK()
77 | request.AssertContains("httptesting")
78 | }()
79 | }
80 | wg.Wait()
81 |
82 | delta := time.Since(issuedAt)
83 | it.True(delta < 20*time.Millisecond)
84 | }
85 |
--------------------------------------------------------------------------------
/server_1_8.go:
--------------------------------------------------------------------------------
1 | //go:build !go1.9
2 |
3 | package httptesting
4 |
5 | import (
6 | "crypto/tls"
7 | "crypto/x509"
8 | "fmt"
9 | "log"
10 | "net/http"
11 | "net/http/cookiejar"
12 | "net/http/httptest"
13 | "net/url"
14 |
15 | "github.com/dolab/httptesting/internal"
16 | )
17 |
18 | // NewServer returns an initialized *Client along with mocked server for testing
19 | // NOTE: You MUST call client.Close() for cleanup after testing.
20 | func NewServer(handler http.Handler, isTLS bool) *Client {
21 | var (
22 | ts *httptest.Server
23 | certs *x509.CertPool
24 | )
25 | if isTLS {
26 | cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)
27 | if err != nil {
28 | panic(fmt.Sprintf("httptesting: NewTLSServer: %v", err))
29 | }
30 |
31 | ts = httptest.NewTLSServer(handler)
32 |
33 | x509cert, err := x509.ParseCertificate(cert.Certificate[0])
34 | if err != nil {
35 | panic(fmt.Sprintf("httptesting: NewTLSServer: %v", err))
36 | }
37 |
38 | certs = x509.NewCertPool()
39 | certs.AddCert(x509cert)
40 | } else {
41 | ts = httptest.NewServer(handler)
42 | }
43 |
44 | jar, err := cookiejar.New(nil)
45 | if err != nil {
46 | panic(err.Error())
47 | }
48 |
49 | urlobj, err := url.Parse(ts.URL)
50 | if err != nil {
51 | panic(err.Error())
52 | }
53 |
54 | return &Client{
55 | server: ts,
56 | host: urlobj.Host,
57 | certs: certs,
58 | jar: jar,
59 | isTLS: isTLS,
60 | }
61 | }
62 |
63 | // NewServerWithTLS returns an initialized *Client along with mocked server for testing
64 | // NOTE: You MUST call client.Close() for cleanup after testing.
65 | func NewServerWithTLS(handler http.Handler, cert tls.Certificate) *Client {
66 | x509cert, err := x509.ParseCertificate(cert.Certificate[0])
67 | if err != nil {
68 | log.Fatal(err)
69 | }
70 |
71 | ts := httptest.NewUnstartedServer(handler)
72 | ts.TLS = &tls.Config{
73 | Certificates: []tls.Certificate{cert},
74 | }
75 | ts.StartTLS()
76 |
77 | certs := x509.NewCertPool()
78 | certs.AddCert(x509cert)
79 |
80 | jar, err := cookiejar.New(nil)
81 | if err != nil {
82 | panic(err.Error())
83 | }
84 |
85 | urlobj, err := url.Parse(ts.URL)
86 | if err != nil {
87 | panic(err.Error())
88 | }
89 |
90 | return &Client{
91 | server: ts,
92 | host: urlobj.Host,
93 | certs: certs,
94 | jar: jar,
95 | isTLS: true,
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/server_1_9.go:
--------------------------------------------------------------------------------
1 | //go:build go1.9
2 |
3 | package httptesting
4 |
5 | import (
6 | "crypto/tls"
7 | "crypto/x509"
8 | "net/http"
9 | "net/http/cookiejar"
10 | "net/http/httptest"
11 | "net/url"
12 | )
13 |
14 | // NewServer returns an initialized *Client along with mocked server for testing
15 | // NOTE: You MUST call client.Close() for cleanup after testing.
16 | func NewServer(handler http.Handler, isTLS bool) *Client {
17 | var (
18 | ts *httptest.Server
19 | certs *x509.CertPool
20 | )
21 | if isTLS {
22 | ts = httptest.NewTLSServer(handler)
23 |
24 | if transport, ok := ts.Client().Transport.(*http.Transport); ok {
25 | certs = transport.TLSClientConfig.RootCAs
26 | }
27 | } else {
28 | ts = httptest.NewServer(handler)
29 | }
30 |
31 | jar, err := cookiejar.New(nil)
32 | if err != nil {
33 | panic(err.Error())
34 | }
35 |
36 | urlobj, err := url.Parse(ts.URL)
37 | if err != nil {
38 | panic(err.Error())
39 | }
40 |
41 | return &Client{
42 | server: ts,
43 | host: urlobj.Host,
44 | certs: certs,
45 | jar: jar,
46 | isTLS: isTLS,
47 | }
48 | }
49 |
50 | // NewServerWithTLS returns an initialized *Client along with mocked server for testing
51 | // NOTE: You MUST call client.Close() for cleanup after testing.
52 | func NewServerWithTLS(handler http.Handler, cert tls.Certificate) *Client {
53 | ts := httptest.NewUnstartedServer(handler)
54 | ts.TLS = &tls.Config{
55 | Certificates: []tls.Certificate{cert},
56 | }
57 | ts.StartTLS()
58 |
59 | jar, err := cookiejar.New(nil)
60 | if err != nil {
61 | panic(err.Error())
62 | }
63 |
64 | urlobj, err := url.Parse(ts.URL)
65 | if err != nil {
66 | panic(err.Error())
67 | }
68 |
69 | var certs *x509.CertPool
70 | if transport, ok := ts.Client().Transport.(*http.Transport); ok {
71 | certs = transport.TLSClientConfig.RootCAs
72 | }
73 |
74 | return &Client{
75 | server: ts,
76 | host: urlobj.Host,
77 | certs: certs,
78 | jar: jar,
79 | isTLS: true,
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/server_test.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "net/http"
7 | "testing"
8 |
9 | "github.com/dolab/httptesting/internal"
10 | "github.com/golib/assert"
11 | )
12 |
13 | func Test_NewServer(t *testing.T) {
14 | it := assert.New(t)
15 |
16 | method := "GET"
17 | uri := "/server"
18 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
19 | w.Header().Set("x-request-method", r.Method)
20 |
21 | w.WriteHeader(http.StatusOK)
22 | w.Write([]byte("TLS"))
23 | })
24 |
25 | ts := NewServer(server, true)
26 | defer ts.Close()
27 |
28 | it.NotNil(ts.server)
29 | it.NotEmpty(ts.host)
30 | it.True(ts.isTLS)
31 |
32 | // it should work with internal client
33 | request := ts.New(t)
34 | request.Get("/server/tls", nil)
35 | request.AssertOK()
36 | request.AssertContains("TLS")
37 |
38 | // it should work with custom TLS client
39 | cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)
40 | if it.Nil(err) {
41 | x509cert, err := x509.ParseCertificate(cert.Certificate[0])
42 | if it.Nil(err) {
43 | client := NewWithTLS(ts.Url(""), x509cert)
44 |
45 | request = client.New(t)
46 | request.Get("/server/tls", nil)
47 | request.AssertOK()
48 | request.AssertContains("TLS")
49 | }
50 | }
51 | }
52 |
53 | func Test_NewServerWithTLS(t *testing.T) {
54 | it := assert.New(t)
55 |
56 | method := "GET"
57 | uri := "/server/tls"
58 | server := newMockServer(method, uri, func(w http.ResponseWriter, r *http.Request) {
59 | w.Header().Set("x-request-method", r.Method)
60 |
61 | w.WriteHeader(http.StatusOK)
62 | w.Write([]byte("TLS"))
63 | })
64 |
65 | cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)
66 | if it.Nil(err) {
67 | ts := NewServerWithTLS(server, cert)
68 | defer ts.Close()
69 |
70 | it.NotNil(ts.server)
71 | it.NotEmpty(ts.host)
72 | it.True(ts.isTLS)
73 |
74 | // it should work with internal client
75 | request := ts.New(t)
76 | request.Get("/server/tls", nil)
77 | request.AssertOK()
78 | request.AssertContains("TLS")
79 |
80 | // it should work with custom TLS client
81 | x509cert, err := x509.ParseCertificate(cert.Certificate[0])
82 | if it.Nil(err) {
83 | client := NewWithTLS(ts.Url(""), x509cert)
84 |
85 | request = client.New(t)
86 | request.Get("/server/tls", nil)
87 | request.AssertOK()
88 | request.AssertContains("TLS")
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/transport.go:
--------------------------------------------------------------------------------
1 | package httptesting
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "crypto/x509"
7 | "net"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | // RequestFilter is a callback for http request injection.
13 | type RequestFilter func(r *http.Request) error
14 |
15 | // FilterTransport defines a custom http.Transport with filters and certs.
16 | type FilterTransport struct {
17 | filters []RequestFilter
18 | certs []*x509.CertPool
19 | }
20 |
21 | func NewFilterTransport(filters []RequestFilter, certs ...*x509.CertPool) *FilterTransport {
22 | return &FilterTransport{
23 | filters: filters,
24 | certs: certs,
25 | }
26 | }
27 |
28 | func (transport *FilterTransport) RoundTrip(r *http.Request) (*http.Response, error) {
29 | tr := &http.Transport{
30 | Proxy: http.ProxyFromEnvironment,
31 | DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
32 | // invoke filters
33 | for _, filter := range transport.filters {
34 | err := filter(r)
35 | if err != nil {
36 | return nil, err
37 | }
38 | }
39 |
40 | dialer := &net.Dialer{
41 | Timeout: 10 * time.Second,
42 | KeepAlive: 30 * time.Second,
43 | }
44 |
45 | conn, err := dialer.Dial(network, address)
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | return conn, nil
51 | },
52 | ResponseHeaderTimeout: 3 * time.Second,
53 | }
54 |
55 | if len(transport.certs) > 0 {
56 | tr.TLSClientConfig = &tls.Config{
57 | RootCAs: transport.certs[0],
58 | InsecureSkipVerify: true,
59 | }
60 | tr.TLSHandshakeTimeout = 5 * time.Second
61 | }
62 |
63 | return tr.RoundTrip(r)
64 | }
65 |
--------------------------------------------------------------------------------