codes_test.go 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. // academic 31xx 业务错误码单测(纯断言,不连库)。
  2. //
  3. // 覆盖 AC-02 验收②:31xx 码全落 3100~3199 段、无重复、文案非空、
  4. // 且与 infra/errors 通用码段(1xxx/2xxx/3xxx/5xxx)不重叠。
  5. package apperr
  6. import "testing"
  7. // TestCodesInAcademicRange 断言全部业务码落在 3100~3199 且唯一。
  8. func TestCodesInAcademicRange(t *testing.T) {
  9. allCodes := All()
  10. if len(allCodes) == 0 {
  11. t.Fatal("apperr.All() 不应为空")
  12. }
  13. seen := make(map[int]string, len(allCodes))
  14. for _, e := range allCodes {
  15. if e == nil {
  16. t.Fatal("apperr.All() 含 nil")
  17. }
  18. if !IsAcademicCode(e.Code) {
  19. t.Fatalf("错误码 %d(%s)越出 academic 段位 3100~3199", e.Code, e.Msg)
  20. }
  21. if prev, dup := seen[e.Code]; dup {
  22. t.Fatalf("错误码 %d 重复定义:%q 与 %q", e.Code, prev, e.Msg)
  23. }
  24. seen[e.Code] = e.Msg
  25. if e.Msg == "" {
  26. t.Fatalf("错误码 %d 文案为空(禁止无文案 sentinel)", e.Code)
  27. }
  28. }
  29. }
  30. // TestKnownCodes 逐条断言设计 §10.1 的码值(防误改错位,造成前端契约漂移)。
  31. func TestKnownCodes(t *testing.T) {
  32. pairs := []struct {
  33. name string
  34. got int
  35. want int
  36. }{
  37. {"3101 余额不足", ErrBalanceInsufficient.Code, 3101},
  38. {"3102 余额为0不可退费", ErrNoBalanceToRefund.Code, 3102},
  39. {"3103 可退金额不足(warn)", ErrRefundableShort.Code, 3103},
  40. {"3104 校区不存在", ErrCampusNotFound.Code, 3104},
  41. {"3105 已报名", ErrActivityAlreadySigned.Code, 3105},
  42. {"3106 老带新限一次", ErrRecommendLimited.Code, 3106},
  43. {"3107 已审核不可取消", ErrActivitySignedAudited.Code, 3107},
  44. {"3108 老带新已消费", ErrRecommendUsed.Code, 3108},
  45. {"3109 赠送金额不足", ErrGiftAmountShort.Code, 3109},
  46. {"3110 活动不可用", ErrActivityUnavailable.Code, 3110},
  47. {"3111 分账额度超限", ErrDivideQuotaExceeded.Code, 3111},
  48. {"3112 课酬方案未配置", ErrPayrollPlanMissing.Code, 3112},
  49. {"3113 欠费核销失败", ErrArrearsCounteractFailed.Code, 3113},
  50. {"3114 幂等重复(内部)", ErrRepeated.Code, 3114},
  51. {"3120 清零前置不满足", ErrClearBalanceRejected.Code, 3120},
  52. {"3121 上课状态非法", ErrRecordStateInvalid.Code, 3121},
  53. {"3122 排班冲突", ErrScheduleConflict.Code, 3122},
  54. {"3140 业务参数错", ErrBusinessParam.Code, 3140},
  55. {"3141 报名人数已满", ErrActivityFull.Code, 3141},
  56. }
  57. for _, p := range pairs {
  58. if p.got != p.want {
  59. t.Fatalf("%s:code = %d,期望 %d", p.name, p.got, p.want)
  60. }
  61. }
  62. if len(All()) != len(pairs) {
  63. t.Fatalf("All() 数量 = %d,期望 %d(新增码须同步本测试与设计 §10.1)", len(All()), len(pairs))
  64. }
  65. }
  66. // TestAllReturnsCopy All() 返回新切片(调用方 append 不得污染内部全集)。
  67. func TestAllReturnsCopy(t *testing.T) {
  68. a := All()
  69. a = append(a, ErrBalanceInsufficient)
  70. if len(All()) != len(a)-1 {
  71. t.Fatal("All() 未返回新切片:内部集合被调用方污染")
  72. }
  73. }
  74. // TestIsAcademicCode 段位边界(含端点/邻接非法值)。
  75. func TestIsAcademicCode(t *testing.T) {
  76. cases := []struct {
  77. code int
  78. want bool
  79. }{
  80. {3099, false},
  81. {3100, true},
  82. {3199, true},
  83. {3200, false},
  84. {1004, false},
  85. {2001, false},
  86. }
  87. for _, tc := range cases {
  88. if got := IsAcademicCode(tc.code); got != tc.want {
  89. t.Fatalf("IsAcademicCode(%d) = %v,期望 %v", tc.code, got, tc.want)
  90. }
  91. }
  92. }