errors_test.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. // errors 包单测(纯逻辑,无 DB/外部依赖)。
  2. //
  3. // 重点覆盖 WithCause 的解包链契约:
  4. // WithCause(sentinel, cause) 返回的错误必须能被 errors.Is 命中**源 sentinel**,
  5. // 同时底层 cause 仍可解出。这是 T4-2 仓储层实证暴露的 latent bug 的回归保护
  6. // (旧实现 Unwrap 只回 Cause,sentinel 副本被拷贝后丢失,errors.Is 永远 false)。
  7. //
  8. // 包名说明:本文件位于 internal/infra/errors 包内,与包自身同名,故标准库 errors
  9. // 一律用别名 stderrors,避免与包内符号(ErrNotFound/WithCause 等)混淆。
  10. package errors
  11. import (
  12. stderrors "errors"
  13. "fmt"
  14. "testing"
  15. )
  16. // TestWithCause_IsHitsBaseSentinel WithCause 的错误必须 errors.Is 命中源 sentinel。
  17. func TestWithCause_IsHitsBaseSentinel(t *testing.T) {
  18. cause := fmt.Errorf("sql: no rows in result set")
  19. wrapped := WithCause(ErrNotFound, cause)
  20. // 源 sentinel 必须可被 errors.Is 命中(Service/Handler 层依赖此契约做分支)
  21. if !stderrors.Is(wrapped, ErrNotFound) {
  22. t.Errorf("errors.Is(WithCause(ErrNotFound, cause), ErrNotFound) = false,期望 true")
  23. }
  24. // 底层 cause 仍可达(日志排障链路不断)
  25. if !stderrors.Is(wrapped, cause) {
  26. t.Errorf("errors.Is(WithCause, cause) = false,底层 cause 丢失")
  27. }
  28. }
  29. // TestWithCause_NilBase 兜底:base 为 nil 时回退 ErrInternal 且仍可命中。
  30. func TestWithCause_NilBase(t *testing.T) {
  31. cause := fmt.Errorf("boom")
  32. wrapped := WithCause(nil, cause)
  33. if wrapped.Code != ErrInternal.Code {
  34. t.Errorf("base=nil 应回退 ErrInternal code=%d,实际 %d", ErrInternal.Code, wrapped.Code)
  35. }
  36. if !stderrors.Is(wrapped, ErrInternal) {
  37. t.Errorf("errors.Is(WithCause(nil, cause), ErrInternal) = false,期望 true")
  38. }
  39. }
  40. // TestNew_NoBase New 创建的自定义错误:无 base、无 cause,Unwrap 返回空链。
  41. func TestNew_NoBase(t *testing.T) {
  42. e := New(3002, "该手机号已被绑定")
  43. if e.Code != 3002 || e.Msg != "该手机号已被绑定" {
  44. t.Errorf("New 字段异常: code=%d msg=%q", e.Code, e.Msg)
  45. }
  46. if got := e.Unwrap(); len(got) != 0 {
  47. t.Errorf("New 创建的错误 Unwrap 应返回空链,实际 %v", got)
  48. }
  49. }
  50. // TestWithCause_CauseAndBaseSame 去重:把 sentinel 本身当 cause 传入,解包链只保留一份。
  51. func TestWithCause_CauseAndBaseSame(t *testing.T) {
  52. wrapped := WithCause(ErrConflict, ErrConflict) // cause == base(同一 *AppError)
  53. chain := wrapped.Unwrap()
  54. if len(chain) != 1 {
  55. t.Errorf("cause==base 应去重为 1 项,实际 %d 项", len(chain))
  56. }
  57. }