diff --git a/modules/caddyhttp/errors_test.go b/modules/caddyhttp/errors_test.go new file mode 100644 index 000000000..0ea468926 --- /dev/null +++ b/modules/caddyhttp/errors_test.go @@ -0,0 +1,168 @@ +package caddyhttp + +import ( + "errors" + "fmt" + "strings" + "testing" +) + +func TestHandlerErrorError(t *testing.T) { + tests := []struct { + name string + err HandlerError + contains []string + }{ + { + name: "full error", + err: HandlerError{ + ID: "abc123", + StatusCode: 404, + Err: fmt.Errorf("not found"), + Trace: "pkg.Func (file.go:10)", + }, + contains: []string{"abc123", "404", "not found", "pkg.Func"}, + }, + { + name: "empty error", + err: HandlerError{}, + contains: []string{}, + }, + { + name: "error with only status code", + err: HandlerError{ + StatusCode: 500, + }, + contains: []string{"500"}, + }, + { + name: "error with only message", + err: HandlerError{ + Err: fmt.Errorf("something broke"), + }, + contains: []string{"something broke"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.err.Error() + for _, needle := range tt.contains { + if !strings.Contains(result, needle) { + t.Errorf("Error() = %q, should contain %q", result, needle) + } + } + }) + } +} + +func TestHandlerErrorUnwrap(t *testing.T) { + originalErr := fmt.Errorf("original error") + he := HandlerError{Err: originalErr} + + unwrapped := he.Unwrap() + if unwrapped != originalErr { + t.Errorf("Unwrap() = %v, want %v", unwrapped, originalErr) + } +} + +func TestError(t *testing.T) { + t.Run("creates error with ID and trace", func(t *testing.T) { + err := fmt.Errorf("test error") + he := Error(500, err) + + if he.StatusCode != 500 { + t.Errorf("StatusCode = %d, want 500", he.StatusCode) + } + if he.ID == "" { + t.Error("ID should not be empty") + } + if len(he.ID) != 9 { + t.Errorf("ID length = %d, want 9", len(he.ID)) + } + if he.Trace == "" { + t.Error("Trace should not be empty") + } + if he.Err != err { + t.Error("Err should be the original error") + } + }) + + t.Run("unwraps existing HandlerError", func(t *testing.T) { + inner := HandlerError{ + ID: "existing_id", + StatusCode: 404, + Err: fmt.Errorf("not found"), + Trace: "existing trace", + } + + he := Error(500, inner) + + // Should keep existing ID + if he.ID != "existing_id" { + t.Errorf("ID = %q, want 'existing_id'", he.ID) + } + // Should keep existing StatusCode + if he.StatusCode != 404 { + t.Errorf("StatusCode = %d, want 404 (existing)", he.StatusCode) + } + // Should keep existing Trace + if he.Trace != "existing trace" { + t.Errorf("Trace = %q, want 'existing trace'", he.Trace) + } + }) + + t.Run("fills missing fields in existing HandlerError", func(t *testing.T) { + inner := HandlerError{ + Err: fmt.Errorf("inner error"), + // ID, StatusCode, and Trace are all empty + } + + he := Error(503, inner) + + if he.ID == "" { + t.Error("should fill missing ID") + } + if he.StatusCode != 503 { + t.Errorf("should fill missing StatusCode with %d, got %d", 503, he.StatusCode) + } + if he.Trace == "" { + t.Error("should fill missing Trace") + } + }) + + t.Run("generates unique IDs", func(t *testing.T) { + ids := make(map[string]struct{}) + for i := 0; i < 100; i++ { + he := Error(500, fmt.Errorf("error %d", i)) + if _, exists := ids[he.ID]; exists { + t.Errorf("duplicate ID generated: %s", he.ID) + } + ids[he.ID] = struct{}{} + } + }) +} + +func TestErrorAsHandlerError(t *testing.T) { + he := Error(404, fmt.Errorf("not found")) + var target HandlerError + if !errors.As(he, &target) { + t.Error("Error() result should be assertable as HandlerError via errors.As") + } +} + +func TestHandlerErrorWithWrappedError(t *testing.T) { + // Test that errors.As can unwrap a wrapped HandlerError + inner := HandlerError{ + ID: "inner", + StatusCode: 404, + Err: fmt.Errorf("inner error"), + } + wrapped := fmt.Errorf("wrapped: %w", inner) + + he := Error(500, wrapped) + // Since wrapped contains a HandlerError, it should be unwrapped + if he.ID != "inner" { + t.Errorf("should unwrap to inner ID 'inner', got %q", he.ID) + } +}