package httpClient import ( "fmt" "net/http" "strings" "testing" ) func TestHTTPError_Error(t *testing.T) { tests := []struct { name string statusCode int message string want string }{ { name: "basic http error", statusCode: 404, message: "Not Found", want: "HTTP 404: Not Found", }, { name: "server error", statusCode: 500, message: "Internal Server Error", want: "HTTP 500: Internal Server Error", }, { name: "unauthorized error", statusCode: 401, message: "Unauthorized", want: "HTTP 401: Unauthorized", }, { name: "empty message", statusCode: 400, message: "", want: "HTTP 400: ", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := &HTTPError{ StatusCode: tt.statusCode, Message: tt.message, } got := err.Error() if got != tt.want { t.Errorf("HTTPError.Error() = %q, want %q", got, tt.want) } }) } } func TestHTTPError_Is(t *testing.T) { err404 := &HTTPError{StatusCode: 404, Message: "Not Found"} err500 := &HTTPError{StatusCode: 500, Message: "Server Error"} otherErr404 := &HTTPError{StatusCode: 404, Message: "Different message"} regularError := fmt.Errorf("regular error") tests := []struct { name string err error target error want bool }{ { name: "same error instance", err: err404, target: err404, want: true, }, { name: "different HTTP errors with same status", err: err404, target: otherErr404, want: true, }, { name: "different HTTP errors with different status", err: err404, target: err500, want: false, }, { name: "HTTP error vs regular error", err: err404, target: regularError, want: false, }, { name: "regular error vs HTTP error", err: regularError, target: err404, want: false, }, { name: "nil target", err: err404, target: nil, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var got bool if httpErr, ok := tt.err.(*HTTPError); ok { got = httpErr.Is(tt.target) } else { got = false // Non-HTTP errors return false } if got != tt.want { t.Errorf("HTTPError.Is() = %v, want %v", got, tt.want) } }) } } func TestIsHTTPError(t *testing.T) { httpErr := &HTTPError{StatusCode: 404, Message: "Not Found"} regularErr := fmt.Errorf("regular error") tests := []struct { name string err error want bool }{ { name: "http error", err: httpErr, want: true, }, { name: "regular error", err: regularErr, want: false, }, { name: "nil error", err: nil, want: false, }, { name: "wrapped http error", err: fmt.Errorf("wrapped: %w", httpErr), want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := IsHTTPError(tt.err) if got != tt.want { t.Errorf("IsHTTPError() = %v, want %v", got, tt.want) } }) } } func TestGetHTTPStatusCode(t *testing.T) { tests := []struct { name string err error want int }{ { name: "http error 404", err: &HTTPError{StatusCode: 404, Message: "Not Found"}, want: 404, }, { name: "http error 500", err: &HTTPError{StatusCode: 500, Message: "Server Error"}, want: 500, }, { name: "wrapped http error", err: fmt.Errorf("wrapped: %w", &HTTPError{StatusCode: 403, Message: "Forbidden"}), want: 403, }, { name: "regular error", err: fmt.Errorf("regular error"), want: 0, }, { name: "nil error", err: nil, want: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := GetHTTPStatusCode(tt.err) if got != tt.want { t.Errorf("GetHTTPStatusCode() = %v, want %v", got, tt.want) } }) } } func TestNewHTTPError(t *testing.T) { statusCode := 404 message := "Page not found" err := NewHTTPError(statusCode, message) // Check type httpErr, ok := err.(*HTTPError) if !ok { t.Fatalf("NewHTTPError should return *HTTPError, got %T", err) } // Check fields if httpErr.StatusCode != statusCode { t.Errorf("Expected StatusCode=%d, got %d", statusCode, httpErr.StatusCode) } if httpErr.Message != message { t.Errorf("Expected Message=%q, got %q", message, httpErr.Message) } // Check error string expectedErrorString := fmt.Sprintf("HTTP %d: %s", statusCode, message) if httpErr.Error() != expectedErrorString { t.Errorf("Expected error string=%q, got %q", expectedErrorString, httpErr.Error()) } } func TestHTTPError_StatusCodeChecks(t *testing.T) { tests := []struct { name string statusCode int isClient bool isServer bool }{ {"200 OK", 200, false, false}, {"400 Bad Request", 400, true, false}, {"401 Unauthorized", 401, true, false}, {"404 Not Found", 404, true, false}, {"499 Client Error", 499, true, false}, {"500 Server Error", 500, false, true}, {"502 Bad Gateway", 502, false, true}, {"599 Server Error", 599, false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := &HTTPError{StatusCode: tt.statusCode, Message: "test"} isClient := err.StatusCode >= 400 && err.StatusCode < 500 isServer := err.StatusCode >= 500 && err.StatusCode < 600 if isClient != tt.isClient { t.Errorf("Status %d: expected isClient=%v, got %v", tt.statusCode, tt.isClient, isClient) } if isServer != tt.isServer { t.Errorf("Status %d: expected isServer=%v, got %v", tt.statusCode, tt.isServer, isServer) } }) } } func TestHTTPError_Integration(t *testing.T) { // Test that HTTPError integrates well with standard error handling err := NewHTTPError(http.StatusNotFound, "Resource not found") // Should be able to use with errors.Is target := &HTTPError{StatusCode: http.StatusNotFound} if !err.(*HTTPError).Is(target) { t.Error("HTTPError should match target with same status code") } // Should be detectable as HTTPError if !IsHTTPError(err) { t.Error("Should be detectable as HTTPError") } // Should return correct status code if GetHTTPStatusCode(err) != http.StatusNotFound { t.Error("Should return correct status code") } // Should have meaningful string representation errorString := err.Error() if !strings.Contains(errorString, "404") { t.Error("Error string should contain status code") } if !strings.Contains(errorString, "Resource not found") { t.Error("Error string should contain message") } } func TestHTTPError_EdgeCases(t *testing.T) { // Test with zero status code err := NewHTTPError(0, "Zero status") if err.Error() != "HTTP 0: Zero status" { t.Errorf("Unexpected error string for zero status: %s", err.Error()) } // Test with very long message longMessage := strings.Repeat("a", 1000) err = NewHTTPError(500, longMessage) if !strings.Contains(err.Error(), longMessage) { t.Error("Long message should be preserved") } // Test status code boundaries for _, code := range []int{399, 400, 499, 500, 599, 600} { err := &HTTPError{StatusCode: code} // Should not panic _ = err.Error() } }