diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 06b1f14..9acf314 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,10 +21,28 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.22.x + go-version: 1.23.x - uses: actions/checkout@v2 - name: golangci-lint - uses: golangci/golangci-lint-action@v6.0.1 + uses: golangci/golangci-lint-action@v6.1.0 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.59.1 + version: v1.61.0 + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the action will use pre-installed Go. + # skip-go-installation: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true \ No newline at end of file diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml index 6356a9d..c031db4 100644 --- a/.github/workflows/gorelease.yml +++ b/.github/workflows/gorelease.yml @@ -9,7 +9,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: 1.22.x + GO_VERSION: 1.23.x jobs: gorelease: runs-on: ubuntu-latest diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 7eb27c9..aa4c2ee 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -21,7 +21,7 @@ jobs: test: strategy: matrix: - go-version: [ 1.13.x, 1.21.x, 1.22.x ] + go-version: [ 1.13.x, 1.22.x, 1.23.x ] runs-on: ubuntu-latest steps: - name: Install Go stable diff --git a/.golangci.yml b/.golangci.yml index d86a0ba..f11affb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,7 +36,6 @@ linters: - ireturn - exhaustruct - nonamedreturns - - structcheck - testableexamples - dupword - depguard @@ -51,21 +50,23 @@ issues: - linters: - staticcheck path: ".go" - text: "\"io/ioutil\" has been deprecated since Go 1.16" # Keeping backwards compatibility with go1.13. + text: "\"io/ioutil\" has been deprecated since" # Keeping backwards compatibility with go1.13. - linters: - gomnd + - mnd - goconst - - goerr113 - noctx - funlen - dupl - structcheck - unused - unparam - - nosnakecase path: "_test.go" - linters: - errcheck # Error checking omitted for brevity. - gosec path: "example_" + - linters: + - revive + text: "unused-parameter: parameter" diff --git a/Makefile b/Makefile index 80527a2..853cc8c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -#GOLANGCI_LINT_VERSION := "v1.59.1" # Optional configuration to pinpoint golangci-lint version. +#GOLANGCI_LINT_VERSION := "v1.61.0" # Optional configuration to pinpoint golangci-lint version. # The head of Makefile determines location of dev-go to include standard targets. GO ?= go diff --git a/client.go b/client.go index ff9836e..f38fa51 100644 --- a/client.go +++ b/client.go @@ -37,9 +37,13 @@ type Client struct { ctx context.Context //nolint:containedctx // Context is configured separately. + req *http.Request resp *http.Response respBody []byte + attempt int + retryDelays []time.Duration + reqHeaders map[string]string reqCookies map[string]string reqQueryParams url.Values @@ -114,6 +118,38 @@ func NewClient(baseURL string) *Client { return c } +// HTTPValue contains information about request and response. +type HTTPValue struct { + Req *http.Request + ReqBody []byte + + Resp *http.Response + RespBody []byte + + OtherResp *http.Response + OtherRespBody []byte + + Attempt int + RetryDelays []time.Duration +} + +// Details returns HTTP request and response information of last run. +func (c *Client) Details() HTTPValue { + return HTTPValue{ + Req: c.req, + ReqBody: c.reqBody, + + Resp: c.resp, + RespBody: c.respBody, + + OtherResp: c.otherResp, + OtherRespBody: c.otherRespBody, + + Attempt: c.attempt, + RetryDelays: c.retryDelays, + } +} + // SetBaseURL changes baseURL configured with constructor. func (c *Client) SetBaseURL(baseURL string) { if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { @@ -132,6 +168,8 @@ func (c *Client) Reset() *Client { c.reqQueryParams = map[string][]string{} c.reqFormDataParams = map[string][]string{} + c.req = nil + c.resp = nil c.respBody = nil @@ -147,6 +185,9 @@ func (c *Client) Reset() *Client { c.otherRespBody = nil c.otherRespExpected = false + c.attempt = 0 + c.retryDelays = nil + return c } @@ -158,7 +199,7 @@ func (c *Client) Reset() *Client { // This method enables context-driven concurrent access to shared base Client. func (c *Client) Fork(ctx context.Context) (context.Context, *Client) { // Pointer to current Client is used as context key - // to enable multiple different clients in sam context. + // to enable multiple different clients in same context. if fc, ok := ctx.Value(c).(*Client); ok { return ctx, fc } @@ -261,7 +302,9 @@ func (c *Client) WithURLEncodedFormDataParam(name, value string) *Client { return c } -func (c *Client) do() (err error) { +func (c *Client) do() (err error) { //nolint:funlen + c.attempt++ + if c.reqConcurrency < 1 { c.reqConcurrency = 1 } @@ -289,7 +332,7 @@ func (c *Client) do() (err error) { wg.Done() }() - resp, er := c.doOnce() + req, resp, er := c.doOnce() if er != nil { return } @@ -305,6 +348,10 @@ func (c *Client) do() (err error) { } mu.Lock() + if c.req == nil { + c.req = req + } + if _, ok := statusCodeCount[resp.StatusCode]; !ok { resps[resp.StatusCode] = resp bodies[resp.StatusCode] = body @@ -315,6 +362,7 @@ func (c *Client) do() (err error) { mu.Unlock() }() } + wg.Wait() if err != nil { @@ -329,6 +377,14 @@ func (c *Client) expectResp(check func() error) (err error) { return check() } + if len(c.reqBody) == 0 && len(c.reqFormDataParams) > 0 { + c.reqBody = []byte(c.reqFormDataParams.Encode()) + + if c.reqMethod == "" { + c.reqMethod = http.MethodPost + } + } + if c.retryBackOff != nil { for { if err = c.do(); err == nil { @@ -343,6 +399,8 @@ func (c *Client) expectResp(check func() error) (err error) { return err } + c.retryDelays = append(c.retryDelays, dur) + time.Sleep(dur) } } @@ -433,15 +491,17 @@ func (c *Client) buildURI() (string, error) { return uri, nil } +type readSeekNopCloser struct { + io.ReadSeeker +} + +func (r *readSeekNopCloser) Close() error { + return nil +} + func (c *Client) buildBody() io.Reader { if len(c.reqBody) > 0 { - return bytes.NewBuffer(c.reqBody) - } else if len(c.reqFormDataParams) > 0 { - if c.reqMethod == "" { - c.reqMethod = http.MethodPost - } - - return strings.NewReader(c.reqFormDataParams.Encode()) + return &readSeekNopCloser{ReadSeeker: bytes.NewReader(c.reqBody)} } return nil @@ -486,17 +546,17 @@ func (c *Client) applyCookies(req *http.Request) { } } -func (c *Client) doOnce() (*http.Response, error) { +func (c *Client) doOnce() (*http.Request, *http.Response, error) { uri, err := c.buildURI() if err != nil { - return nil, err + return nil, nil, err } body := c.buildBody() req, err := http.NewRequestWithContext(c.ctx, c.reqMethod, uri, body) if err != nil { - return nil, err + return nil, nil, err } c.applyHeaders(req) @@ -513,10 +573,14 @@ func (c *Client) doOnce() (*http.Response, error) { cl.Transport = tr cl.Jar = j - return cl.Do(req) + resp, err := cl.Do(req) + + return req, resp, err } - return tr.RoundTrip(req) + resp, err := tr.RoundTrip(req) + + return req, resp, err } // ExpectResponseStatus sets expected response status code. diff --git a/client_test.go b/client_test.go index 15d612d..5eac023 100644 --- a/client_test.go +++ b/client_test.go @@ -104,6 +104,17 @@ func TestNewClient(t *testing.T) { val, found := vars.Get("$var1") assert.True(t, found) assert.Equal(t, "abc", val) + + details := c.Details() + assert.NotNil(t, details.Req) + assert.NotNil(t, details.Resp) + assert.NotNil(t, details.OtherResp) + + assert.Equal(t, http.StatusAccepted, details.Resp.StatusCode) + assert.Equal(t, http.StatusConflict, details.OtherResp.StatusCode) + + assert.Equal(t, 1, details.Attempt) + assert.Empty(t, details.RetryDelays) } func TestNewClient_failedExpectation(t *testing.T) { diff --git a/go.mod b/go.mod index ecb5fc6..9d6a00f 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/bool64/httpmock go 1.18 require ( - github.com/bool64/dev v0.2.35 + github.com/bool64/dev v0.2.36 github.com/bool64/shared v0.1.5 github.com/stretchr/testify v1.8.2 - github.com/swaggest/assertjson v1.8.1 + github.com/swaggest/assertjson v1.9.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/iancoleman/orderedmap v0.2.0 // indirect + github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index f69362d..8129116 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bool64/dev v0.2.35 h1:M17TLsO/pV2J7PYI/gpe3Ua26ETkzZGb+dC06eoMqlk= -github.com/bool64/dev v0.2.35/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/dev v0.2.36 h1:yU3bbOTujoxhWnt8ig8t94PVmZXIkCaRj9C57OtqJBY= +github.com/bool64/dev v0.2.36/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,8 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= -github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -33,8 +33,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/swaggest/assertjson v1.8.1 h1:Be2EHY9S2qwKWV+xWZB747Cd7Y79YK6JLdeyrgFvyMo= -github.com/swaggest/assertjson v1.8.1/go.mod h1:/8kNRmDZAZfavS5VeWYtCimLGebn0Ak1/iErFUi+DEM= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff h1:7YqG491bE4vstXRz1lD38rbSgbXnirvROz1lZiOnPO8= github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=