diff --git a/internal/pkg/errors/errors_test.go b/internal/pkg/errors/errors_test.go index 25e6290..67a6433 100644 --- a/internal/pkg/errors/errors_test.go +++ b/internal/pkg/errors/errors_test.go @@ -181,3 +181,52 @@ func TestToHTTP_MetadataDeepCopy(t *testing.T) { appErr.Metadata["k"] = "changed-again" require.Equal(t, "v", body.Metadata["k"]) } + +func TestHTTPStatusErrorFunctions(t *testing.T) { + tests := []struct { + name string + createFn func(string, string) *ApplicationError + isFn func(error) bool + code int + }{ + {"BadRequest", BadRequest, IsBadRequest, http.StatusBadRequest}, + {"TooManyRequests", TooManyRequests, IsTooManyRequests, http.StatusTooManyRequests}, + {"Unauthorized", Unauthorized, IsUnauthorized, http.StatusUnauthorized}, + {"Forbidden", Forbidden, IsForbidden, http.StatusForbidden}, + {"NotFound", NotFound, IsNotFound, http.StatusNotFound}, + {"Conflict", Conflict, IsConflict, http.StatusConflict}, + {"InternalServer", InternalServer, IsInternalServer, http.StatusInternalServerError}, + {"ServiceUnavailable", ServiceUnavailable, IsServiceUnavailable, http.StatusServiceUnavailable}, + {"GatewayTimeout", GatewayTimeout, IsGatewayTimeout, http.StatusGatewayTimeout}, + {"ClientClosed", ClientClosed, IsClientClosed, 499}, + } + + for _, tt := range tests { + t.Run(tt.name+"_create", func(t *testing.T) { + err := tt.createFn("REASON", "message") + require.Equal(t, int32(tt.code), err.Code) + require.Equal(t, "REASON", err.Reason) + require.Equal(t, "message", err.Message) + }) + + t.Run(tt.name+"_is_true", func(t *testing.T) { + err := New(tt.code, "ANY", "test") + require.True(t, tt.isFn(err)) + }) + + t.Run(tt.name+"_is_false", func(t *testing.T) { + err := New(tt.code+1, "ANY", "test") + require.False(t, tt.isFn(err)) + }) + + t.Run(tt.name+"_is_nil", func(t *testing.T) { + require.False(t, tt.isFn(nil)) + }) + } +} + +func TestToHTTP_NonApplicationError(t *testing.T) { + code, body := ToHTTP(stderrors.New("plain error")) + require.Equal(t, http.StatusInternalServerError, code) + require.Equal(t, int32(UnknownCode), body.Code) +} diff --git a/internal/pkg/googleapi/status_test.go b/internal/pkg/googleapi/status_test.go new file mode 100644 index 0000000..8f2eb1b --- /dev/null +++ b/internal/pkg/googleapi/status_test.go @@ -0,0 +1,40 @@ +package googleapi + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHTTPStatusToGoogleStatus(t *testing.T) { + tests := []struct { + name string + status int + want string + }{ + {"bad_request_400", http.StatusBadRequest, "INVALID_ARGUMENT"}, + {"unauthorized_401", http.StatusUnauthorized, "UNAUTHENTICATED"}, + {"forbidden_403", http.StatusForbidden, "PERMISSION_DENIED"}, + {"not_found_404", http.StatusNotFound, "NOT_FOUND"}, + {"too_many_requests_429", http.StatusTooManyRequests, "RESOURCE_EXHAUSTED"}, + {"internal_server_500", http.StatusInternalServerError, "INTERNAL"}, + {"bad_gateway_502", http.StatusBadGateway, "INTERNAL"}, + {"service_unavailable_503", http.StatusServiceUnavailable, "INTERNAL"}, + {"ok_200", http.StatusOK, "UNKNOWN"}, + {"created_201", http.StatusCreated, "UNKNOWN"}, + {"accepted_202", http.StatusAccepted, "UNKNOWN"}, + {"no_content_204", http.StatusNoContent, "UNKNOWN"}, + {"bad_request_boundary", 400, "INVALID_ARGUMENT"}, + {"server_error_boundary", 500, "INTERNAL"}, + {"custom_4xx", 418, "UNKNOWN"}, + {"custom_5xx", 599, "INTERNAL"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := HTTPStatusToGoogleStatus(tt.status) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/pkg/httputil/body_test.go b/internal/pkg/httputil/body_test.go new file mode 100644 index 0000000..f3782c6 --- /dev/null +++ b/internal/pkg/httputil/body_test.go @@ -0,0 +1,101 @@ +package httputil + +import ( + "bytes" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReadRequestBodyWithPrealloc(t *testing.T) { + tests := []struct { + name string + req *http.Request + wantBody []byte + wantErr bool + wantNilResult bool + }{ + { + name: "nil_request", + req: nil, + wantNilResult: true, + }, + { + name: "nil_body", + req: &http.Request{ + Body: nil, + }, + wantNilResult: true, + }, + { + name: "empty_body", + req: &http.Request{ + Body: http.NoBody, + ContentLength: 0, + }, + wantBody: []byte{}, + }, + { + name: "small_body_content_length_100", + req: func() *http.Request { + body := strings.NewReader("small body content") + return &http.Request{ + Body: io.NopCloser(body), + ContentLength: 100, + } + }(), + wantBody: []byte("small body content"), + }, + { + name: "large_body", + req: func() *http.Request { + data := bytes.Repeat([]byte("x"), 2000) + return &http.Request{ + Body: io.NopCloser(bytes.NewReader(data)), + ContentLength: 2000, + } + }(), + wantBody: bytes.Repeat([]byte("x"), 2000), + }, + { + name: "very_large_body", + req: func() *http.Request { + data := bytes.Repeat([]byte("y"), 2<<20+1000) // > 2MB + return &http.Request{ + Body: io.NopCloser(bytes.NewReader(data)), + ContentLength: int64(2<<20 + 1000), + } + }(), + wantBody: bytes.Repeat([]byte("y"), 2<<20+1000), + }, + { + name: "chunked_transfer_encoding", + req: func() *http.Request { + return &http.Request{ + Body: io.NopCloser(strings.NewReader("chunked data")), + ContentLength: -1, + } + }(), + wantBody: []byte("chunked data"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ReadRequestBodyWithPrealloc(tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantNilResult { + require.Nil(t, got) + } else { + require.Equal(t, tt.wantBody, got) + } + }) + } +}