norm_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. package norm
  2. import "testing"
  3. // TestPhone 验证 phone 强归一规则(P0-06 §2.1 五步顺序)。
  4. func TestPhone(t *testing.T) {
  5. cases := []struct {
  6. name string
  7. raw string
  8. wantCanon string
  9. wantOK bool
  10. }{
  11. // 标准 11 位
  12. {"纯 11 位", "13800000000", "13800000000", true},
  13. {"前后空白", " 13800000000 ", "13800000000", true},
  14. // 去分隔符
  15. {"横线分隔", "138-0000-0000", "13800000000", true},
  16. {"点号分隔", "138.0000.0000", "13800000000", true},
  17. {"空格分隔", "138 0000 0000", "13800000000", true},
  18. // 国际前缀
  19. {"+86 前缀", "+8613800000000", "13800000000", true},
  20. {"0086 前缀", "008613800000000", "13800000000", true},
  21. {"86 前缀无加号", "8613800000000", "13800000000", true},
  22. {"0086 后非大陆号段", "008610800000000", "", false}, // 剥 0086 后 108... 号段不符 → 不剥,整体也非 11 位 → false
  23. {"86 后非大陆号段", "8610800000000", "", false},
  24. // 异常
  25. {"空串", "", "", false},
  26. {"仅空白", " ", "", false},
  27. {"10 位", "1380000000", "", false},
  28. {"12 位", "138000000001", "", false},
  29. {"号段不符 10 开头", "10800000000", "", false},
  30. {"号段不符 12 开头", "12800000000", "", false},
  31. {"含字母", "1380000000a", "", false},
  32. }
  33. for _, c := range cases {
  34. t.Run(c.name, func(t *testing.T) {
  35. got, ok := Phone(c.raw)
  36. if got != c.wantCanon || ok != c.wantOK {
  37. t.Fatalf("Phone(%q) = (%q, %v), want (%q, %v)", c.raw, got, ok, c.wantCanon, c.wantOK)
  38. }
  39. })
  40. }
  41. }
  42. // TestPhoneIdempotent 验证幂等性:归一(归一(x)) == 归一(x)。
  43. func TestPhoneIdempotent(t *testing.T) {
  44. raws := []string{"+8613800000000", "138-0000-0000", "138.0000.0000", " 13800000000 ", "8613800000000"}
  45. for _, raw := range raws {
  46. first, ok1 := Phone(raw)
  47. if !ok1 {
  48. t.Fatalf("Phone(%q) 首次归一失败,无法验证幂等", raw)
  49. }
  50. second, ok2 := Phone(first)
  51. if second != first || !ok2 {
  52. t.Fatalf("Phone 非幂等:Phone(%q)=(%q,%v) → 再归一 (%q,%v)", raw, first, ok1, second, ok2)
  53. }
  54. }
  55. }
  56. // TestWechatOpenID 验证 wechat openid 仅 trim。
  57. func TestWechatOpenID(t *testing.T) {
  58. cases := []struct {
  59. name string
  60. raw string
  61. wantCanon string
  62. wantOK bool
  63. }{
  64. {"标准", "openid_abc123", "openid_abc123", true},
  65. {"含空白", " openid_abc123 ", "openid_abc123", true},
  66. {"空串", "", "", false},
  67. {"仅空白", " ", "", false},
  68. }
  69. for _, c := range cases {
  70. t.Run(c.name, func(t *testing.T) {
  71. got, ok := WechatOpenID(c.raw)
  72. if got != c.wantCanon || ok != c.wantOK {
  73. t.Fatalf("WechatOpenID(%q) = (%q, %v), want (%q, %v)", c.raw, got, ok, c.wantCanon, c.wantOK)
  74. }
  75. })
  76. }
  77. }
  78. // TestEmail 验证 email 轻归一(trim + 小写 + 正则)。
  79. func TestEmail(t *testing.T) {
  80. cases := []struct {
  81. name string
  82. raw string
  83. wantCanon string
  84. wantOK bool
  85. }{
  86. {"标准", "User@Example.COM", "user@example.com", true},
  87. {"含空白", " user@example.com ", "user@example.com", true},
  88. {"保留 +tag", "user+tag@example.com", "user+tag@example.com", true},
  89. {"缺域名点", "user@examplecom", "", false},
  90. {"缺 @", "userexample.com", "", false},
  91. {"空串", "", "", false},
  92. }
  93. for _, c := range cases {
  94. t.Run(c.name, func(t *testing.T) {
  95. got, ok := Email(c.raw)
  96. if got != c.wantCanon || ok != c.wantOK {
  97. t.Fatalf("Email(%q) = (%q, %v), want (%q, %v)", c.raw, got, ok, c.wantCanon, c.wantOK)
  98. }
  99. })
  100. }
  101. }
  102. // TestUsername 验证 username 轻归一(trim + 剔除不可见字符 + 小写,无 ok)。
  103. func TestUsername(t *testing.T) {
  104. cases := []struct {
  105. name string
  106. raw string
  107. want string
  108. }{
  109. {"标准", "ZhangSan", "zhangsan"},
  110. {"含空白", " ZhangSan ", "zhangsan"},
  111. {"空串", "", ""},
  112. {"已小写", "abc123", "abc123"},
  113. // 回归(e2e 实测 yuansiwei_i.t_user id=44008):
  114. // U+202D LTR-MARK + 号码 + U+202C POP DIRECTIONAL FORMATTING 包裹 →
  115. // 剔除后与干净值同串,命中"首个占用者"规则而非字节不同绕过去重撞唯一键。
  116. {"bidi 控制符包裹", "\u202D18077117616\u202C", "18077117616"},
  117. {"零宽空格 U+200B (Cf)", "a\u200Bb", "ab"},
  118. {"零宽非连接符 U+200C + 软连字符 U+00AD", "a\u200Cb\u00ADC", "abc"},
  119. {"双向嵌入 U+202A 中段", "Zhang\u202ASan\u202C", "zhangsan"},
  120. }
  121. for _, c := range cases {
  122. t.Run(c.name, func(t *testing.T) {
  123. got := Username(c.raw)
  124. if got != c.want {
  125. t.Fatalf("Username(%q) = %q, want %q", c.raw, got, c.want)
  126. }
  127. })
  128. }
  129. }
  130. // TestUsernameCollisionAfterStrip 验证"同账号字节不同"在归一后收敛为同一串
  131. // (44008 脏 username 与 42020 干净 username 归一结果相等 → 迁移侧首个占用者规则生效)。
  132. func TestUsernameCollisionAfterStrip(t *testing.T) {
  133. clean := Username("18077117616")
  134. dirty := Username("\u202D18077117616\u202C")
  135. if clean != dirty {
  136. t.Fatalf("归一后应相等(同账号不同字节形态):clean=%q dirty=%q", clean, dirty)
  137. }
  138. }
  139. // TestEmailInvisibleStripped 验证 email 归一同样剔除不可见字符(t_identity.email 亦走唯一键)。
  140. func TestEmailInvisibleStripped(t *testing.T) {
  141. canon, ok := Email("user\u200Bname@example.com")
  142. if !ok || canon != "username@example.com" {
  143. t.Fatalf("Email 应剔除零宽空格并归一:got=(%q,%v)", canon, ok)
  144. }
  145. }