357 lines
9.4 KiB
Go
357 lines
9.4 KiB
Go
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")
|
|
}
|
|
}
|