package norm import "testing" // TestPhone 验证 phone 强归一规则(P0-06 §2.1 五步顺序)。 func TestPhone(t *testing.T) { cases := []struct { name string raw string wantCanon string wantOK bool }{ // 标准 11 位 {"纯 11 位", "13800000000", "13800000000", true}, {"前后空白", " 13800000000 ", "13800000000", true}, // 去分隔符 {"横线分隔", "138-0000-0000", "13800000000", true}, {"点号分隔", "138.0000.0000", "13800000000", true}, {"空格分隔", "138 0000 0000", "13800000000", true}, // 国际前缀 {"+86 前缀", "+8613800000000", "13800000000", true}, {"0086 前缀", "008613800000000", "13800000000", true}, {"86 前缀无加号", "8613800000000", "13800000000", true}, {"0086 后非大陆号段", "008610800000000", "", false}, // 剥 0086 后 108... 号段不符 → 不剥,整体也非 11 位 → false {"86 后非大陆号段", "8610800000000", "", false}, // 异常 {"空串", "", "", false}, {"仅空白", " ", "", false}, {"10 位", "1380000000", "", false}, {"12 位", "138000000001", "", false}, {"号段不符 10 开头", "10800000000", "", false}, {"号段不符 12 开头", "12800000000", "", false}, {"含字母", "1380000000a", "", false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got, ok := Phone(c.raw) if got != c.wantCanon || ok != c.wantOK { t.Fatalf("Phone(%q) = (%q, %v), want (%q, %v)", c.raw, got, ok, c.wantCanon, c.wantOK) } }) } } // TestPhoneIdempotent 验证幂等性:归一(归一(x)) == 归一(x)。 func TestPhoneIdempotent(t *testing.T) { raws := []string{"+8613800000000", "138-0000-0000", "138.0000.0000", " 13800000000 ", "8613800000000"} for _, raw := range raws { first, ok1 := Phone(raw) if !ok1 { t.Fatalf("Phone(%q) 首次归一失败,无法验证幂等", raw) } second, ok2 := Phone(first) if second != first || !ok2 { t.Fatalf("Phone 非幂等:Phone(%q)=(%q,%v) → 再归一 (%q,%v)", raw, first, ok1, second, ok2) } } } // TestWechatOpenID 验证 wechat openid 仅 trim。 func TestWechatOpenID(t *testing.T) { cases := []struct { name string raw string wantCanon string wantOK bool }{ {"标准", "openid_abc123", "openid_abc123", true}, {"含空白", " openid_abc123 ", "openid_abc123", true}, {"空串", "", "", false}, {"仅空白", " ", "", false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got, ok := WechatOpenID(c.raw) if got != c.wantCanon || ok != c.wantOK { t.Fatalf("WechatOpenID(%q) = (%q, %v), want (%q, %v)", c.raw, got, ok, c.wantCanon, c.wantOK) } }) } } // TestEmail 验证 email 轻归一(trim + 小写 + 正则)。 func TestEmail(t *testing.T) { cases := []struct { name string raw string wantCanon string wantOK bool }{ {"标准", "User@Example.COM", "user@example.com", true}, {"含空白", " user@example.com ", "user@example.com", true}, {"保留 +tag", "user+tag@example.com", "user+tag@example.com", true}, {"缺域名点", "user@examplecom", "", false}, {"缺 @", "userexample.com", "", false}, {"空串", "", "", false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got, ok := Email(c.raw) if got != c.wantCanon || ok != c.wantOK { t.Fatalf("Email(%q) = (%q, %v), want (%q, %v)", c.raw, got, ok, c.wantCanon, c.wantOK) } }) } } // TestUsername 验证 username 轻归一(trim + 剔除不可见字符 + 小写,无 ok)。 func TestUsername(t *testing.T) { cases := []struct { name string raw string want string }{ {"标准", "ZhangSan", "zhangsan"}, {"含空白", " ZhangSan ", "zhangsan"}, {"空串", "", ""}, {"已小写", "abc123", "abc123"}, // 回归(e2e 实测 yuansiwei_i.t_user id=44008): // U+202D LTR-MARK + 号码 + U+202C POP DIRECTIONAL FORMATTING 包裹 → // 剔除后与干净值同串,命中"首个占用者"规则而非字节不同绕过去重撞唯一键。 {"bidi 控制符包裹", "\u202D18077117616\u202C", "18077117616"}, {"零宽空格 U+200B (Cf)", "a\u200Bb", "ab"}, {"零宽非连接符 U+200C + 软连字符 U+00AD", "a\u200Cb\u00ADC", "abc"}, {"双向嵌入 U+202A 中段", "Zhang\u202ASan\u202C", "zhangsan"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := Username(c.raw) if got != c.want { t.Fatalf("Username(%q) = %q, want %q", c.raw, got, c.want) } }) } } // TestUsernameCollisionAfterStrip 验证"同账号字节不同"在归一后收敛为同一串 // (44008 脏 username 与 42020 干净 username 归一结果相等 → 迁移侧首个占用者规则生效)。 func TestUsernameCollisionAfterStrip(t *testing.T) { clean := Username("18077117616") dirty := Username("\u202D18077117616\u202C") if clean != dirty { t.Fatalf("归一后应相等(同账号不同字节形态):clean=%q dirty=%q", clean, dirty) } } // TestEmailInvisibleStripped 验证 email 归一同样剔除不可见字符(t_identity.email 亦走唯一键)。 func TestEmailInvisibleStripped(t *testing.T) { canon, ok := Email("user\u200Bname@example.com") if !ok || canon != "username@example.com" { t.Fatalf("Email 应剔除零宽空格并归一:got=(%q,%v)", canon, ok) } }