package errors import ( "encoding/json" "errors" "net/http" "net/http/httptest" "testing" ) func TestStatusCode(t *testing.T) { tests := []struct { name string err error expectedStatus int }{ { name: "nil error", err: nil, expectedStatus: http.StatusOK, }, { name: "validation error", err: NewValidationError("field", "invalid", nil), expectedStatus: http.StatusBadRequest, }, { name: "not found error", err: NewNotFoundError("user", "123"), expectedStatus: http.StatusNotFound, }, { name: "unauthorized error", err: NewUnauthorizedError("invalid token"), expectedStatus: http.StatusUnauthorized, }, { name: "forbidden error", err: NewForbiddenError("resource", "delete"), expectedStatus: http.StatusForbidden, }, { name: "conflict error", err: NewConflictError("user", "email", "test@example.com"), expectedStatus: http.StatusConflict, }, { name: "timeout error", err: NewTimeoutError("operation", "30s"), expectedStatus: http.StatusRequestTimeout, }, { name: "rate limit error", err: NewRateLimitError(100, 60), expectedStatus: http.StatusTooManyRequests, }, { name: "service error", err: NewServiceError("rqlite", "unavailable", 503, nil), expectedStatus: http.StatusServiceUnavailable, }, { name: "internal error", err: NewInternalError("something went wrong", nil), expectedStatus: http.StatusInternalServerError, }, { name: "sentinel ErrNotFound", err: ErrNotFound, expectedStatus: http.StatusNotFound, }, { name: "sentinel ErrUnauthorized", err: ErrUnauthorized, expectedStatus: http.StatusUnauthorized, }, { name: "sentinel ErrForbidden", err: ErrForbidden, expectedStatus: http.StatusForbidden, }, { name: "standard error", err: errors.New("generic error"), expectedStatus: http.StatusInternalServerError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { status := StatusCode(tt.err) if status != tt.expectedStatus { t.Errorf("Expected status %d, got %d", tt.expectedStatus, status) } }) } } func TestCodeToHTTPStatus(t *testing.T) { tests := []struct { code string expectedStatus int }{ {CodeOK, http.StatusOK}, {CodeInvalidArgument, http.StatusBadRequest}, {CodeValidation, http.StatusBadRequest}, {CodeNotFound, http.StatusNotFound}, {CodeUnauthorized, http.StatusUnauthorized}, {CodeUnauthenticated, http.StatusUnauthorized}, {CodeForbidden, http.StatusForbidden}, {CodePermissionDenied, http.StatusForbidden}, {CodeConflict, http.StatusConflict}, {CodeAlreadyExists, http.StatusConflict}, {CodeTimeout, http.StatusRequestTimeout}, {CodeDeadlineExceeded, http.StatusRequestTimeout}, {CodeRateLimit, http.StatusTooManyRequests}, {CodeResourceExhausted, http.StatusTooManyRequests}, {CodeServiceUnavailable, http.StatusServiceUnavailable}, {CodeUnavailable, http.StatusServiceUnavailable}, {CodeInternal, http.StatusInternalServerError}, {CodeUnknown, http.StatusInternalServerError}, {CodeUnimplemented, http.StatusNotImplemented}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { status := codeToHTTPStatus(tt.code) if status != tt.expectedStatus { t.Errorf("Code %s: expected status %d, got %d", tt.code, tt.expectedStatus, status) } }) } } func TestToHTTPError(t *testing.T) { traceID := "trace-123" t.Run("nil error", func(t *testing.T) { httpErr := ToHTTPError(nil, traceID) if httpErr.Status != http.StatusOK { t.Errorf("Expected status 200, got %d", httpErr.Status) } if httpErr.Code != CodeOK { t.Errorf("Expected code OK, got %s", httpErr.Code) } if httpErr.TraceID != traceID { t.Errorf("Expected trace ID %s, got %s", traceID, httpErr.TraceID) } }) t.Run("validation error with details", func(t *testing.T) { err := NewValidationError("email", "invalid format", "not-an-email") httpErr := ToHTTPError(err, traceID) if httpErr.Status != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", httpErr.Status) } if httpErr.Code != CodeValidation { t.Errorf("Expected code VALIDATION_ERROR, got %s", httpErr.Code) } if httpErr.Details["field"] != "email" { t.Errorf("Expected field detail 'email', got %s", httpErr.Details["field"]) } }) t.Run("not found error with details", func(t *testing.T) { err := NewNotFoundError("user", "123") httpErr := ToHTTPError(err, traceID) if httpErr.Status != http.StatusNotFound { t.Errorf("Expected status 404, got %d", httpErr.Status) } if httpErr.Details["resource"] != "user" { t.Errorf("Expected resource detail 'user', got %s", httpErr.Details["resource"]) } if httpErr.Details["id"] != "123" { t.Errorf("Expected id detail '123', got %s", httpErr.Details["id"]) } }) t.Run("forbidden error with details", func(t *testing.T) { err := NewForbiddenError("function", "delete") httpErr := ToHTTPError(err, traceID) if httpErr.Details["resource"] != "function" { t.Errorf("Expected resource detail 'function', got %s", httpErr.Details["resource"]) } if httpErr.Details["action"] != "delete" { t.Errorf("Expected action detail 'delete', got %s", httpErr.Details["action"]) } }) t.Run("conflict error with details", func(t *testing.T) { err := NewConflictError("user", "email", "test@example.com") httpErr := ToHTTPError(err, traceID) if httpErr.Details["resource"] != "user" { t.Errorf("Expected resource detail 'user', got %s", httpErr.Details["resource"]) } if httpErr.Details["field"] != "email" { t.Errorf("Expected field detail 'email', got %s", httpErr.Details["field"]) } }) t.Run("timeout error with details", func(t *testing.T) { err := NewTimeoutError("function execution", "30s") httpErr := ToHTTPError(err, traceID) if httpErr.Details["operation"] != "function execution" { t.Errorf("Expected operation detail, got %s", httpErr.Details["operation"]) } if httpErr.Details["duration"] != "30s" { t.Errorf("Expected duration detail '30s', got %s", httpErr.Details["duration"]) } }) t.Run("service error with details", func(t *testing.T) { err := NewServiceError("rqlite", "unavailable", 503, nil) httpErr := ToHTTPError(err, traceID) if httpErr.Details["service"] != "rqlite" { t.Errorf("Expected service detail 'rqlite', got %s", httpErr.Details["service"]) } }) t.Run("internal error with operation", func(t *testing.T) { err := NewInternalError("failed", nil).WithOperation("saveUser") httpErr := ToHTTPError(err, traceID) if httpErr.Details["operation"] != "saveUser" { t.Errorf("Expected operation detail 'saveUser', got %s", httpErr.Details["operation"]) } }) t.Run("standard error", func(t *testing.T) { err := errors.New("generic error") httpErr := ToHTTPError(err, traceID) if httpErr.Status != http.StatusInternalServerError { t.Errorf("Expected status 500, got %d", httpErr.Status) } if httpErr.Code != CodeInternal { t.Errorf("Expected code INTERNAL, got %s", httpErr.Code) } if httpErr.Message != "generic error" { t.Errorf("Expected message 'generic error', got %s", httpErr.Message) } }) } func TestWriteHTTPError(t *testing.T) { t.Run("validation error response", func(t *testing.T) { err := NewValidationError("email", "invalid format", "bad-email") w := httptest.NewRecorder() WriteHTTPError(w, err, "trace-123") if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } contentType := w.Header().Get("Content-Type") if contentType != "application/json" { t.Errorf("Expected Content-Type application/json, got %s", contentType) } var httpErr HTTPError if err := json.NewDecoder(w.Body).Decode(&httpErr); err != nil { t.Fatalf("Failed to decode response: %v", err) } if httpErr.Code != CodeValidation { t.Errorf("Expected code VALIDATION_ERROR, got %s", httpErr.Code) } if httpErr.TraceID != "trace-123" { t.Errorf("Expected trace ID trace-123, got %s", httpErr.TraceID) } if httpErr.Details["field"] != "email" { t.Errorf("Expected field detail 'email', got %s", httpErr.Details["field"]) } }) t.Run("unauthorized error with realm", func(t *testing.T) { err := NewUnauthorizedError("invalid token").WithRealm("api") w := httptest.NewRecorder() WriteHTTPError(w, err, "trace-456") authHeader := w.Header().Get("WWW-Authenticate") expectedAuth := `Bearer realm="api"` if authHeader != expectedAuth { t.Errorf("Expected WWW-Authenticate %q, got %q", expectedAuth, authHeader) } }) t.Run("rate limit error with retry-after", func(t *testing.T) { err := NewRateLimitError(100, 60) w := httptest.NewRecorder() WriteHTTPError(w, err, "trace-789") if w.Code != http.StatusTooManyRequests { t.Errorf("Expected status 429, got %d", w.Code) } // Note: The retry-after header implementation may need adjustment // as we're converting int to rune which may not be the desired behavior }) t.Run("not found error", func(t *testing.T) { err := NewNotFoundError("user", "123") w := httptest.NewRecorder() WriteHTTPError(w, err, "trace-abc") if w.Code != http.StatusNotFound { t.Errorf("Expected status 404, got %d", w.Code) } var httpErr HTTPError if err := json.NewDecoder(w.Body).Decode(&httpErr); err != nil { t.Fatalf("Failed to decode response: %v", err) } if httpErr.Details["resource"] != "user" { t.Errorf("Expected resource detail 'user', got %s", httpErr.Details["resource"]) } if httpErr.Details["id"] != "123" { t.Errorf("Expected id detail '123', got %s", httpErr.Details["id"]) } }) } func TestHTTPStatusToCode(t *testing.T) { tests := []struct { status int expectedCode string }{ {http.StatusOK, CodeOK}, {http.StatusBadRequest, CodeInvalidArgument}, {http.StatusUnauthorized, CodeUnauthenticated}, {http.StatusForbidden, CodePermissionDenied}, {http.StatusNotFound, CodeNotFound}, {http.StatusConflict, CodeAlreadyExists}, {http.StatusRequestTimeout, CodeDeadlineExceeded}, {http.StatusTooManyRequests, CodeResourceExhausted}, {http.StatusNotImplemented, CodeUnimplemented}, {http.StatusServiceUnavailable, CodeUnavailable}, {http.StatusInternalServerError, CodeInternal}, {418, CodeInvalidArgument}, // Client error (4xx) {502, CodeInternal}, // Server error (5xx) } for _, tt := range tests { t.Run(http.StatusText(tt.status), func(t *testing.T) { code := HTTPStatusToCode(tt.status) if code != tt.expectedCode { t.Errorf("Status %d: expected code %s, got %s", tt.status, tt.expectedCode, code) } }) } } func TestHTTPErrorJSON(t *testing.T) { httpErr := &HTTPError{ Status: http.StatusBadRequest, Code: CodeValidation, Message: "validation failed", Details: map[string]string{ "field": "email", }, TraceID: "trace-123", } data, err := json.Marshal(httpErr) if err != nil { t.Fatalf("Failed to marshal HTTPError: %v", err) } var decoded HTTPError if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("Failed to unmarshal HTTPError: %v", err) } if decoded.Code != httpErr.Code { t.Errorf("Expected code %s, got %s", httpErr.Code, decoded.Code) } if decoded.Message != httpErr.Message { t.Errorf("Expected message %s, got %s", httpErr.Message, decoded.Message) } if decoded.TraceID != httpErr.TraceID { t.Errorf("Expected trace ID %s, got %s", httpErr.TraceID, decoded.TraceID) } if decoded.Details["field"] != "email" { t.Errorf("Expected field detail 'email', got %s", decoded.Details["field"]) } } func BenchmarkStatusCode(b *testing.B) { err := NewNotFoundError("user", "123") b.ResetTimer() for i := 0; i < b.N; i++ { _ = StatusCode(err) } } func BenchmarkToHTTPError(b *testing.B) { err := NewValidationError("email", "invalid", "bad") b.ResetTimer() for i := 0; i < b.N; i++ { _ = ToHTTPError(err, "trace-123") } } func BenchmarkWriteHTTPError(b *testing.B) { err := NewInternalError("test error", nil) b.ResetTimer() for i := 0; i < b.N; i++ { w := httptest.NewRecorder() WriteHTTPError(w, err, "trace-123") } }