diff --git a/.golangci.yml b/.golangci.yml index 69cd3dd61..dad398b6b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -26,7 +26,9 @@ linters-settings: - name: error-return - name: error-strings - name: error-naming + - name: errorf - name: exported + - name: indent-error-flow - name: if-return - name: increment-decrement - name: var-naming @@ -40,7 +42,9 @@ linters-settings: - name: errorf - name: empty-block - name: superfluous-else + - name: struct-tag - name: unused-parameter + - name: unused-receiver - name: unreachable-code - name: redefines-builtin-id tagliatelle: @@ -56,54 +60,79 @@ linters: # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint disable-all: true enable: - - bodyclose - - deadcode - - depguard - - dogsled - - errcheck - - exhaustive - - exportloopref - - gochecknoinits - - gocritic - - gocyclo + - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] + - deadcode # Finds unused code [fast: false, auto-fix: false] + - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: false, auto-fix: false] + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] + - exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] + - exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false] + - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] + - gocritic # Provides many diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false] + - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - godot # Check if comments end in a period [fast: true, auto-fix: true] - - gofmt - - goimports - - goprintffuncname - - gosec - - gosimple - - govet - - ineffassign - - misspell - - nakedret - - nolintlint + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] + - goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false] + - gosec # Inspects source code for security problems [fast: false, auto-fix: false] + - gosimple # Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] + - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] + - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] + - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] + - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] - - rowserrcheck - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - varcheck - - whitespace + - rowserrcheck # checks whether Err of rows is checked successfully [fast: false, auto-fix: false] + - staticcheck #megacheck): Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] + - structcheck # Finds unused struct fields [fast: false, auto-fix: false] + - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false] + - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] + - unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false] + - unparam # Reports unused function parameters [fast: false, auto-fix: false] + - unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] + - varcheck # Finds unused global variables and constants [fast: false, auto-fix: false] + - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] # don't enable: - # - asciicheck - # - dupl - # - funlen - # - gochecknoglobals - # - gocognit - # - goconst - # - godot - # - godox - # - goerr113 - # - gomnd - # - lll - # - maligned - # - nestif - # - noctx - # - prealloc - # - testpackage - # - wsl +# - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] +# - cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false] +# - dupl # Tool for code clone detection [fast: true, auto-fix: false] +# - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false] +# - exhaustivestruct # Checks if all struct's fields are initialized [fast: false, auto-fix: false] +# - forbidigo # Forbids identifiers [fast: true, auto-fix: false] +# - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false] +# - funlen # Tool for detection of long functions [fast: true, auto-fix: false] +# - gci # Gci control golang package import order and make it always deterministic. [fast: true, auto-fix: true] +# - gochecknoglobals # check that no global variables exist [fast: true, auto-fix: false] +# - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] +# - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] +# - godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] +# - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] +# - goerr113 # Golang linter to check the errors handling expressions [fast: false, auto-fix: false] +# - gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true] +# - goheader # Checks is file header matches to pattern [fast: true, auto-fix: false] +# - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] +# - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] +# - ifshort # Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false] +# - importas # Enforces consistent import aliases [fast: false, auto-fix: false] +# - interfacer # Linter that suggests narrower interface types [fast: false, auto-fix: false] +# - lll # Reports long lines [fast: true, auto-fix: false] +# - makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] +# - maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: false, auto-fix: false] +# - nestif # Reports deeply nested if statements [fast: true, auto-fix: false] +# - nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] +# - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] +# - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false] +# - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test [fast: true, auto-fix: false] +# - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] +# - predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false] +# - promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false] +# - scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] +# - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false] +# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false] +# - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false] +# - wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false] +# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] +# - wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] diff --git a/mongodbatlas/mongodbatlas.go b/mongodbatlas/mongodbatlas.go index ca5a018ad..0cf2ae527 100644 --- a/mongodbatlas/mongodbatlas.go +++ b/mongodbatlas/mongodbatlas.go @@ -169,24 +169,6 @@ type ListOptions struct { ItemsPerPage int `url:"itemsPerPage,omitempty"` } -// ErrorResponse reports the error caused by an API request. -type ErrorResponse struct { - // HTTP response that caused this error - Response *http.Response - - // The error code as specified in https://docs.atlas.mongodb.com/reference/api/api-errors/ - ErrorCode string `json:"errorCode"` - - // HTTP status code. - HTTPCode int `json:"error"` - - // A short description of the error, which is simply the HTTP status phrase. - Reason string `json:"reason"` - - // A more detailed description of the error. - Detail string `json:"detail,omitempty"` -} - func (resp *Response) getCurrentPageLink() (*Link, error) { if link := resp.getLinkByRef("self"); link != nil { return link, nil @@ -222,7 +204,7 @@ func (resp *Response) CurrentPage() (int, error) { pageNum, err := strconv.Atoi(pageNumStr) if err != nil { - return 0, fmt.Errorf("error getting current page: %s", err) + return 0, fmt.Errorf("error getting current page: %w", err) } return pageNum, nil @@ -360,7 +342,7 @@ func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body int } // newEncodedBody returns an ReadWriter object containing the body of the http request. -func (c *Client) newEncodedBody(body interface{}) (io.Reader, error) { +func newEncodedBody(body interface{}) (io.Reader, error) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) @@ -407,7 +389,7 @@ func (c *Client) newRequest(ctx context.Context, urlStr, method string, body int var buf io.Reader if body != nil { - if buf, err = c.newEncodedBody(body); err != nil { + if buf, err = newEncodedBody(body); err != nil { return nil, err } } @@ -476,7 +458,7 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res } } else { decErr := json.NewDecoder(resp.Body).Decode(v) - if decErr == io.EOF { + if errors.Is(decErr, io.EOF) { decErr = nil // ignore EOF errors caused by empty response body } if decErr != nil { @@ -488,11 +470,35 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res return response, err } +// ErrorResponse reports the error caused by an API request. +type ErrorResponse struct { + // Response that caused this error + Response *http.Response + // ErrorCode is the code as specified in https://docs.atlas.mongodb.com/reference/api/api-errors/ + ErrorCode string `json:"errorCode"` + // HTTPCode status code. + HTTPCode int `json:"error"` + // Reason is short description of the error, which is simply the HTTP status phrase. + Reason string `json:"reason"` + // Detail is more detailed description of the error. + Detail string `json:"detail,omitempty"` +} + func (r *ErrorResponse) Error() string { return fmt.Sprintf("%v %v: %d (request %q) %v", r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.ErrorCode, r.Detail) } +func (r *ErrorResponse) Is(target error) bool { + var v *ErrorResponse + + return errors.As(target, &v) && + r.ErrorCode == v.ErrorCode && + r.HTTPCode == v.HTTPCode && + r.Reason == v.Reason && + r.Detail == v.Detail +} + // CheckResponse checks the API response for errors, and returns them if present. A response is considered an // error if it has a status code outside the 200 range. API error responses are expected to have either no response // body, or a JSON response body that maps to ErrorResponse. Any other response body will be silently ignored. diff --git a/mongodbatlas/mongodbatlas_test.go b/mongodbatlas/mongodbatlas_test.go index 3ee7625c3..5585fc3f0 100644 --- a/mongodbatlas/mongodbatlas_test.go +++ b/mongodbatlas/mongodbatlas_test.go @@ -17,6 +17,7 @@ package mongodbatlas import ( "context" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -73,49 +74,41 @@ func setup() (client *Client, mux *http.ServeMux, teardown func()) { } func testMethod(t *testing.T, r *http.Request, expected string) { + t.Helper() if expected != r.Method { t.Errorf("Request method = %v, expected %v", r.Method, expected) } } func testURLParseError(t *testing.T, err error) { + t.Helper() if err == nil { t.Errorf("Expected error to be returned") } - if err, ok := err.(*url.Error); !ok || err.Op != "parse" { + var target *url.Error + if errors.As(err, &target) && target.Op != "parse" { t.Errorf("Expected URL parse error, got %+v", err) } } -func testClientServices(t *testing.T, c *Client) { - services := []string{} - - cp := reflect.ValueOf(c) - cv := reflect.Indirect(cp) - - for _, s := range services { - if cv.FieldByName(s).IsNil() { - t.Errorf("c.%s shouldn't be nil", s) - } - } -} - func testClientDefaultBaseURL(t *testing.T, c *Client) { + t.Helper() if c.BaseURL == nil || c.BaseURL.String() != defaultBaseURL { t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL, defaultBaseURL) } } func testClientDefaultUserAgent(t *testing.T, c *Client) { + t.Helper() if c.UserAgent != userAgent { t.Errorf("NewClient UserAgent = %v, expected %v", c.UserAgent, userAgent) } } func testClientDefaults(t *testing.T, c *Client) { + t.Helper() testClientDefaultBaseURL(t, c) testClientDefaultUserAgent(t, c) - testClientServices(t, c) } func TestNewClient(t *testing.T) { @@ -407,7 +400,8 @@ func TestDo_redirectLoop(t *testing.T) { if err == nil { t.Error("Expected error to be returned.") } - if err, ok := err.(*url.Error); !ok { + var target *url.Error + if !errors.As(err, &target) { t.Errorf("Expected a URL error; got %#v.", err) } } @@ -439,9 +433,8 @@ func TestCheckResponse(t *testing.T) { ), ), } - err := CheckResponse(res).(*ErrorResponse) - - if err == nil { + var target *ErrorResponse + if !errors.As(CheckResponse(res), &target) { t.Fatalf("Expected error response.") } @@ -452,8 +445,8 @@ func TestCheckResponse(t *testing.T) { Reason: "Conflict", Detail: `A group with name "Test" already exists`, } - if !reflect.DeepEqual(err, expected) { - t.Errorf("Error = %#v, expected %#v", err, expected) + if !errors.Is(target, expected) { + t.Errorf("Got = %#v, expected %#v", target, expected) } } @@ -465,17 +458,16 @@ func TestCheckResponse_noBody(t *testing.T) { StatusCode: http.StatusBadRequest, Body: ioutil.NopCloser(strings.NewReader("")), } - err := CheckResponse(res).(*ErrorResponse) - - if err == nil { + var target *ErrorResponse + if !errors.As(CheckResponse(res), &target) { t.Errorf("Expected error response.") } expected := &ErrorResponse{ Response: res, } - if !reflect.DeepEqual(err, expected) { - t.Errorf("Error = %#v, expected %#v", err, expected) + if !errors.Is(target, expected) { + t.Errorf("Got = %#v, expected %#v", target, expected) } } @@ -563,6 +555,7 @@ func TestCustomBaseURL_badURL(t *testing.T) { } func checkCurrentPage(t *testing.T, resp *Response, expectedPage int) { //nolint:unparam // currently we always use expectedPage with value 2 but that may change + t.Helper() p, err := resp.CurrentPage() if err != nil { t.Fatal(err)