codes_test.go 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  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. {"3143 重复报名", ErrEnrollmentDuplicate.Code, 3143},
  57. }
  58. for _, p := range pairs {
  59. if p.got != p.want {
  60. t.Fatalf("%s:code = %d,期望 %d", p.name, p.got, p.want)
  61. }
  62. }
  63. if len(All()) != len(pairs) {
  64. t.Fatalf("All() 数量 = %d,期望 %d(新增码须同步本测试与设计 §10.1)", len(All()), len(pairs))
  65. }
  66. }
  67. // TestAllReturnsCopy All() 返回新切片(调用方 append 不得污染内部全集)。
  68. func TestAllReturnsCopy(t *testing.T) {
  69. a := All()
  70. a = append(a, ErrBalanceInsufficient)
  71. if len(All()) != len(a)-1 {
  72. t.Fatal("All() 未返回新切片:内部集合被调用方污染")
  73. }
  74. }
  75. // TestIsAcademicCode 段位边界(含端点/邻接非法值)。
  76. func TestIsAcademicCode(t *testing.T) {
  77. cases := []struct {
  78. code int
  79. want bool
  80. }{
  81. {3099, false},
  82. {3100, true},
  83. {3199, true},
  84. {3200, false},
  85. {1004, false},
  86. {2001, false},
  87. }
  88. for _, tc := range cases {
  89. if got := IsAcademicCode(tc.code); got != tc.want {
  90. t.Fatalf("IsAcademicCode(%d) = %v,期望 %v", tc.code, got, tc.want)
  91. }
  92. }
  93. }