From c249866c7ba2a88f8c10c63b1cb2e953d2883406 Mon Sep 17 00:00:00 2001 From: Michail Safronov Date: Mon, 25 Mar 2024 13:11:08 +0500 Subject: [PATCH] feat(render,find): strip private info from returned errors info --- cmd/carbonapi/http/find_handlers.go | 3 +- cmd/carbonapi/http/helper.go | 62 ++++++- cmd/carbonapi/http/render_handler.go | 2 +- cmd/mockbackend/e2etesting.go | 6 +- .../testcases/find_error/find_error.yaml | 2 + .../render_error_all/render_error_all.yaml | 4 + .../render_error_all_rr.yaml | 3 + .../render_error_refused/carbonapi.yaml | 57 ++++++ .../render_error_refused.yaml | 43 +++++ zipper/helper/requests.go | 101 ++++++++--- zipper/helper/requests_test.go | 169 +++++++++++++++++- 11 files changed, 416 insertions(+), 36 deletions(-) create mode 100644 cmd/mockbackend/testcases/render_error_refused/carbonapi.yaml create mode 100644 cmd/mockbackend/testcases/render_error_refused/render_error_refused.yaml diff --git a/cmd/carbonapi/http/find_handlers.go b/cmd/carbonapi/http/find_handlers.go index 298cae2c4..b84d318e0 100644 --- a/cmd/carbonapi/http/find_handlers.go +++ b/cmd/carbonapi/http/find_handlers.go @@ -23,6 +23,7 @@ import ( "github.com/go-graphite/carbonapi/date" "github.com/go-graphite/carbonapi/intervalset" utilctx "github.com/go-graphite/carbonapi/util/ctx" + "github.com/go-graphite/carbonapi/zipper/helper" ) // Find handler and it's helper functions @@ -283,7 +284,7 @@ func findHandler(w http.ResponseWriter, r *http.Request) { if returnCode < 300 { multiGlobs = &pbv3.MultiGlobResponse{Metrics: []pbv3.GlobResponse{}} } else { - setError(w, &accessLogDetails, http.StatusText(returnCode), returnCode, uid.String()) + setError(w, &accessLogDetails, helper.MerryRootError(err), returnCode, uid.String()) // We don't want to log this as an error if it's something normal // Normal is everything that is >= 500. So if config.Config.NotFoundStatusCode is 500 - this will be // logged as error diff --git a/cmd/carbonapi/http/helper.go b/cmd/carbonapi/http/helper.go index d35f5a56b..3febd4371 100644 --- a/cmd/carbonapi/http/helper.go +++ b/cmd/carbonapi/http/helper.go @@ -2,6 +2,7 @@ package http import ( "fmt" + "html" "net/http" "strings" "time" @@ -229,6 +230,46 @@ func splitRemoteAddr(addr string) (string, string) { return tmp[0], tmp[1] } +// stripError for strip network errors (ip and other private info) +func stripError(err string) string { + k, v, ok := strings.Cut(err, ": ") + if ok { + if strings.Contains(v, "connection refused") { + return html.EscapeString(k) + ": connection refused" + } else if strings.Contains(v, " lookup ") { + return html.EscapeString(k) + ": lookup error" + } else if strings.Contains(v, "broken pipe") { + return html.EscapeString(k) + ": broken pipe" + } else if strings.Contains(v, " connection reset ") { + return html.EscapeString(k) + ": connection reset" + } + } + return html.EscapeString(err) +} + +func joinErrors(errs []string, sep string, status int) (msg, reason string) { + if len(errs) == 0 { + msg = http.StatusText(status) + } else { + n := len(sep) * (len(errs) - 1) + for i := 0; i < len(errs); i++ { + n += len(errs[i]) + } + + var b strings.Builder + b.Grow(n) + b.WriteString(stripError(errs[0])) + for _, s := range errs[1:] { + b.WriteString(sep) + b.WriteString(stripError(s)) + } + + reason = b.String() + msg = reason + } + return +} + func buildParseErrorString(target, e string, err error) string { msg := fmt.Sprintf("%s\n\n%-20s: %s\n", http.StatusText(http.StatusBadRequest), "Target", target) if err != nil { @@ -285,9 +326,28 @@ func timestampTruncate(ts int64, duration time.Duration, durations []config.Dura func setError(w http.ResponseWriter, accessLogDetails *carbonapipb.AccessLogDetails, msg string, status int, carbonapiUUID string) { w.Header().Set(ctxHeaderUUID, carbonapiUUID) - http.Error(w, http.StatusText(status)+": "+msg, status) + if msg == "" { + msg = http.StatusText(status) + } accessLogDetails.Reason = msg accessLogDetails.HTTPCode = int32(status) + msg = html.EscapeString(stripError(msg)) + http.Error(w, msg, status) +} + +func setErrors(w http.ResponseWriter, accessLogDetails *carbonapipb.AccessLogDetails, errMsgs []string, status int, carbonapiUUID string) { + w.Header().Set(ctxHeaderUUID, carbonapiUUID) + var msg string + if status != http.StatusOK { + if len(errMsgs) == 0 { + msg = http.StatusText(status) + accessLogDetails.Reason = msg + } else { + msg, accessLogDetails.Reason = joinErrors(errMsgs, "\r\n", status) + } + } + accessLogDetails.HTTPCode = int32(status) + http.Error(w, msg, status) } func queryLengthLimitExceeded(query []string, maxLength uint64) bool { diff --git a/cmd/carbonapi/http/render_handler.go b/cmd/carbonapi/http/render_handler.go index f2938527c..83feafcbb 100644 --- a/cmd/carbonapi/http/render_handler.go +++ b/cmd/carbonapi/http/render_handler.go @@ -362,7 +362,7 @@ func renderHandler(w http.ResponseWriter, r *http.Request) { } if returnCode == http.StatusBadRequest || returnCode == http.StatusNotFound || returnCode == http.StatusForbidden || returnCode >= 500 { - setError(w, accessLogDetails, strings.Join(errMsgs, ","), returnCode, uid.String()) + setErrors(w, accessLogDetails, errMsgs, returnCode, uid.String()) logAsError = true return } diff --git a/cmd/mockbackend/e2etesting.go b/cmd/mockbackend/e2etesting.go index b8ab00ef3..f5d4aa668 100644 --- a/cmd/mockbackend/e2etesting.go +++ b/cmd/mockbackend/e2etesting.go @@ -45,6 +45,7 @@ type Query struct { type ExpectedResponse struct { HttpCode int `yaml:"httpCode"` ContentType string `yaml:"contentType"` + ErrBody string `yaml:"errBody"` ExpectedResults []ExpectedResult `yaml:"expectedResults"` } @@ -245,8 +246,11 @@ func doTest(logger *zap.Logger, t *Query, verbose bool) []error { ) } - // We don't need to actually check body of response if we expect any sort of error (4xx/5xx) + // We don't need to actually check body of response if we expect any sort of error (4xx/5xx), but for check error handling do this if t.ExpectedResponse.HttpCode >= 300 { + if t.ExpectedResponse.ErrBody != "" && t.ExpectedResponse.ErrBody != string(b) { + failures = append(failures, merry2.Errorf("mismatch error body, got '%s', expected '%s'", string(b), t.ExpectedResponse.ErrBody)) + } return failures } diff --git a/cmd/mockbackend/testcases/find_error/find_error.yaml b/cmd/mockbackend/testcases/find_error/find_error.yaml index 53b5003f7..f165bbe57 100644 --- a/cmd/mockbackend/testcases/find_error/find_error.yaml +++ b/cmd/mockbackend/testcases/find_error/find_error.yaml @@ -61,6 +61,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "Service Unavailable\n" # 503, partial success - endpoint: "http://127.0.0.1:8081" @@ -69,6 +70,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "Service Unavailable\n" listeners: - address: ":9070" diff --git a/cmd/mockbackend/testcases/render_error_all/render_error_all.yaml b/cmd/mockbackend/testcases/render_error_all/render_error_all.yaml index d312d89d0..b8ee69c27 100644 --- a/cmd/mockbackend/testcases/render_error_all/render_error_all.yaml +++ b/cmd/mockbackend/testcases/render_error_all/render_error_all.yaml @@ -45,6 +45,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "c: timeout while fetching Response\n" # 503 - endpoint: "http://127.0.0.1:8081" @@ -53,6 +54,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "d: Service Unavailable\n" # partial success - endpoint: "http://127.0.0.1:8081" @@ -61,6 +63,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "d: Service Unavailable\n" # partial success, must fail, target d failed - endpoint: "http://127.0.0.1:8081" @@ -69,6 +72,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "divideSeries(a,d): Service Unavailable\n" listeners: - address: ":9070" diff --git a/cmd/mockbackend/testcases/render_error_all_rr/render_error_all_rr.yaml b/cmd/mockbackend/testcases/render_error_all_rr/render_error_all_rr.yaml index a401b99ae..2c0e64748 100644 --- a/cmd/mockbackend/testcases/render_error_all_rr/render_error_all_rr.yaml +++ b/cmd/mockbackend/testcases/render_error_all_rr/render_error_all_rr.yaml @@ -81,6 +81,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "d: Service Unavailable\n" # partial success - endpoint: "http://127.0.0.1:8081" @@ -89,6 +90,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "d: Service Unavailable\n" # partial success # TODO: must fail, target d failed @@ -98,6 +100,7 @@ test: expectedResponse: httpCode: 503 contentType: "text/plain; charset=utf-8" + errBody: "divideSeries(a,d): Service Unavailable\n" listeners: - address: ":9070" diff --git a/cmd/mockbackend/testcases/render_error_refused/carbonapi.yaml b/cmd/mockbackend/testcases/render_error_refused/carbonapi.yaml new file mode 100644 index 000000000..e9109f72d --- /dev/null +++ b/cmd/mockbackend/testcases/render_error_refused/carbonapi.yaml @@ -0,0 +1,57 @@ +listen: "localhost:8081" +expvar: + enabled: true + pprofEnabled: false + listen: "" +concurency: 1000 +notFoundStatusCode: 200 +cache: + type: "mem" + size_mb: 0 + defaultTimeoutSec: 60 +cpus: 0 +tz: "" +maxBatchSize: 0 +graphite: + host: "" + interval: "60s" + prefix: "carbon.api" + pattern: "{prefix}.{fqdn}" +idleConnections: 10 +pidFile: "" +upstreams: + buckets: 10 + timeouts: + find: "2s" + render: "5s" + connect: "200ms" + concurrencyLimitPerServer: 0 + keepAliveInterval: "30s" + maxIdleConnsPerHost: 100 + backendsv2: + backends: + - + groupName: "mock-001" + protocol: "auto" + lbMethod: "all" + maxTries: 1 + maxBatchSize: 0 + keepAliveInterval: "10s" + concurrencyLimit: 0 + forceAttemptHTTP2: true + maxIdleConnsPerHost: 1000 + timeouts: + find: "3s" + render: "5s" + connect: "200ms" + servers: + - "http://127.0.0.1:9071" +graphite09compat: false +expireDelaySec: 10 +logger: + - logger: "" + file: "stderr" + level: "debug" + encoding: "console" + encodingTime: "iso8601" + encodingDuration: "seconds" diff --git a/cmd/mockbackend/testcases/render_error_refused/render_error_refused.yaml b/cmd/mockbackend/testcases/render_error_refused/render_error_refused.yaml new file mode 100644 index 000000000..d789e12c1 --- /dev/null +++ b/cmd/mockbackend/testcases/render_error_refused/render_error_refused.yaml @@ -0,0 +1,43 @@ +version: "v1" +test: + apps: + - name: "carbonapi" + binary: "./carbonapi" + args: + - "-config" + - "./cmd/mockbackend/testcases/render_error_refused/carbonapi.yaml" + - "-exact-config" + queries: + - endpoint: "http://127.0.0.1:8081" + type: "GET" + URL: "/render/?target=a&format=json" + expectedResponse: + httpCode: 503 + contentType: "text/plain; charset=utf-8" + errBody: "a: connection refused\n" + - endpoint: "http://127.0.0.1:8081" + type: "GET" + URL: "/render/?target=a&target=b&format=json" + expectedResponse: + httpCode: 503 + contentType: "text/plain; charset=utf-8" + errBody: "a: connection refused\r\nb: connection refused\n" + +listeners: + - address: ":9070" + expressions: + "a": + pathExpression: "a" + data: + - metricName: "a" + values: [0,1,2,2,3] + + # timeout + "c": + pathExpression: "c" + code: 404 + replyDelayMS: 7000 + + "d": + pathExpression: "d" + code: 503 diff --git a/zipper/helper/requests.go b/zipper/helper/requests.go index b31e99a13..010539130 100644 --- a/zipper/helper/requests.go +++ b/zipper/helper/requests.go @@ -124,6 +124,52 @@ func HttpErrorCode(err merry.Error) (code int) { return } +func stripKey(key string, n int) string { + if len(key) > n+3 { + key = key[:n/2] + "..." + key[n/2+1:] + } + return key +} + +// for stable return code on multiply errors +func recalcCode(code, newCode int) int { + if newCode == http.StatusGatewayTimeout || newCode == http.StatusBadGateway { + // simplify code, one error type for communications errors, all we can retry + newCode = http.StatusServiceUnavailable + } + if code == 0 || code == http.StatusNotFound { + return newCode + } + + if newCode >= 400 && newCode < 500 && code >= 400 && code < 500 { + if newCode == http.StatusBadRequest { + return newCode + } else if newCode == http.StatusForbidden && code != http.StatusBadRequest { + return newCode + } + } + if newCode < code { + code = newCode + } + return code +} + +func MerryRootError(err error) string { + c := merry.RootCause(err) + if c == nil { + c = err + } + return merryError(c) +} + +func merryError(err error) string { + if msg := merry.Message(err); len(msg) > 0 { + return strings.TrimRight(msg, "\n") + } else { + return err.Error() + } +} + func MergeHttpErrors(errors []merry.Error) (int, []string) { returnCode := http.StatusNotFound errMsgs := make([]string, 0) @@ -141,40 +187,36 @@ func MergeHttpErrors(errors []merry.Error) (int, []string) { code = http.StatusBadRequest } - if msg := merry.Message(c); len(msg) > 0 { - errMsgs = append(errMsgs, strings.TrimRight(msg, "\n")) - } else { - errMsgs = append(errMsgs, c.Error()) - } + errMsgs = append(errMsgs, merryError(c)) - if code == http.StatusGatewayTimeout || code == http.StatusBadGateway { - // simplify code, one error type for communications errors, all we can retry - code = http.StatusServiceUnavailable - } - - if code == http.StatusBadRequest { - // The 400 is returned on wrong requests, e.g. non-existent functions - returnCode = code - } else if returnCode == http.StatusNotFound || code == http.StatusForbidden { - // First error or access denied (may be limits or other) - returnCode = code - } else if code != http.StatusServiceUnavailable { - returnCode = code - } + returnCode = recalcCode(returnCode, code) } return returnCode, errMsgs } func MergeHttpErrorMap(errorsMap map[string]merry.Error) (int, []string) { - errors := make([]merry.Error, len(errorsMap)) - i := 0 - for _, err := range errorsMap { - errors[i] = err - i++ + returnCode := http.StatusNotFound + errMsgs := make([]string, 0) + for key, err := range errorsMap { + c := merry.RootCause(err) + if c == nil { + c = err + } + + code := merry.HTTPCode(err) + if code == http.StatusNotFound { + continue + } else if code == http.StatusInternalServerError && merry.Is(c, parser.ErrInvalidArg) { + // check for invalid args, see applyByNode rewrite function + code = http.StatusBadRequest + } + + errMsgs = append(errMsgs, stripKey(key, 128)+": "+merryError(c)) + returnCode = recalcCode(returnCode, code) } - return MergeHttpErrors(errors) + return returnCode, errMsgs } func HttpErrorByCode(err merry.Error) merry.Error { @@ -332,7 +374,7 @@ func (c *HttpQuery) doRequest(ctx context.Context, logger *zap.Logger, server, u return &ServerResponse{Server: server, Response: body}, nil } -func (c *HttpQuery) DoQuery(ctx context.Context, logger *zap.Logger, uri string, r types.Request) (*ServerResponse, merry.Error) { +func (c *HttpQuery) DoQuery(ctx context.Context, logger *zap.Logger, uri string, r types.Request) (resp *ServerResponse, err merry.Error) { maxTries := c.maxTries if len(c.servers) > maxTries { maxTries = len(c.servers) @@ -345,11 +387,14 @@ func (c *HttpQuery) DoQuery(ctx context.Context, logger *zap.Logger, uri string, res, err := c.doRequest(ctx, logger, server, uri, r) if err != nil { logger.Debug("have errors", - zap.Error(err), + zap.String("error", err.Error()), + zap.String("server", server), ) e = e.WithCause(err).WithHTTPCode(merry.HTTPCode(err)) code = merry.HTTPCode(err) + // TODO (msaf1980): may be metric for server failures ? + // TODO (msaf1980): may be retry policy for avoid retry bad queries ? continue } @@ -359,7 +404,7 @@ func (c *HttpQuery) DoQuery(ctx context.Context, logger *zap.Logger, uri string, return nil, types.ErrMaxTriesExceeded.WithCause(e).WithHTTPCode(code) } -func (c *HttpQuery) DoQueryToAll(ctx context.Context, logger *zap.Logger, uri string, r types.Request) ([]*ServerResponse, merry.Error) { +func (c *HttpQuery) DoQueryToAll(ctx context.Context, logger *zap.Logger, uri string, r types.Request) (resp []*ServerResponse, err merry.Error) { maxTries := c.maxTries if len(c.servers) > maxTries { maxTries = len(c.servers) diff --git a/zipper/helper/requests_test.go b/zipper/helper/requests_test.go index 0b60d7187..56e01b493 100644 --- a/zipper/helper/requests_test.go +++ b/zipper/helper/requests_test.go @@ -5,6 +5,8 @@ import ( "net" "net/http" "reflect" + "sort" + "strconv" "testing" "github.com/ansel1/merry" @@ -13,8 +15,6 @@ import ( ) func TestMergeHttpErrors(t *testing.T) { - type args struct { - } tests := []struct { name string errors []merry.Error @@ -120,7 +120,7 @@ func TestMergeHttpErrors(t *testing.T) { merry.New("error").WithHTTPCode(http.StatusBadRequest), merry.New("limit").WithHTTPCode(http.StatusForbidden), }, - wantCode: http.StatusForbidden, // Last win + wantCode: http.StatusBadRequest, want: []string{"error", "limit"}, }, { @@ -129,7 +129,7 @@ func TestMergeHttpErrors(t *testing.T) { merry.New("limit").WithHTTPCode(http.StatusForbidden), merry.New("error").WithHTTPCode(http.StatusBadRequest), }, - wantCode: http.StatusBadRequest, // Last win + wantCode: http.StatusBadRequest, want: []string{"limit", "error"}, }, } @@ -146,6 +146,143 @@ func TestMergeHttpErrors(t *testing.T) { } } +func TestMergeHttpErrorMap(t *testing.T) { + tests := []struct { + name string + errors map[string]merry.Error + wantCode int + want []string + }{ + { + name: "NotFound", + errors: map[string]merry.Error{}, + wantCode: http.StatusNotFound, + want: []string{}, + }, + { + name: "NetErr", + errors: map[string]merry.Error{ + "a": types.ErrBackendError.WithValue("server", "test").WithCause(&net.OpError{Op: "connect", Err: fmt.Errorf("refused")}).WithHTTPCode(http.StatusServiceUnavailable), + }, + wantCode: http.StatusServiceUnavailable, + want: []string{"a: connect: refused"}, + }, + { + name: "NetErr (incapsulated)", + errors: map[string]merry.Error{ + "b": types.ErrMaxTriesExceeded.WithCause(types.ErrBackendError.WithValue("server", "test").WithCause(&net.OpError{Op: "connect", Err: fmt.Errorf("refused")})).WithHTTPCode(http.StatusServiceUnavailable), + }, + wantCode: http.StatusServiceUnavailable, + want: []string{"b: connect: refused"}, + }, + { + name: "ServiceUnavailable", + errors: map[string]merry.Error{ + "d": merry.New("unavaliable").WithHTTPCode(http.StatusServiceUnavailable), + }, + wantCode: http.StatusServiceUnavailable, + want: []string{"d: unavaliable"}, + }, + { + name: "GatewayTimeout and ServiceUnavailable", + errors: map[string]merry.Error{ + "a": merry.New("timeout").WithHTTPCode(http.StatusGatewayTimeout), + "de": merry.New("unavaliable").WithHTTPCode(http.StatusServiceUnavailable), + }, + wantCode: http.StatusServiceUnavailable, + want: []string{"a: timeout", "de: unavaliable"}, + }, + { + name: "ServiceUnavailable and GatewayTimeout", + errors: map[string]merry.Error{ + "de": merry.New("unavaliable").WithHTTPCode(http.StatusServiceUnavailable), + "a": merry.New("timeout").WithHTTPCode(http.StatusGatewayTimeout), + }, + wantCode: http.StatusServiceUnavailable, + want: []string{"a: timeout", "de: unavaliable"}, + }, + { + name: "Forbidden and GatewayTimeout", + errors: map[string]merry.Error{ + "de": merry.New("limit").WithHTTPCode(http.StatusForbidden), + "c": merry.New("timeout").WithHTTPCode(http.StatusGatewayTimeout), + }, + wantCode: http.StatusForbidden, + want: []string{"c: timeout", "de: limit"}, + }, + { + name: "GatewayTimeout and Forbidden", + errors: map[string]merry.Error{ + "a": merry.New("limit").WithHTTPCode(http.StatusForbidden), + "c": merry.New("timeout").WithHTTPCode(http.StatusGatewayTimeout), + }, + wantCode: http.StatusForbidden, + want: []string{"a: limit", "c: timeout"}, + }, + { + name: "InternalServerError and Forbidden", + errors: map[string]merry.Error{ + "a": merry.New("error").WithHTTPCode(http.StatusInternalServerError), + "cd": merry.New("limit").WithHTTPCode(http.StatusForbidden), + }, + wantCode: http.StatusForbidden, + want: []string{"a: error", "cd: limit"}, + }, + { + name: "InternalServerError and GatewayTimeout", + errors: map[string]merry.Error{ + "a": merry.New("error").WithHTTPCode(http.StatusInternalServerError), + "b": merry.New("timeout").WithHTTPCode(http.StatusGatewayTimeout), + }, + wantCode: http.StatusInternalServerError, + want: []string{"a: error", "b: timeout"}, + }, + { + name: "GatewayTimeout and InternalServerError", + errors: map[string]merry.Error{ + "a": merry.New("timeout").WithHTTPCode(http.StatusGatewayTimeout), + "cd": merry.New("error").WithHTTPCode(http.StatusInternalServerError), + }, + wantCode: http.StatusInternalServerError, + want: []string{"a: timeout", "cd: error"}, + }, + { + name: "BadRequest and Forbidden", + errors: map[string]merry.Error{ + "de": merry.New("error").WithHTTPCode(http.StatusBadRequest), + "a": merry.New("limit").WithHTTPCode(http.StatusForbidden), + }, + wantCode: http.StatusBadRequest, + want: []string{"a: limit", "de: error"}, + }, + { + name: "Forbidden and BadRequest", + errors: map[string]merry.Error{ + "a": merry.New("limit").WithHTTPCode(http.StatusForbidden), + "b{c,de,klmn}.cde.d{c,de,klmn}.e{c,de,klmn}.k{c,de,klmn}.b{c,de,klmn}.cde.d{c,de,klmn}.e{c,de,klmn}.k{c,de,klmn}.e{c,de,klmn}.k{c,de,klmn}": merry.New("error").WithHTTPCode(http.StatusBadRequest), + }, + wantCode: http.StatusBadRequest, + want: []string{ + "a: limit", + "b{c,de,klmn}.cde.d{c,de,klmn}.e{c,de,klmn}.k{c,de,klmn}.b{c,de,k...mn}.cde.d{c,de,klmn}.e{c,de,klmn}.k{c,de,klmn}.e{c,de,klmn}.k{c,de,klmn}: error", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotCode, got := MergeHttpErrorMap(tt.errors) + if gotCode != tt.wantCode { + t.Errorf("MergeHttpErrors() gotCode = %v, want %v", gotCode, tt.wantCode) + } + // sort error strings + sort.Strings(got) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MergeHttpErrors() got = %v, want %v", got, tt.want) + } + }) + } +} + func Test_stripHtmlTags(t *testing.T) { tests := []struct { name string @@ -184,3 +321,27 @@ func Test_stripHtmlTags(t *testing.T) { }) } } + +func Test_recalcCode(t *testing.T) { + tests := []struct { + code int + newCode int + want int + }{ + {code: 500, newCode: 403, want: 403}, + {code: 403, newCode: 500, want: 403}, + {code: 403, newCode: 400, want: 400}, + {code: 400, newCode: 403, want: 400}, + {code: 500, newCode: 503, want: 500}, + {code: 503, newCode: 500, want: 500}, + {code: 503, newCode: 502, want: 503}, + {code: 0, newCode: 502, want: 503}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + if got := recalcCode(tt.code, tt.newCode); got != tt.want { + t.Errorf("recalcCode(%d, %d) = %d, want %d", tt.code, tt.newCode, got, tt.want) + } + }) + } +}