package xerrors import ( "encoding/json" "errors" "fmt" "strings" "testing" ) func TestNew(t *testing.T) { err := New(ErrInvalidInput, "test message") if err.code != ErrInvalidInput { t.Errorf("expected code %s, got %s", ErrInvalidInput, err.code) } if err.message != "test message" { t.Errorf("expected message %q, got %q", "test message", err.message) } if err.err != nil { t.Errorf("expected nil cause, got %v", err.err) } if err.fields != nil { t.Errorf("expected nil fields, got %v", err.fields) } } func TestWrap(t *testing.T) { cause := errors.New("original error") err := Wrap(ErrInternal, "wrapped message", cause) if err.code != ErrInternal { t.Errorf("expected code %s, got %s", ErrInternal, err.code) } if err.message != "wrapped message" { t.Errorf("expected message %q, got %q", "wrapped message", err.message) } if err.err != cause { t.Errorf("expected cause %v, got %v", cause, err.err) } } func TestConvenienceConstructors(t *testing.T) { t.Run("InvalidInput", func(t *testing.T) { err := InvalidInput("field %s is required", "email") if err.code != ErrInvalidInput { t.Errorf("expected code %s, got %s", ErrInvalidInput, err.code) } if err.message != "field email is required" { t.Errorf("unexpected message: %s", err.message) } }) t.Run("NotFound", func(t *testing.T) { err := NotFound("user %d not found", 42) if err.code != ErrNotFound { t.Errorf("expected code %s, got %s", ErrNotFound, err.code) } if err.message != "user 42 not found" { t.Errorf("unexpected message: %s", err.message) } }) t.Run("Internal", func(t *testing.T) { err := Internal("db error: %v", errors.New("conn lost")) if err.code != ErrInternal { t.Errorf("expected code %s, got %s", ErrInternal, err.code) } if err.message != "db error: conn lost" { t.Errorf("unexpected message: %s", err.message) } }) } func TestErr_Error(t *testing.T) { t.Run("without cause", func(t *testing.T) { err := New(ErrNotFound, "user not found") want := "NOT_FOUND: user not found" if err.Error() != want { t.Errorf("expected %q, got %q", want, err.Error()) } }) t.Run("with cause", func(t *testing.T) { err := New(ErrAlreadyExists, "conflict occurred").WithError(errors.New("db error")) want := "ALREADY_EXISTS: conflict occurred → db error" if err.Error() != want { t.Errorf("expected %q, got %q", want, err.Error()) } }) } func TestErr_Unwrap(t *testing.T) { sentinel := errors.New("sentinel") wrapped := Wrap(ErrInternal, "something failed", sentinel) if !errors.Is(wrapped, sentinel) { t.Error("errors.Is should find sentinel through Unwrap") } var target *Err outer := fmt.Errorf("outer: %w", wrapped) if !errors.As(outer, &target) { t.Error("errors.As should find *Err through fmt.Errorf wrapping") } } func TestErr_Detailed(t *testing.T) { err := New(ErrInvalidInput, "invalid name"). WithContext("field", "name"). WithError(errors.New("too short")) d := err.Detailed() if !strings.Contains(d, "code: INVALID_ARGUMENT") { t.Errorf("Detailed missing code, got: %s", d) } if !strings.Contains(d, "message: invalid name") { t.Errorf("Detailed missing message, got: %s", d) } if !strings.Contains(d, "cause: too short") { t.Errorf("Detailed missing cause, got: %s", d) } if !strings.Contains(d, "fields:") { t.Errorf("Detailed missing fields, got: %s", d) } } func TestErr_Accessors(t *testing.T) { err := New(ErrInvalidInput, "bad input"). WithContext("k", "v") if err.Code() != ErrInvalidInput { t.Errorf("Code() = %s, want %s", err.Code(), ErrInvalidInput) } if err.Message() != "bad input" { t.Errorf("Message() = %q, want %q", err.Message(), "bad input") } fields := err.Fields() if fields["k"] != "v" { t.Errorf("Fields()[k] = %v, want v", fields["k"]) } } func TestErr_Fields_DefensiveCopy(t *testing.T) { err := New(ErrInternal, "err").WithContext("key", "original") fields := err.Fields() fields["key"] = "mutated" // The internal state must not be affected. if err.fields["key"] != "original" { t.Error("Fields() returned the internal map directly; mutation affected the error") } } func TestErr_Fields_EmptyMap(t *testing.T) { err := New(ErrInternal, "no fields") fields := err.Fields() if fields == nil { t.Error("Fields() must return a non-nil map even when no fields are set") } if len(fields) != 0 { t.Errorf("Fields() must return an empty map, got %v", fields) } } func TestErr_WithContext_Chaining(t *testing.T) { err := New(ErrInvalidInput, "multi-field error"). WithContext("field1", "a"). WithContext("field2", "b") if err.fields["field1"] != "a" || err.fields["field2"] != "b" { t.Errorf("WithContext chaining failed, fields: %v", err.fields) } } func TestErr_WithContext_Overwrite(t *testing.T) { err := New(ErrInvalidInput, "msg"). WithContext("key", "first"). WithContext("key", "second") if err.fields["key"] != "second" { t.Errorf("expected overwrite to second, got %v", err.fields["key"]) } } func TestErr_MarshalJSON(t *testing.T) { t.Run("with fields", func(t *testing.T) { err := New(ErrNotFound, "user not found").WithContext("id", "42") b, jsonErr := json.Marshal(err) if jsonErr != nil { t.Fatalf("MarshalJSON error: %v", jsonErr) } var out map[string]any if jsonErr = json.Unmarshal(b, &out); jsonErr != nil { t.Fatalf("unmarshal error: %v", jsonErr) } if out["code"] != "NOT_FOUND" { t.Errorf("json code = %v, want NOT_FOUND", out["code"]) } if out["message"] != "user not found" { t.Errorf("json message = %v, want 'user not found'", out["message"]) } if out["fields"] == nil { t.Error("json fields key missing") } }) t.Run("without fields omitempty", func(t *testing.T) { err := New(ErrInternal, "boom") b, jsonErr := json.Marshal(err) if jsonErr != nil { t.Fatalf("MarshalJSON error: %v", jsonErr) } if strings.Contains(string(b), "fields") { t.Errorf("json should omit fields key when empty, got: %s", b) } }) } func TestErr_DuckTyping_ErrorCode(t *testing.T) { err := New(ErrPermissionDenied, "not allowed") if err.ErrorCode() != "PERMISSION_DENIED" { t.Errorf("ErrorCode() = %s, want PERMISSION_DENIED", err.ErrorCode()) } } func TestErr_DuckTyping_ErrorContext(t *testing.T) { err := New(ErrInvalidInput, "msg").WithContext("field", "email") ctx := err.ErrorContext() if ctx["field"] != "email" { t.Errorf("ErrorContext()[field] = %v, want email", ctx["field"]) } } func TestErr_DuckTyping_ErrorContext_Nil(t *testing.T) { err := New(ErrInternal, "no fields") // ErrorContext returns the raw internal map — nil is acceptable here // (logz handles nil maps in its enrichment loop). _ = err.ErrorContext() } func TestCode_Description(t *testing.T) { tests := []struct { code Code want string }{ {ErrInvalidInput, "Invalid input provided"}, {ErrUnauthorized, "Authentication required"}, {ErrPermissionDenied, "Insufficient permissions"}, {ErrNotFound, "Resource not found"}, {ErrAlreadyExists, "Resource already exists"}, {ErrGone, "Resource permanently deleted"}, {ErrPreconditionFailed, "Precondition not met"}, {ErrRateLimited, "Rate limit exceeded"}, {ErrCancelled, "Request cancelled"}, {ErrInternal, "Internal error"}, {ErrNotImplemented, "Not implemented"}, {ErrUnavailable, "Service unavailable"}, {ErrDeadlineExceeded, "Deadline exceeded"}, {Code("CUSTOM_CODE"), "CUSTOM_CODE"}, } for _, tt := range tests { if got := tt.code.Description(); got != tt.want { t.Errorf("Code(%s).Description() = %q, want %q", tt.code, got, tt.want) } } }