affiliate_service_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "math"
  6. "testing"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // TestResolveRebateRatePercent_PerUserOverride verifies that per-inviter
  10. // AffRebateRatePercent overrides the global rate, that NULL falls back to the
  11. // global rate, and that out-of-range exclusive rates are clamped silently.
  12. //
  13. // SettingService is left nil here so globalRebateRatePercent returns the
  14. // documented default (AffiliateRebateRateDefault = 20%) — this exercises the
  15. // fallback path without spinning up a settings stub.
  16. func TestResolveRebateRatePercent_PerUserOverride(t *testing.T) {
  17. t.Parallel()
  18. svc := &AffiliateService{}
  19. // nil exclusive rate → falls back to global default (20%)
  20. require.InDelta(t, AffiliateRebateRateDefault,
  21. svc.resolveRebateRatePercent(context.Background(), &AffiliateSummary{}), 1e-9)
  22. // exclusive rate set → overrides global
  23. rate := 50.0
  24. require.InDelta(t, 50.0,
  25. svc.resolveRebateRatePercent(context.Background(), &AffiliateSummary{AffRebateRatePercent: &rate}), 1e-9)
  26. // exclusive rate 0 → returns 0 (no rebate, intentional)
  27. zero := 0.0
  28. require.InDelta(t, 0.0,
  29. svc.resolveRebateRatePercent(context.Background(), &AffiliateSummary{AffRebateRatePercent: &zero}), 1e-9)
  30. // exclusive rate above max → clamped to Max
  31. tooHigh := 250.0
  32. require.InDelta(t, AffiliateRebateRateMax,
  33. svc.resolveRebateRatePercent(context.Background(), &AffiliateSummary{AffRebateRatePercent: &tooHigh}), 1e-9)
  34. // exclusive rate below min → clamped to Min
  35. tooLow := -5.0
  36. require.InDelta(t, AffiliateRebateRateMin,
  37. svc.resolveRebateRatePercent(context.Background(), &AffiliateSummary{AffRebateRatePercent: &tooLow}), 1e-9)
  38. }
  39. // TestIsEnabled_NilSettingServiceReturnsDefault verifies that IsEnabled
  40. // safely handles a nil settingService dependency by returning the default
  41. // (off). This protects callers from nil-pointer crashes in misconfigured
  42. // environments.
  43. func TestIsEnabled_NilSettingServiceReturnsDefault(t *testing.T) {
  44. t.Parallel()
  45. svc := &AffiliateService{}
  46. require.False(t, svc.IsEnabled(context.Background()))
  47. require.Equal(t, AffiliateEnabledDefault, svc.IsEnabled(context.Background()))
  48. }
  49. // TestValidateExclusiveRate_BoundaryAndInvalid covers the validator used by
  50. // admin-facing rate setters: nil is always valid (clear), in-range values
  51. // are accepted, NaN/Inf and out-of-range values produce a typed BadRequest.
  52. func TestValidateExclusiveRate_BoundaryAndInvalid(t *testing.T) {
  53. t.Parallel()
  54. require.NoError(t, validateExclusiveRate(nil))
  55. for _, v := range []float64{0, 0.01, 50, 99.99, 100} {
  56. v := v
  57. require.NoError(t, validateExclusiveRate(&v), "value %v should be valid", v)
  58. }
  59. for _, v := range []float64{-0.01, 100.01, -100, 200} {
  60. v := v
  61. require.Error(t, validateExclusiveRate(&v), "value %v should be rejected", v)
  62. }
  63. nan := math.NaN()
  64. require.Error(t, validateExclusiveRate(&nan))
  65. posInf := math.Inf(1)
  66. require.Error(t, validateExclusiveRate(&posInf))
  67. negInf := math.Inf(-1)
  68. require.Error(t, validateExclusiveRate(&negInf))
  69. }
  70. func TestMaskEmail(t *testing.T) {
  71. t.Parallel()
  72. require.Equal(t, "a***@g***.com", maskEmail("alice@gmail.com"))
  73. require.Equal(t, "x***@d***", maskEmail("x@domain"))
  74. require.Equal(t, "", maskEmail(""))
  75. }
  76. func TestIsValidAffiliateCodeFormat(t *testing.T) {
  77. t.Parallel()
  78. // 邀请码格式校验同时服务于:
  79. // 1) 系统自动生成的 12 位随机码(A-Z 去 I/O,2-9 去 0/1)
  80. // 2) 管理员设置的自定义专属码(如 "VIP2026"、"NEW_USER-1")
  81. // 因此校验放宽到 [A-Z0-9_-]{4,32}(要求调用方先 ToUpper)。
  82. cases := []struct {
  83. name string
  84. in string
  85. want bool
  86. }{
  87. {"valid canonical 12-char", "ABCDEFGHJKLM", true},
  88. {"valid all digits 2-9", "234567892345", true},
  89. {"valid mixed", "A2B3C4D5E6F7", true},
  90. {"valid admin custom short", "VIP1", true},
  91. {"valid admin custom with hyphen", "NEW-USER", true},
  92. {"valid admin custom with underscore", "VIP_2026", true},
  93. {"valid 32-char max", "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345", true},
  94. // Previously-excluded chars (I/O/0/1) are now allowed since admins may use them.
  95. {"letter I now allowed", "IBCDEFGHJKLM", true},
  96. {"letter O now allowed", "OBCDEFGHJKLM", true},
  97. {"digit 0 now allowed", "0BCDEFGHJKLM", true},
  98. {"digit 1 now allowed", "1BCDEFGHJKLM", true},
  99. {"too short (3 chars)", "ABC", false},
  100. {"too long (33 chars)", "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456", false},
  101. {"lowercase rejected (caller must ToUpper first)", "abcdefghjklm", false},
  102. {"empty", "", false},
  103. {"utf8 non-ascii", "ÄÄÄÄÄÄ", false}, // bytes out of charset
  104. {"ascii punctuation .", "ABCDEFGHJK.M", false},
  105. {"whitespace", "ABCDEFGHJK M", false},
  106. }
  107. for _, tc := range cases {
  108. tc := tc
  109. t.Run(tc.name, func(t *testing.T) {
  110. t.Parallel()
  111. require.Equal(t, tc.want, isValidAffiliateCodeFormat(tc.in))
  112. })
  113. }
  114. }