package errors import ( "errors" "fmt" "net/http" "strings" "testing" ) func TestNew(t *testing.T) { t.Parallel() e := New("not_found", "用户不存在") if e.Code != "not_found" { t.Fatalf("Code = %q, want %q", e.Code, "not_found") } if e.Message != "用户不存在" { t.Fatalf("Message = %q, want %q", e.Message, "用户不存在") } if e.Error() != "用户不存在" { t.Fatalf("Error() = %q, want Message", e.Error()) } if e.Cause != nil { t.Fatalf("Cause = %v, want nil", e.Cause) } if e.HTTPCode != 0 { t.Fatalf("HTTPCode = %d, want 0 (unset)", e.HTTPCode) } } func TestNew_UnwrapNil(t *testing.T) { t.Parallel() if got := New("x", "y").Unwrap(); got != nil { t.Fatalf("Unwrap() = %v, want nil for New", got) } } func TestWrap(t *testing.T) { t.Parallel() original := ErrNotFound wrapped := Wrap(original, "not_found", "找不到用户 %s", "alice") if wrapped.Cause != original { t.Fatalf("Cause = %v, want original ErrNotFound", wrapped.Cause) } if wrapped.Message != "找不到用户 alice" { t.Fatalf("Message = %q, want %q", wrapped.Message, "找不到用户 alice") } if wrapped.Unwrap() != original { t.Fatalf("Unwrap() != original cause") } if !errors.Is(wrapped, original) { t.Fatalf("errors.Is(err, original) = false, want true (%%w penetration)") } if !errors.Is(wrapped, ErrNotFound) { t.Fatalf("errors.Is(err, ErrNotFound) = false, want true") } } func TestWrap_DoesNotOverMatch(t *testing.T) { t.Parallel() // errors.Is must not match an unrelated sentinel solely because the wrapper // uses that code string. wrapped := Wrap(ErrNotFound, "conflict", "写入冲突") if errors.Is(wrapped, ErrConflict) { t.Fatalf("errors.Is(err, ErrConflict) = true, want false") } } func TestWrap_ChainPenetrates(t *testing.T) { t.Parallel() outer := Wrap(Wrap(ErrNotFound, "internal", "加载配置失败"), "conflict", "写入冲突") if !errors.Is(outer, ErrNotFound) { t.Fatalf("errors.Is(outer, ErrNotFound) = false, want true through nested wraps") } var appErr *AppError if !errors.As(outer, &appErr) { t.Fatalf("errors.As(outer) = false, want true") } if appErr.Code != "conflict" { t.Fatalf("extracted Code = %q, want outer %q", appErr.Code, "conflict") } } func TestIsAppError(t *testing.T) { t.Parallel() cases := []struct { name string err error want bool }{ {"sentinel", ErrNotFound, true}, {"wrapped", Wrap(ErrNotFound, "not_found", "missing"), true}, {"nested_wrap", Wrap(Wrap(ErrNotFound, "internal", "x"), "bad_request", "y"), true}, {"std_error", fmt.Errorf("boom"), false}, {"nil", nil, false}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() if got := IsAppError(c.err); got != c.want { t.Fatalf("IsAppError(%v) = %v, want %v", c.err, got, c.want) } }) } } func TestAs(t *testing.T) { t.Parallel() outer := Wrap(Wrap(ErrNotFound, "internal", "加载失败"), "conflict", "写入冲突") appErr, ok := As(outer) if !ok { t.Fatalf("As(outer) ok = false, want true") } if appErr.Code != "conflict" { t.Fatalf("As code = %q, want outer %q", appErr.Code, "conflict") } _, ok = As(fmt.Errorf("boom")) if ok { t.Fatalf("As(std error) ok = true, want false") } // a bare wrapped AppError chain: As must extract the typed error inner := Wrap(ErrUnauthorized, "unauthorized", "请先登录") extracted, ok := As(inner) if !ok { t.Fatalf("As(inner) ok = false, want true") } if extracted != inner { t.Fatalf("As(inner) != inner") } } func TestHTTPStatus(t *testing.T) { t.Parallel() cases := []struct { name string err error want int }{ {"internal", ErrInternal, http.StatusInternalServerError}, {"not_found", ErrNotFound, http.StatusNotFound}, {"conflict", ErrConflict, http.StatusConflict}, {"unauthorized", ErrUnauthorized, http.StatusUnauthorized}, {"forbidden", ErrForbidden, http.StatusForbidden}, {"bad_request", ErrBadRequest, http.StatusBadRequest}, {"rate_limited", ErrRateLimited, http.StatusTooManyRequests}, {"timeout", ErrTimeout, http.StatusServiceUnavailable}, {"std_error", fmt.Errorf("boom"), http.StatusInternalServerError}, {"nil", nil, http.StatusInternalServerError}, {"custom_no_http", New("custom", "x"), http.StatusInternalServerError}, {"wrap_inherits_cause", Wrap(ErrNotFound, "not_found", "missing"), http.StatusNotFound}, {"wrap_std_error", Wrap(fmt.Errorf("db down"), "internal", "查询失败"), http.StatusInternalServerError}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() if got := HTTPStatus(c.err); got != c.want { t.Fatalf("HTTPStatus(%v) = %d, want %d", c.err, got, c.want) } }) } } func TestToJSON(t *testing.T) { t.Parallel() e := Wrap(ErrNotFound, "not_found", "资源不存在") js := ToJSON(e) if len(js) != 1 { t.Fatalf("ToJSON keys = %d, want exactly 1 (error)", len(js)) } obj, ok := js["error"].(map[string]any) if !ok { t.Fatalf("error field is not a map, got %T", js["error"]) } if got := obj["code"]; got != "not_found" { t.Fatalf("code = %v, want %q", got, "not_found") } if got := obj["message"]; got != "资源不存在" { t.Fatalf("message = %v, want %q", got, "资源不存在") } } func TestToJSON_AlignsClientContract(t *testing.T) { t.Parallel() js := ToJSON(ErrUnauthorized) obj := js["error"].(map[string]any) if obj["code"] != "unauthorized" { t.Fatalf("code = %v, want %q", obj["code"], "unauthorized") } if obj["message"] != "unauthorized" { t.Fatalf("message = %v, want %q", obj["message"], "unauthorized") } if _, exists := js["data"]; exists { t.Fatalf("ToJSON leaked a data key; contract is only {error:{code,message}}") } } func TestToJSON_DoesNotExposeInternal(t *testing.T) { t.Parallel() raw := fmt.Errorf("connection refused: host=db-internal password=secret") js := ToJSON(raw) obj := js["error"].(map[string]any) if obj["code"] != "internal" { t.Fatalf("code = %v, want %q", obj["code"], "internal") } if obj["message"] != "internal error" { t.Fatalf("message = %v, want %q", obj["message"], "internal error") } body := fmt.Sprintf("%v", js) if strings.Contains(body, "secret") || strings.Contains(body, "db-internal") { t.Fatalf("ToJSON leaked internal detail: %s", body) } } func TestWrap_SafeMessageKeepsCause(t *testing.T) { t.Parallel() raw := fmt.Errorf("insert failed: PRIMARY KEY violation") wrapped := Wrap(raw, "conflict", "资源已存在") obj := ToJSON(wrapped)["error"].(map[string]any) if obj["message"] != "资源已存在" { t.Fatalf("message = %v, want %q", obj["message"], "资源已存在") } if strings.Contains(obj["message"].(string), "PRIMARY KEY") { t.Fatalf("user-facing message leaked internal detail: %s", obj["message"]) } // The raw cause is still preserved for logging/diagnostics. if wrapped.Cause != raw { t.Fatalf("Cause not preserved, got %v", wrapped.Cause) } }