package httperror import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestNewValidationError(t *testing.T) { fieldErrors := map[string]string{ "email": "Email is required", "password": "Password must be at least 8 characters", } problem := NewValidationError(fieldErrors) if problem.Type != TypeValidationError { t.Errorf("Expected type %s, got %s", TypeValidationError, problem.Type) } if problem.Status != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, problem.Status) } if problem.Title != "Validation Failed" { t.Errorf("Expected title 'Validation Failed', got '%s'", problem.Title) } if len(problem.Errors) != 2 { t.Errorf("Expected 2 field errors, got %d", len(problem.Errors)) } if problem.Errors["email"] != "Email is required" { t.Errorf("Expected email error, got '%s'", problem.Errors["email"]) } if problem.Timestamp == "" { t.Error("Expected timestamp to be set") } } func TestNewValidationError_Empty(t *testing.T) { problem := NewValidationError(map[string]string{}) if problem.Detail != "Validation failed." { t.Errorf("Expected detail 'Validation failed.', got '%s'", problem.Detail) } } func TestNewBadRequestError(t *testing.T) { detail := "Invalid request payload" problem := NewBadRequestError(detail) if problem.Type != TypeBadRequest { t.Errorf("Expected type %s, got %s", TypeBadRequest, problem.Type) } if problem.Status != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, problem.Status) } if problem.Detail != detail { t.Errorf("Expected detail '%s', got '%s'", detail, problem.Detail) } } func TestNewUnauthorizedError(t *testing.T) { detail := "Invalid token" problem := NewUnauthorizedError(detail) if problem.Type != TypeUnauthorized { t.Errorf("Expected type %s, got %s", TypeUnauthorized, problem.Type) } if problem.Status != http.StatusUnauthorized { t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, problem.Status) } if problem.Detail != detail { t.Errorf("Expected detail '%s', got '%s'", detail, problem.Detail) } } func TestNewUnauthorizedError_DefaultMessage(t *testing.T) { problem := NewUnauthorizedError("") if problem.Detail != "Authentication is required to access this resource." { t.Errorf("Expected default detail message, got '%s'", problem.Detail) } } func TestNewForbiddenError(t *testing.T) { detail := "Insufficient permissions" problem := NewForbiddenError(detail) if problem.Type != TypeForbidden { t.Errorf("Expected type %s, got %s", TypeForbidden, problem.Type) } if problem.Status != http.StatusForbidden { t.Errorf("Expected status %d, got %d", http.StatusForbidden, problem.Status) } } func TestNewNotFoundError(t *testing.T) { problem := NewNotFoundError("User") if problem.Type != TypeNotFound { t.Errorf("Expected type %s, got %s", TypeNotFound, problem.Type) } if problem.Status != http.StatusNotFound { t.Errorf("Expected status %d, got %d", http.StatusNotFound, problem.Status) } if problem.Detail != "User not found." { t.Errorf("Expected detail 'User not found.', got '%s'", problem.Detail) } } func TestNewConflictError(t *testing.T) { detail := "Email already exists" problem := NewConflictError(detail) if problem.Type != TypeConflict { t.Errorf("Expected type %s, got %s", TypeConflict, problem.Type) } if problem.Status != http.StatusConflict { t.Errorf("Expected status %d, got %d", http.StatusConflict, problem.Status) } } func TestNewInternalServerError(t *testing.T) { detail := "Database connection failed" problem := NewInternalServerError(detail) if problem.Type != TypeInternalError { t.Errorf("Expected type %s, got %s", TypeInternalError, problem.Type) } if problem.Status != http.StatusInternalServerError { t.Errorf("Expected status %d, got %d", http.StatusInternalServerError, problem.Status) } } func TestNewServiceUnavailableError(t *testing.T) { problem := NewServiceUnavailableError("") if problem.Type != TypeServiceUnavailable { t.Errorf("Expected type %s, got %s", TypeServiceUnavailable, problem.Type) } if problem.Status != http.StatusServiceUnavailable { t.Errorf("Expected status %d, got %d", http.StatusServiceUnavailable, problem.Status) } } func TestWithInstance(t *testing.T) { problem := NewBadRequestError("Test") instance := "/api/v1/test" problem.WithInstance(instance) if problem.Instance != instance { t.Errorf("Expected instance '%s', got '%s'", instance, problem.Instance) } } func TestWithTraceID(t *testing.T) { problem := NewBadRequestError("Test") traceID := "trace-123" problem.WithTraceID(traceID) if problem.TraceID != traceID { t.Errorf("Expected traceID '%s', got '%s'", traceID, problem.TraceID) } } func TestWithError(t *testing.T) { problem := NewBadRequestError("Test") problem.WithError("email", "Email is required") problem.WithError("password", "Password is required") if len(problem.Errors) != 2 { t.Errorf("Expected 2 errors, got %d", len(problem.Errors)) } if problem.Errors["email"] != "Email is required" { t.Errorf("Expected email error, got '%s'", problem.Errors["email"]) } } func TestProblemDetailError(t *testing.T) { detail := "Test detail" problem := NewBadRequestError(detail) if problem.Error() != detail { t.Errorf("Expected Error() to return detail, got '%s'", problem.Error()) } // Test with no detail problem2 := &ProblemDetail{ Title: "Test Title", } if problem2.Error() != "Test Title" { t.Errorf("Expected Error() to return title, got '%s'", problem2.Error()) } } func TestExtractRequestID_FromContext(t *testing.T) { req := httptest.NewRequest("GET", "/test", nil) // Note: In real code, request ID would be set by middleware // For testing, we'll test the empty case requestID := ExtractRequestID(req) // Should return empty string when no request ID is present if requestID != "" { t.Errorf("Expected empty string, got '%s'", requestID) } } func TestExtractRequestID_FromHeader(t *testing.T) { req := httptest.NewRequest("GET", "/test", nil) req.Header.Set("X-Request-ID", "test-request-123") requestID := ExtractRequestID(req) if requestID != "test-request-123" { t.Errorf("Expected 'test-request-123', got '%s'", requestID) } } func TestRespondWithProblem(t *testing.T) { problem := NewValidationError(map[string]string{ "email": "Email is required", }) problem.WithInstance("/api/v1/test") problem.WithTraceID("trace-123") w := httptest.NewRecorder() RespondWithProblem(w, problem) // Check status code if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } // Check content type contentType := w.Header().Get("Content-Type") if contentType != "application/problem+json" { t.Errorf("Expected Content-Type 'application/problem+json', got '%s'", contentType) } // Check JSON response var response ProblemDetail if err := json.NewDecoder(w.Body).Decode(&response); err != nil { t.Fatalf("Failed to decode response: %v", err) } if response.Type != TypeValidationError { t.Errorf("Expected type %s, got %s", TypeValidationError, response.Type) } if response.Instance != "/api/v1/test" { t.Errorf("Expected instance '/api/v1/test', got '%s'", response.Instance) } if response.TraceID != "trace-123" { t.Errorf("Expected traceID 'trace-123', got '%s'", response.TraceID) } if len(response.Errors) != 1 { t.Errorf("Expected 1 error, got %d", len(response.Errors)) } } func TestRespondWithError_ProblemDetail(t *testing.T) { req := httptest.NewRequest("GET", "/api/v1/test", nil) w := httptest.NewRecorder() problem := NewBadRequestError("Test error") RespondWithError(w, req, problem) // Check status code if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } // Check that instance was set var response ProblemDetail json.NewDecoder(w.Body).Decode(&response) if response.Instance != "/api/v1/test" { t.Errorf("Expected instance to be set automatically, got '%s'", response.Instance) } } func TestRespondWithError_StandardError(t *testing.T) { req := httptest.NewRequest("GET", "/api/v1/test", nil) w := httptest.NewRecorder() err := &customError{message: "Custom error"} RespondWithError(w, req, err) // Check status code (should be 500 for standard errors) if w.Code != http.StatusInternalServerError { t.Errorf("Expected status %d, got %d", http.StatusInternalServerError, w.Code) } // Check that it was wrapped in a ProblemDetail var response ProblemDetail json.NewDecoder(w.Body).Decode(&response) if response.Type != TypeInternalError { t.Errorf("Expected type %s, got %s", TypeInternalError, response.Type) } if response.Detail != "Custom error" { t.Errorf("Expected detail 'Custom error', got '%s'", response.Detail) } } // Helper type for testing standard error handling type customError struct { message string } func (e *customError) Error() string { return e.message } func TestChaining(t *testing.T) { // Test method chaining problem := NewBadRequestError("Test"). WithInstance("/api/v1/test"). WithTraceID("trace-123"). WithError("field1", "error1"). WithError("field2", "error2") if problem.Instance != "/api/v1/test" { t.Error("Instance not set correctly through chaining") } if problem.TraceID != "trace-123" { t.Error("TraceID not set correctly through chaining") } if len(problem.Errors) != 2 { t.Error("Errors not set correctly through chaining") } }