├── assets └── banner.png ├── Makefile ├── .codecov.yml ├── go.mod ├── response.go ├── example └── example.go ├── LICENSE ├── .github └── workflows │ └── build.yml ├── go.sum ├── option.go ├── option_test.go ├── README.md ├── response_test.go ├── client.go ├── .gitignore └── client_test.go /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bozd4g/go-http-client/HEAD/assets/banner.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @go test $(go list ./... | grep -v /example/) 3 | 4 | cov: 5 | @go test -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v /example/) 6 | 7 | coverage: 8 | $(MAKE) cov 9 | @go tool cover -html=coverage.txt -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: true 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: "reach,diff,flags,tree" 19 | behavior: default 20 | require_changes: false -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bozd4g/go-http-client 2 | 3 | go 1.19 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pkg/errors v0.9.1 // indirect 10 | github.com/pmezard/go-difflib v1.0.0 // indirect 11 | github.com/stretchr/objx v0.1.0 // indirect 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package gohttpclient 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type ( 9 | Response struct { 10 | res *http.Response 11 | body []byte 12 | } 13 | ) 14 | 15 | func (r *Response) Body() []byte { 16 | return r.body 17 | } 18 | 19 | func (r *Response) Unmarshal(v any) error { 20 | return json.Unmarshal(r.body, &v) 21 | } 22 | 23 | func (r *Response) Status() int { 24 | return r.res.StatusCode 25 | } 26 | 27 | func (r *Response) Headers() http.Header { 28 | return r.res.Header 29 | } 30 | 31 | func (r *Response) Cookies() []*http.Cookie { 32 | return r.res.Cookies() 33 | } 34 | 35 | func (r *Response) Ok() bool { 36 | return r.res.StatusCode >= 200 && r.res.StatusCode <= 299 37 | } 38 | 39 | func (r *Response) Get() *http.Response { 40 | return r.res 41 | } 42 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | ghc "github.com/bozd4g/go-http-client" 9 | ) 10 | 11 | type Post struct { 12 | ID int `json:"id"` 13 | Title string `json:"title"` 14 | } 15 | 16 | func main() { 17 | ctx := context.Background() 18 | 19 | opts := []ghc.ClientOption{ 20 | ghc.WithDefaultHeaders(), 21 | ghc.WithTimeout(time.Second * 3), 22 | } 23 | client := ghc.New("https://jsonplaceholder.typicode.com", opts...) 24 | 25 | reqOpts := []ghc.Option{ 26 | ghc.WithHeader("x-useragent", "go-http-client"), 27 | ghc.WithHeader("x-correlationid", "123456789"), 28 | } 29 | response, err := client.Get(ctx, "/posts/1", reqOpts...) 30 | if err != nil { 31 | log.Fatalf("error: %v", err) 32 | } 33 | 34 | var post Post 35 | if err := response.Unmarshal(&post); err != nil { 36 | log.Fatalf("error: %v", err) 37 | } 38 | 39 | log.Printf(post.Title) // sunt aut facere repellat provident occaecati... 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Furkan Bozdag 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 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Set up Go 14 | uses: actions/setup-go@v3 15 | with: 16 | go-version: 1.18 17 | 18 | - name: Build 19 | run: go build -v ./... 20 | 21 | test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: 1.18 30 | 31 | - name: Test 32 | run: make cov 33 | 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v2 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | file: ./coverage.txt 39 | flags: unittests 40 | name: codecov-umbrella 41 | fail_ci_if_error: true 42 | 43 | lint: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v3 47 | 48 | - name: Set up Go 49 | uses: actions/setup-go@v3 50 | with: 51 | go-version: 1.18 52 | 53 | - name: Lint 54 | run: go vet ./... -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 10 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package gohttpclient 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | type ( 9 | Option func(c *Client) 10 | ClientOption Option 11 | ) 12 | 13 | func WithCustomHttpClient(client *http.Client) ClientOption { 14 | return func(c *Client) { 15 | c.httpClient = client 16 | } 17 | } 18 | 19 | func WithDefaultHeaders() ClientOption { 20 | return func(c *Client) { 21 | if c.headers == nil { 22 | c.headers = make(map[string]Header) 23 | } 24 | 25 | c.headers["Content-Type"] = Header{Value: "application/json", IsDefault: true} 26 | c.headers["Accept"] = Header{Value: "application/json", IsDefault: true} 27 | } 28 | } 29 | 30 | func WithTimeout(timeout time.Duration) ClientOption { 31 | return func(c *Client) { 32 | c.timeout = timeout 33 | c.httpClient.Timeout = timeout 34 | } 35 | } 36 | 37 | func WithHeader(key, value string) Option { 38 | return func(c *Client) { 39 | if c.headers == nil { 40 | c.headers = make(map[string]Header) 41 | } 42 | 43 | c.headers[key] = Header{Value: value, IsDefault: false} 44 | } 45 | } 46 | 47 | func WithQuery(key, value string) Option { 48 | return func(c *Client) { 49 | if c.query == nil { 50 | c.query = make(map[string]string) 51 | } 52 | 53 | c.query[key] = value 54 | } 55 | } 56 | 57 | func WithBody(body []byte) Option { 58 | return func(c *Client) { 59 | c.body = body 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /option_test.go: -------------------------------------------------------------------------------- 1 | package gohttpclient 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type TestOptionSuite struct { 13 | suite.Suite 14 | ctx context.Context 15 | } 16 | 17 | func TestOption(t *testing.T) { 18 | suite.Run(t, new(TestOptionSuite)) 19 | } 20 | 21 | func (s *TestOptionSuite) SetupSuite() { 22 | s.ctx = context.Background() 23 | } 24 | 25 | func (s *TestOptionSuite) Test_WithCustomHttpClient_ShouldRunSuccesfully() { 26 | // Arrange 27 | baseUrl := "http://localhost:8080" 28 | httpClient := &http.Client{} 29 | client := New(baseUrl) 30 | 31 | // Act 32 | WithCustomHttpClient(httpClient)(client) 33 | 34 | // Assert 35 | s.Assert().NotNil(client.httpClient) 36 | s.Assert().Equal(httpClient, client.httpClient) 37 | } 38 | 39 | func (s *TestOptionSuite) Test_WithDefaultHeaders_ShouldRunSuccesfully() { 40 | // Arrange 41 | baseUrl := "http://localhost:8080" 42 | client := New(baseUrl) 43 | 44 | // Act 45 | WithDefaultHeaders()(client) 46 | 47 | // Assert 48 | s.Assert().Equal("application/json", client.headers["Content-Type"].Value) 49 | s.Assert().Equal("application/json", client.headers["Accept"].Value) 50 | } 51 | 52 | func (s *TestOptionSuite) Test_WithDefaultHeaders_WhenCalledByNew_ShouldRunSuccesfully() { 53 | // Arrange 54 | baseUrl := "http://localhost:8080" 55 | 56 | // Act 57 | client := New(baseUrl, WithDefaultHeaders()) 58 | 59 | // Assert 60 | s.Assert().Equal("application/json", client.headers["Content-Type"].Value) 61 | s.Assert().Equal("application/json", client.headers["Accept"].Value) 62 | } 63 | 64 | func (s *TestOptionSuite) Test_WithTimeout_ShouldRunSuccesfully() { 65 | // Arrange 66 | timeout := 5 * time.Second 67 | baseUrl := "http://localhost:8080" 68 | client := New(baseUrl) 69 | 70 | // Act 71 | WithTimeout(timeout)(client) 72 | 73 | // Assert 74 | s.Assert().Equal(timeout, client.timeout) 75 | } 76 | 77 | func (s *TestOptionSuite) Test_WithHeader_ShouldRunSuccesfully() { 78 | // Arrange 79 | baseUrl := "http://localhost:8080" 80 | client := New(baseUrl) 81 | 82 | // Act 83 | WithHeader("Content-Type", "application/json")(client) 84 | 85 | // Assert 86 | s.Assert().Equal("application/json", client.headers["Content-Type"].Value) 87 | } 88 | 89 | func (s *TestOptionSuite) Test_WithQuery_ShouldRunSuccesfully() { 90 | // Arrange 91 | baseUrl := "http://localhost:8080" 92 | client := New(baseUrl) 93 | 94 | // Act 95 | WithQuery("key", "value")(client) 96 | 97 | // Assert 98 | s.Assert().Equal("value", client.query["key"]) 99 | } 100 | 101 | func (s *TestOptionSuite) Test_WithBody_ShouldRunSuccesfully() { 102 | // Arrange 103 | baseUrl := "http://localhost:8080" 104 | client := New(baseUrl) 105 | 106 | // Act 107 | WithBody([]byte("body"))(client) 108 | 109 | // Assert 110 | s.Assert().Equal("body", string(client.body)) 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
13 | An enhanced http client for Golang 14 |
15 | 16 |17 | Documentation on go.dev 🔗 18 |
19 | 20 | 31 | 32 | This package provides you a http client package for your http requests. You can send requests quicly with this package. If you want to contribute this package, please fork and [create](https://github.com/bozd4g/go-http-client/pulls) a pull request. 33 | 34 | ## Installation 35 | ``` 36 | $ go get -u github.com/bozd4g/go-http-client/ 37 | ``` 38 | 39 | ## Example Usage 40 | ```go 41 | package main 42 | 43 | import ( 44 | "context" 45 | "log" 46 | 47 | gohttpclient "github.com/bozd4g/go-http-client" 48 | ) 49 | 50 | type Post struct { 51 | ID int `json:"id"` 52 | Title string `json:"title"` 53 | } 54 | 55 | func main() { 56 | ctx := context.Background() 57 | client := gohttpclient.New("https://jsonplaceholder.typicode.com") 58 | 59 | response, err := client.Get(ctx, "/posts/1") 60 | if err != nil { 61 | log.Fatalf("error: %v", err) 62 | } 63 | 64 | var post Post 65 | if err := response.Unmarshal(&post); err != nil { 66 | log.Fatalf("error: %v", err) 67 | } 68 | 69 | log.Printf(post.Title) // sunt aut facere repellat provident occaecati... 70 | } 71 | ``` 72 | 73 | ## License 74 | Copyright (c) 2020 Furkan Bozdag 75 | 76 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 77 | 78 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 79 | 80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 81 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package gohttpclient 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type TestResponseSuite struct { 14 | suite.Suite 15 | ctx context.Context 16 | } 17 | 18 | func TestResponse(t *testing.T) { 19 | suite.Run(t, new(TestResponseSuite)) 20 | } 21 | 22 | func (s *TestResponseSuite) SetupSuite() { 23 | s.ctx = context.Background() 24 | } 25 | 26 | func (s *TestResponseSuite) Test_Body_ShouldRunSuccesfully() { 27 | // Arrange 28 | body := []byte("test") 29 | res := &http.Response{ 30 | Body: ioutil.NopCloser(bytes.NewBuffer(body)), 31 | } 32 | 33 | // Act 34 | resp := Response{res, body} 35 | 36 | // Assert 37 | s.Equal(body, resp.Body()) 38 | } 39 | 40 | func (s *TestResponseSuite) Test_Unmarshal_ShouldRunSuccesfully() { 41 | // Arrange 42 | body := []byte(`{"name":"test"}`) 43 | res := &http.Response{} 44 | 45 | // Act 46 | resp := Response{res, body} 47 | 48 | // Assert 49 | var response map[string]interface{} 50 | err := resp.Unmarshal(&response) 51 | s.NoError(err) 52 | s.Equal("test", response["name"]) 53 | } 54 | 55 | func (s *TestResponseSuite) Test_Unmarshal_WhenBodyIsWrong_ShouldReturnError() { 56 | // Arrange 57 | resp := Response{ 58 | body: nil, 59 | } 60 | 61 | // Act 62 | var response map[string]interface{} 63 | err := resp.Unmarshal(&response) 64 | 65 | // Assert 66 | s.Errorf(err, "error reading") 67 | } 68 | 69 | func (s *TestResponseSuite) Test_Unmarshal_WhenUnMarshalReturnsError_ShouldReturnError() { 70 | // Arrange 71 | body := []byte(`{"name":"test"`) 72 | res := &http.Response{} 73 | 74 | // Act 75 | resp := Response{res, body} 76 | 77 | // Assert 78 | var response map[string]interface{} 79 | err := resp.Unmarshal(&response) 80 | s.Error(err) 81 | } 82 | 83 | func (s *TestResponseSuite) Test_Status_ShouldRunSuccesfully() { 84 | // Arrange 85 | resp := Response{ 86 | res: &http.Response{ 87 | StatusCode: 200, 88 | }, 89 | } 90 | 91 | // Act 92 | status := resp.Status() 93 | 94 | // Assert 95 | s.Equal(200, status) 96 | } 97 | 98 | func (s *TestResponseSuite) Test_Header_ShouldRunSuccesfully() { 99 | // Arrange 100 | resp := Response{ 101 | res: &http.Response{ 102 | Header: http.Header{ 103 | "Content-Type": []string{"application/json"}, 104 | }, 105 | }, 106 | } 107 | 108 | // Act 109 | header := resp.Headers() 110 | 111 | // Assert 112 | s.Equal("application/json", header["Content-Type"][0]) 113 | } 114 | 115 | func (s *TestResponseSuite) Test_Cookies_ShouldRunSuccesfully() { 116 | // Arrange 117 | resp := Response{ 118 | res: &http.Response{ 119 | Header: http.Header{ 120 | "Set-Cookie": []string{"test=1"}, 121 | }, 122 | }, 123 | } 124 | 125 | // Act 126 | cookies := resp.Cookies() 127 | 128 | // Assert 129 | s.Equal("test=1", cookies[0].String()) 130 | } 131 | 132 | func (s *TestResponseSuite) Test_Ok_ShouldRunSuccesfully() { 133 | // Arrange 134 | resp := Response{ 135 | res: &http.Response{ 136 | StatusCode: 200, 137 | }, 138 | } 139 | 140 | // Act 141 | ok := resp.Ok() 142 | 143 | // Assert 144 | s.True(ok) 145 | } 146 | 147 | func (s *TestResponseSuite) Test_Get_ShouldRunSuccesfully() { 148 | // Arrange 149 | resp := Response{ 150 | res: &http.Response{ 151 | Header: http.Header{ 152 | "Content-Type": []string{"application/json"}, 153 | }, 154 | }, 155 | } 156 | 157 | // Act 158 | res := resp.Get() 159 | 160 | // Assert 161 | s.Equal(resp.res, res) 162 | } 163 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package gohttpclient 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type ( 14 | // Client is a struct who has BaseUrl property 15 | Client struct { 16 | baseUrl string 17 | httpClient *http.Client 18 | 19 | headers map[string]Header 20 | query map[string]string 21 | body []byte 22 | timeout time.Duration 23 | } 24 | 25 | // Clienter is a interface who calls the methods 26 | Clienter interface{} 27 | 28 | Header struct { 29 | Value string 30 | IsDefault bool 31 | } 32 | ) 33 | 34 | const ( 35 | DEFAULT_TIMEOUT = 10 * time.Second 36 | ) 37 | 38 | // New func returns a Client struct 39 | func New(baseUrl string, opts ...ClientOption) *Client { 40 | httpClient := &http.Client{Timeout: DEFAULT_TIMEOUT} 41 | client := &Client{httpClient: httpClient, baseUrl: baseUrl, timeout: DEFAULT_TIMEOUT} 42 | 43 | for _, opt := range opts { 44 | opt(client) 45 | } 46 | 47 | return client 48 | } 49 | 50 | // Get func returns a request 51 | func (c *Client) Get(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 52 | clear := c.initOpts(opts...) 53 | defer clear() 54 | 55 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseUrl+endpoint, nil) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | prepReq := c.prepareReq(req) 61 | return c.sendReq(ctx, prepReq) 62 | } 63 | 64 | // Post func returns a request 65 | func (c *Client) Post(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 66 | clear := c.initOpts(opts...) 67 | defer clear() 68 | 69 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseUrl+endpoint, bytes.NewBuffer(c.body)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | prepReq := c.prepareReq(req) 75 | return c.sendReq(ctx, prepReq) 76 | 77 | } 78 | 79 | // Put func returns a request 80 | func (c *Client) Put(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 81 | clear := c.initOpts(opts...) 82 | defer clear() 83 | 84 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseUrl+endpoint, bytes.NewBuffer(c.body)) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | prepReq := c.prepareReq(req) 90 | return c.sendReq(ctx, prepReq) 91 | } 92 | 93 | // Patch func returns a request 94 | func (c *Client) Patch(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 95 | clear := c.initOpts(opts...) 96 | defer clear() 97 | 98 | req, err := http.NewRequestWithContext(ctx, http.MethodPatch, c.baseUrl+endpoint, bytes.NewBuffer(c.body)) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | prepReq := c.prepareReq(req) 104 | return c.sendReq(ctx, prepReq) 105 | } 106 | 107 | // Delete func returns a request 108 | func (c *Client) Delete(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 109 | clear := c.initOpts(opts...) 110 | defer clear() 111 | 112 | req, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.baseUrl+endpoint, bytes.NewBuffer(c.body)) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | prepReq := c.prepareReq(req) 118 | return c.sendReq(ctx, prepReq) 119 | } 120 | 121 | func (c *Client) Connect(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 122 | clear := c.initOpts(opts...) 123 | defer clear() 124 | 125 | req, err := http.NewRequestWithContext(ctx, http.MethodConnect, c.baseUrl+endpoint, nil) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | prepReq := c.prepareReq(req) 131 | return c.sendReq(ctx, prepReq) 132 | } 133 | 134 | func (c *Client) Options(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 135 | clear := c.initOpts(opts...) 136 | defer clear() 137 | 138 | req, err := http.NewRequestWithContext(ctx, http.MethodOptions, c.baseUrl+endpoint, nil) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | prepReq := c.prepareReq(req) 144 | return c.sendReq(ctx, prepReq) 145 | } 146 | 147 | func (c *Client) Trace(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { 148 | clear := c.initOpts(opts...) 149 | defer clear() 150 | 151 | req, err := http.NewRequestWithContext(ctx, http.MethodTrace, c.baseUrl+endpoint, nil) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | prepReq := c.prepareReq(req) 157 | return c.sendReq(ctx, prepReq) 158 | } 159 | 160 | // PrepareRequest func returns a request 161 | func (c *Client) PrepareRequest(ctx context.Context, method, endpoint string, opts ...Option) (*http.Request, error) { 162 | clear := c.initOpts(opts...) 163 | defer clear() 164 | 165 | req, err := http.NewRequestWithContext(ctx, method, c.baseUrl+endpoint, bytes.NewBuffer(c.body)) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | return c.prepareReq(req), nil 171 | } 172 | 173 | func (c *Client) initOpts(opts ...Option) func() { 174 | for _, opt := range opts { 175 | opt(c) 176 | } 177 | 178 | return func() { 179 | for key, header := range c.headers { 180 | if !header.IsDefault { 181 | delete(c.headers, key) 182 | } 183 | } 184 | 185 | c.query = make(map[string]string) 186 | c.body = nil 187 | } 188 | } 189 | 190 | func (c *Client) prepareReq(req *http.Request) *http.Request { 191 | // set headers 192 | for key, header := range c.headers { 193 | req.Header.Set(key, header.Value) 194 | } 195 | 196 | // set query 197 | q := req.URL.Query() 198 | for key, value := range c.query { 199 | q.Add(key, value) 200 | } 201 | 202 | req.URL.RawQuery = q.Encode() 203 | return req 204 | } 205 | 206 | func (c *Client) sendReq(ctx context.Context, req *http.Request) (*Response, error) { 207 | reqCtx, cancel := context.WithTimeout(ctx, c.timeout) 208 | defer cancel() 209 | 210 | res, err := c.httpClient.Do(req.WithContext(reqCtx)) 211 | if err != nil { 212 | return nil, errors.Wrap(err, "failed to send request") 213 | } 214 | 215 | defer res.Body.Close() 216 | body, err := ioutil.ReadAll(res.Body) 217 | if err != nil { 218 | return nil, errors.Wrap(err, "failed to read response body") 219 | } 220 | 221 | return &Response{res, body}, nil 222 | } 223 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #custom 2 | .env 3 | coverage.out 4 | coverage.txt 5 | 6 | # globs 7 | Makefile.in 8 | *.userprefs 9 | *.usertasks 10 | config.make 11 | config.status 12 | aclocal.m4 13 | install-sh 14 | autom4te.cache/ 15 | *.tar.gz 16 | tarballs/ 17 | test-results/ 18 | 19 | # Mac bundle stuff 20 | *.dmg 21 | *.app 22 | 23 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 24 | # General 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must end with two \r 30 | Icon 31 | 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear in the root of a volume 37 | .DocumentRevisions-V100 38 | .fseventsd 39 | .Spotlight-V100 40 | .TemporaryItems 41 | .Trashes 42 | .VolumeIcon.icns 43 | .com.apple.timemachine.donotpresent 44 | 45 | # Directories potentially created on remote AFP share 46 | .AppleDB 47 | .AppleDesktop 48 | Network Trash Folder 49 | Temporary Items 50 | .apdisk 51 | 52 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 53 | # Windows thumbnail cache files 54 | Thumbs.db 55 | ehthumbs.db 56 | ehthumbs_vista.db 57 | 58 | # Dump file 59 | *.stackdump 60 | 61 | # Folder config file 62 | [Dd]esktop.ini 63 | 64 | # Recycle Bin used on file shares 65 | $RECYCLE.BIN/ 66 | 67 | # Windows Installer files 68 | *.cab 69 | *.msi 70 | *.msix 71 | *.msm 72 | *.msp 73 | 74 | # Windows shortcuts 75 | *.lnk 76 | 77 | # content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 78 | ## Ignore Visual Studio temporary files, build results, and 79 | ## files generated by popular Visual Studio add-ons. 80 | ## 81 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 82 | 83 | # User-specific files 84 | *.suo 85 | *.user 86 | *.userosscache 87 | *.sln.docstates 88 | 89 | # User-specific files (MonoDevelop/Xamarin Studio) 90 | *.userprefs 91 | 92 | # Build results 93 | [Dd]ebug/ 94 | [Dd]ebugPublic/ 95 | [Rr]elease/ 96 | [Rr]eleases/ 97 | x64/ 98 | x86/ 99 | bld/ 100 | [Bb]in/ 101 | [Oo]bj/ 102 | [Ll]og/ 103 | 104 | # Visual Studio 2015/2017 cache/options directory 105 | .vs/ 106 | # Uncomment if you have tasks that create the project's static files in wwwroot 107 | #wwwroot/ 108 | 109 | # Visual Studio 2017 auto generated files 110 | Generated\ Files/ 111 | 112 | # MSTest test Results 113 | [Tt]est[Rr]esult*/ 114 | [Bb]uild[Ll]og.* 115 | 116 | # NUNIT 117 | *.VisualState.xml 118 | TestResult.xml 119 | 120 | # Build Results of an ATL Project 121 | [Dd]ebugPS/ 122 | [Rr]eleasePS/ 123 | dlldata.c 124 | 125 | # Benchmark Results 126 | BenchmarkDotNet.Artifacts/ 127 | 128 | # .NET Core 129 | project.lock.json 130 | project.fragment.lock.json 131 | artifacts/ 132 | 133 | # StyleCop 134 | StyleCopReport.xml 135 | 136 | # Files built by Visual Studio 137 | *_i.c 138 | *_p.c 139 | *_h.h 140 | *.ilk 141 | *.meta 142 | *.obj 143 | *.iobj 144 | *.pch 145 | *.pdb 146 | *.ipdb 147 | *.pgc 148 | *.pgd 149 | *.rsp 150 | *.sbr 151 | *.tlb 152 | *.tli 153 | *.tlh 154 | *.tmp 155 | *.tmp_proj 156 | *_wpftmp.csproj 157 | *.log 158 | *.vspscc 159 | *.vssscc 160 | .builds 161 | *.pidb 162 | *.svclog 163 | *.scc 164 | 165 | # Chutzpah Test files 166 | _Chutzpah* 167 | 168 | # Visual C++ cache files 169 | ipch/ 170 | *.aps 171 | *.ncb 172 | *.opendb 173 | *.opensdf 174 | *.sdf 175 | *.cachefile 176 | *.VC.db 177 | *.VC.VC.opendb 178 | 179 | # Visual Studio profiler 180 | *.psess 181 | *.vsp 182 | *.vspx 183 | *.sap 184 | 185 | # Visual Studio Trace Files 186 | *.e2e 187 | 188 | # TFS 2012 Local Workspace 189 | $tf/ 190 | 191 | # Guidance Automation Toolkit 192 | *.gpState 193 | 194 | # ReSharper is a .NET coding add-in 195 | _ReSharper*/ 196 | *.[Rr]e[Ss]harper 197 | *.DotSettings.user 198 | 199 | # JustCode is a .NET coding add-in 200 | .JustCode 201 | 202 | # TeamCity is a build add-in 203 | _TeamCity* 204 | 205 | # DotCover is a Code Coverage Tool 206 | *.dotCover 207 | 208 | # AxoCover is a Code Coverage Tool 209 | .axoCover/* 210 | !.axoCover/settings.json 211 | 212 | # Visual Studio code coverage results 213 | *.coverage 214 | *.coveragexml 215 | 216 | # NCrunch 217 | _NCrunch_* 218 | .*crunch*.local.xml 219 | nCrunchTemp_* 220 | 221 | # MightyMoose 222 | *.mm.* 223 | AutoTest.Net/ 224 | 225 | # Web workbench (sass) 226 | .sass-cache/ 227 | 228 | # Installshield output folder 229 | [Ee]xpress/ 230 | 231 | # DocProject is a documentation generator add-in 232 | DocProject/buildhelp/ 233 | DocProject/Help/*.HxT 234 | DocProject/Help/*.HxC 235 | DocProject/Help/*.hhc 236 | DocProject/Help/*.hhk 237 | DocProject/Help/*.hhp 238 | DocProject/Help/Html2 239 | DocProject/Help/html 240 | 241 | # Click-Once directory 242 | publish/ 243 | 244 | # Publish Web Output 245 | *.[Pp]ublish.xml 246 | *.azurePubxml 247 | # Note: Comment the next line if you want to checkin your web deploy settings, 248 | # but database connection strings (with potential passwords) will be unencrypted 249 | *.pubxml 250 | *.publishproj 251 | 252 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 253 | # checkin your Azure Web App publish settings, but sensitive information contained 254 | # in these scripts will be unencrypted 255 | PublishScripts/ 256 | 257 | # NuGet Packages 258 | *.nupkg 259 | # The packages folder can be ignored because of Package Restore 260 | **/[Pp]ackages/* 261 | # except build/, which is used as an MSBuild target. 262 | !**/[Pp]ackages/build/ 263 | # Uncomment if necessary however generally it will be regenerated when needed 264 | #!**/[Pp]ackages/repositories.config 265 | # NuGet v3's project.json files produces more ignorable files 266 | *.nuget.props 267 | *.nuget.targets 268 | 269 | # Microsoft Azure Build Output 270 | csx/ 271 | *.build.csdef 272 | 273 | # Microsoft Azure Emulator 274 | ecf/ 275 | rcf/ 276 | 277 | # Windows Store app package directories and files 278 | AppPackages/ 279 | BundleArtifacts/ 280 | Package.StoreAssociation.xml 281 | _pkginfo.txt 282 | *.appx 283 | 284 | # Visual Studio cache files 285 | # files ending in .cache can be ignored 286 | *.[Cc]ache 287 | # but keep track of directories ending in .cache 288 | !*.[Cc]ache/ 289 | 290 | # Others 291 | ClientBin/ 292 | ~$* 293 | *~ 294 | *.dbmdl 295 | *.dbproj.schemaview 296 | *.jfm 297 | *.pfx 298 | *.publishsettings 299 | orleans.codegen.cs 300 | 301 | # Including strong name files can present a security risk 302 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 303 | #*.snk 304 | 305 | # Since there are multiple workflows, uncomment next line to ignore bower_components 306 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 307 | #bower_components/ 308 | 309 | # RIA/Silverlight projects 310 | Generated_Code/ 311 | 312 | # Backup & report files from converting an old project file 313 | # to a newer Visual Studio version. Backup files are not needed, 314 | # because we have git ;-) 315 | _UpgradeReport_Files/ 316 | Backup*/ 317 | UpgradeLog*.XML 318 | UpgradeLog*.htm 319 | ServiceFabricBackup/ 320 | *.rptproj.bak 321 | 322 | # SQL Server files 323 | *.mdf 324 | *.ldf 325 | *.ndf 326 | 327 | # Business Intelligence projects 328 | *.rdl.data 329 | *.bim.layout 330 | *.bim_*.settings 331 | *.rptproj.rsuser 332 | 333 | # Microsoft Fakes 334 | FakesAssemblies/ 335 | 336 | # GhostDoc plugin setting file 337 | *.GhostDoc.xml 338 | 339 | # Node.js Tools for Visual Studio 340 | .ntvs_analysis.dat 341 | node_modules/ 342 | 343 | # Visual Studio 6 build log 344 | *.plg 345 | 346 | # Visual Studio 6 workspace options file 347 | *.opt 348 | 349 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 350 | *.vbw 351 | 352 | # Visual Studio LightSwitch build output 353 | **/*.HTMLClient/GeneratedArtifacts 354 | **/*.DesktopClient/GeneratedArtifacts 355 | **/*.DesktopClient/ModelManifest.xml 356 | **/*.Server/GeneratedArtifacts 357 | **/*.Server/ModelManifest.xml 358 | _Pvt_Extensions 359 | 360 | # Paket dependency manager 361 | .paket/paket.exe 362 | paket-files/ 363 | 364 | # FAKE - F# Make 365 | .fake/ 366 | 367 | # JetBrains Rider 368 | .idea/ 369 | *.sln.iml 370 | 371 | # CodeRush personal settings 372 | .cr/personal 373 | 374 | # Python Tools for Visual Studio (PTVS) 375 | __pycache__/ 376 | *.pyc 377 | 378 | # Cake - Uncomment if you are using it 379 | # tools/** 380 | # !tools/packages.config 381 | 382 | # Tabs Studio 383 | *.tss 384 | 385 | # Telerik's JustMock configuration file 386 | *.jmconfig 387 | 388 | # BizTalk build output 389 | *.btp.cs 390 | *.btm.cs 391 | *.odx.cs 392 | *.xsd.cs 393 | 394 | # OpenCover UI analysis results 395 | OpenCover/ 396 | 397 | # Azure Stream Analytics local run output 398 | ASALocalRun/ 399 | 400 | # MSBuild Binary and Structured Log 401 | *.binlog 402 | 403 | # NVidia Nsight GPU debugger configuration file 404 | *.nvuser 405 | 406 | # MFractors (Xamarin productivity tool) working folder 407 | .mfractor/ 408 | 409 | # Local History for Visual Studio 410 | .localhistory/ -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package gohttpclient 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type TestClientSuite struct { 14 | suite.Suite 15 | ctx context.Context 16 | } 17 | 18 | type TestMethod struct { 19 | name, baseUrl string 20 | method func(ctx context.Context, endpoint string, opts ...Option) (*Response, error) 21 | options []Option 22 | } 23 | 24 | func TestClient(t *testing.T) { 25 | suite.Run(t, new(TestClientSuite)) 26 | } 27 | 28 | func (s *TestClientSuite) SetupSuite() { 29 | s.ctx = context.Background() 30 | } 31 | 32 | func (s *TestClientSuite) Test_New_ShouldRunSuccesfully() { 33 | // Arrange 34 | baseUrl := "http://localhost:8080" 35 | 36 | // Act 37 | client := New(baseUrl) 38 | 39 | // Assert 40 | s.NotNil(client) 41 | } 42 | 43 | func (s *TestClientSuite) Test_Request_WhenRequestIsInvalid_ShouldReturnError() { 44 | // Arrange 45 | baseUrl := "http://localhost:8080" 46 | client := New(baseUrl) 47 | requests := []TestMethod{ 48 | { 49 | name: "GET", 50 | baseUrl: baseUrl, 51 | method: client.Get, 52 | }, 53 | { 54 | name: "POST", 55 | baseUrl: baseUrl, 56 | method: client.Post, 57 | }, 58 | { 59 | name: "PUT", 60 | baseUrl: baseUrl, 61 | method: client.Put, 62 | }, 63 | { 64 | name: "PATCH", 65 | baseUrl: baseUrl, 66 | method: client.Patch, 67 | }, 68 | { 69 | name: "DELETE", 70 | baseUrl: baseUrl, 71 | method: client.Delete, 72 | }, 73 | { 74 | name: "CONNECT", 75 | baseUrl: baseUrl, 76 | method: client.Connect, 77 | }, 78 | { 79 | name: "OPTIONS", 80 | baseUrl: baseUrl, 81 | method: client.Options, 82 | }, 83 | { 84 | name: "TRACE", 85 | baseUrl: baseUrl, 86 | method: client.Trace, 87 | }, 88 | } 89 | 90 | for _, req := range requests { 91 | s.Suite.Run(req.name, func() { 92 | // Act 93 | response, err := req.method(nil, "") 94 | 95 | // Assert 96 | s.Nil(response) 97 | s.Error(err) 98 | }) 99 | } 100 | } 101 | 102 | func (s *TestClientSuite) Test_Request_WhenDoReturnsAnError_ShouldReturnError() { 103 | // Arrange 104 | baseUrlWithInvalidSchema := "htt \\`" 105 | client := New(baseUrlWithInvalidSchema) 106 | requests := []TestMethod{ 107 | { 108 | name: "GET", 109 | baseUrl: baseUrlWithInvalidSchema, 110 | method: client.Get, 111 | }, 112 | { 113 | name: "POST", 114 | baseUrl: baseUrlWithInvalidSchema, 115 | method: client.Post, 116 | }, 117 | { 118 | name: "PUT", 119 | baseUrl: baseUrlWithInvalidSchema, 120 | method: client.Put, 121 | }, 122 | { 123 | name: "PATCH", 124 | baseUrl: baseUrlWithInvalidSchema, 125 | method: client.Patch, 126 | }, 127 | { 128 | name: "DELETE", 129 | baseUrl: baseUrlWithInvalidSchema, 130 | method: client.Delete, 131 | }, 132 | { 133 | name: "CONNECT", 134 | baseUrl: baseUrlWithInvalidSchema, 135 | method: client.Connect, 136 | }, 137 | { 138 | name: "OPTIONS", 139 | baseUrl: baseUrlWithInvalidSchema, 140 | method: client.Options, 141 | }, 142 | { 143 | name: "TRACE", 144 | baseUrl: baseUrlWithInvalidSchema, 145 | method: client.Trace, 146 | }, 147 | } 148 | 149 | for _, req := range requests { 150 | s.Suite.Run(req.name, func() { 151 | // Act 152 | response, err := req.method(s.ctx, "") 153 | 154 | // Assert 155 | s.Nil(response) 156 | s.Error(err) 157 | }) 158 | } 159 | } 160 | 161 | func (s *TestClientSuite) Test_Request_WhenBodyReturnsError_ShouldReturnError() { 162 | // Arrange 163 | svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 164 | w.Header().Set("Content-Length", "1") 165 | w.Write(nil) 166 | })) 167 | defer svc.Close() 168 | 169 | client := New(svc.URL) 170 | requests := []TestMethod{ 171 | { 172 | name: "GET", 173 | baseUrl: svc.URL, 174 | method: client.Get, 175 | }, 176 | { 177 | name: "POST", 178 | baseUrl: svc.URL, 179 | method: client.Post, 180 | }, 181 | { 182 | name: "PUT", 183 | baseUrl: svc.URL, 184 | method: client.Put, 185 | }, 186 | { 187 | name: "PATCH", 188 | baseUrl: svc.URL, 189 | method: client.Patch, 190 | }, 191 | { 192 | name: "DELETE", 193 | baseUrl: svc.URL, 194 | method: client.Delete, 195 | }, 196 | { 197 | name: "CONNECT", 198 | baseUrl: svc.URL, 199 | method: client.Connect, 200 | }, 201 | { 202 | name: "OPTIONS", 203 | baseUrl: svc.URL, 204 | method: client.Options, 205 | }, 206 | { 207 | name: "TRACE", 208 | baseUrl: svc.URL, 209 | method: client.Trace, 210 | }, 211 | } 212 | 213 | for _, req := range requests { 214 | s.Suite.Run(req.name, func() { 215 | // Act 216 | response, err := req.method(s.ctx, "") 217 | 218 | // Assert 219 | s.Nil(response) 220 | s.Error(err) 221 | }) 222 | } 223 | } 224 | 225 | func (s *TestClientSuite) Test_Request_ShouldRunSuccesfully() { 226 | // Arrange 227 | svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 228 | w.WriteHeader(http.StatusOK) 229 | })) 230 | defer svc.Close() 231 | 232 | client := New(svc.URL) 233 | requests := []TestMethod{ 234 | { 235 | name: "GET", 236 | baseUrl: svc.URL, 237 | method: client.Get, 238 | }, 239 | { 240 | name: "POST", 241 | baseUrl: svc.URL, 242 | method: client.Post, 243 | }, 244 | { 245 | name: "PUT", 246 | baseUrl: svc.URL, 247 | method: client.Put, 248 | }, 249 | { 250 | name: "PATCH", 251 | baseUrl: svc.URL, 252 | method: client.Patch, 253 | }, 254 | { 255 | name: "DELETE", 256 | baseUrl: svc.URL, 257 | method: client.Delete, 258 | }, 259 | { 260 | name: "CONNECT", 261 | baseUrl: svc.URL, 262 | method: client.Connect, 263 | }, 264 | { 265 | name: "OPTIONS", 266 | baseUrl: svc.URL, 267 | method: client.Options, 268 | }, 269 | { 270 | name: "TRACE", 271 | baseUrl: svc.URL, 272 | method: client.Trace, 273 | }, 274 | } 275 | 276 | for _, req := range requests { 277 | s.Suite.Run(req.name, func() { 278 | // Act 279 | response, err := req.method(s.ctx, "") 280 | 281 | // Assert 282 | s.NotNil(response) 283 | s.NoError(err) 284 | }) 285 | } 286 | } 287 | 288 | func (s *TestClientSuite) Test_Request_WithOptions_ShouldRunSuccesfully() { 289 | // Arrange 290 | // init test server 291 | svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 292 | w.WriteHeader(http.StatusOK) 293 | })) 294 | defer svc.Close() 295 | 296 | client := New(svc.URL) 297 | requests := []TestMethod{ 298 | { 299 | name: "GET", 300 | baseUrl: svc.URL, 301 | method: client.Get, 302 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 303 | }, 304 | { 305 | name: "POST", 306 | baseUrl: svc.URL, 307 | method: client.Post, 308 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 309 | }, 310 | { 311 | name: "PUT", 312 | baseUrl: svc.URL, 313 | method: client.Put, 314 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 315 | }, 316 | { 317 | name: "PATCH", 318 | baseUrl: svc.URL, 319 | method: client.Patch, 320 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 321 | }, 322 | { 323 | name: "DELETE", 324 | baseUrl: svc.URL, 325 | method: client.Delete, 326 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 327 | }, 328 | { 329 | name: "CONNECT", 330 | baseUrl: svc.URL, 331 | method: client.Connect, 332 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 333 | }, 334 | { 335 | name: "OPTIONS", 336 | baseUrl: svc.URL, 337 | method: client.Options, 338 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 339 | }, 340 | { 341 | name: "TRACE", 342 | baseUrl: svc.URL, 343 | method: client.Trace, 344 | options: []Option{WithHeader("key", "value"), WithQuery("key", "value")}, 345 | }, 346 | } 347 | 348 | for _, req := range requests { 349 | s.Suite.Run(req.name, func() { 350 | // Act 351 | response, err := req.method(s.ctx, "", req.options...) 352 | 353 | // Assert 354 | s.NotNil(response) 355 | s.NoError(err) 356 | }) 357 | } 358 | } 359 | 360 | func (s *TestClientSuite) Test_PrepareRequest_WhenRequestIsInvalid_ShouldReturnError() { 361 | // Arrange 362 | baseUrl := "http://localhost:8080" 363 | method := "GET" 364 | endpoint := "/test" 365 | client := New(baseUrl) 366 | 367 | // Act 368 | response, err := client.PrepareRequest(nil, method, endpoint) 369 | 370 | // Assert 371 | s.Nil(response) 372 | s.Error(err) 373 | } 374 | 375 | func (s *TestClientSuite) Test_PrepareRequest_ShouldRunSuccesfully() { 376 | // Arrange 377 | baseUrl := "http://localhost:8080" 378 | method := "GET" 379 | endpoint := "/test" 380 | body := []byte("test") 381 | client := New(baseUrl) 382 | 383 | // Act 384 | request, err := client.PrepareRequest(s.ctx, method, endpoint, WithBody(body)) 385 | 386 | // Assert 387 | s.NoError(err) 388 | s.NotNil(request) 389 | s.Equal(method, request.Method) 390 | s.Equal(baseUrl+endpoint, request.URL.String()) 391 | 392 | requestBody, err := ioutil.ReadAll(request.Body) 393 | s.NoError(err) 394 | s.Equal(body, requestBody) 395 | } 396 | --------------------------------------------------------------------------------