| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- package helper
- import (
- "encoding/json"
- "strings"
- "testing"
- "fwh-toolbox-server/domain"
- "fwh-toolbox-server/dto"
- )
- // ---------- 掩码函数单元测试 ----------
- func TestMaskName(t *testing.T) {
- cases := map[string]string{
- "": "",
- "张三": "张***",
- "John": "J***",
- "李": "李***",
- }
- for in, want := range cases {
- if got := maskName(in); got != want {
- t.Errorf("maskName(%q)=%q, want %q", in, got, want)
- }
- }
- }
- func TestMaskEmail(t *testing.T) {
- if got := maskEmail("john@corp.com"); got != "j***@corp.com" {
- t.Errorf("maskEmail got %q", got)
- }
- // 非法邮箱退化为完全脱敏
- if got := maskEmail("notanemail"); got != maskFull() {
- t.Errorf("maskEmail(invalid) got %q", got)
- }
- if got := maskEmail(""); got != "" {
- t.Errorf("maskEmail(empty) got %q", got)
- }
- }
- func TestMaskAccount(t *testing.T) {
- if got := maskAccount("abcdefgh"); got != "ab***gh" {
- t.Errorf("maskAccount got %q", got)
- }
- // 过短完全脱敏
- if got := maskAccount("abc"); got != maskFull() {
- t.Errorf("maskAccount(short) got %q", got)
- }
- }
- func TestMaskFullFixedLength(t *testing.T) {
- if got := maskFull(); got != strings.Repeat(MaskChar, fullMaskLen) {
- t.Errorf("maskFull got %q", got)
- }
- }
- // ---------- 优化 B:流式 token 脱敏测试 ----------
- func TestMaskJSONBytes_FlatFields(t *testing.T) {
- in := []byte(`{
- "sn": "SN123",
- "userName": "Alice",
- "contactNumber": "13812345678",
- "email": "a@b.com",
- "longitude": 121.5,
- "latitude": 31.2,
- "address": "123 Main St",
- "city": "Shanghai",
- "province": "SH",
- "country": "CN",
- "iccid": "8986012345678901234",
- "imei": "356938035643809",
- "account": "installer01"
- }`)
- out, err := MaskJSONBytes(in)
- if err != nil {
- t.Fatalf("MaskJSONBytes err: %v", err)
- }
- var m map[string]any
- if err := json.Unmarshal(out, &m); err != nil {
- t.Fatalf("output not valid JSON: %v\n%s", err, out)
- }
- // 保留字段
- if m["sn"] != "SN123" {
- t.Errorf("sn should be preserved, got %v", m["sn"])
- }
- if m["iccid"] != "8986012345678901234" {
- t.Errorf("iccid should be preserved, got %v", m["iccid"])
- }
- if m["city"] != "Shanghai" || m["province"] != "SH" || m["country"] != "CN" {
- t.Errorf("admin region should be preserved: city=%v province=%v country=%v", m["city"], m["province"], m["country"])
- }
- // 完全脱敏字段
- full := maskFull()
- for _, k := range []string{"contactNumber", "longitude", "latitude", "address", "imei"} {
- if m[k] != full {
- t.Errorf("%s should be fully masked, got %v", k, m[k])
- }
- }
- // 姓名
- if m["userName"] != "A***" {
- t.Errorf("userName masked wrong: %v", m["userName"])
- }
- // 邮箱
- if m["email"] != "a***@b.com" {
- t.Errorf("email masked wrong: %v", m["email"])
- }
- // 账号
- if m["account"] != "in***01" {
- t.Errorf("account masked wrong: %v", m["account"])
- }
- }
- func TestMaskJSONBytes_Nested(t *testing.T) {
- in := []byte(`{
- "data": [
- {"userName": "Bob", "sn": "S1", "nested": {"phone": "111", "keep": "ok"}},
- {"userName": "Carol", "sn": "S2"}
- ],
- "total": 2
- }`)
- out, err := MaskJSONBytes(in)
- if err != nil {
- t.Fatalf("err: %v", err)
- }
- var m map[string]any
- if err := json.Unmarshal(out, &m); err != nil {
- t.Fatalf("invalid JSON: %v\n%s", err, out)
- }
- arr := m["data"].([]any)
- first := arr[0].(map[string]any)
- if first["userName"] != "B***" {
- t.Errorf("nested list userName wrong: %v", first["userName"])
- }
- if first["sn"] != "S1" {
- t.Errorf("sn should be preserved in list: %v", first["sn"])
- }
- nested := first["nested"].(map[string]any)
- if nested["phone"] != maskFull() {
- t.Errorf("deeply nested phone should be masked: %v", nested["phone"])
- }
- if nested["keep"] != "ok" {
- t.Errorf("non-sensitive nested field should be preserved: %v", nested["keep"])
- }
- if m["total"].(float64) != 2 {
- t.Errorf("total wrong: %v", m["total"])
- }
- }
- func TestMaskJSONBytes_SensitiveSubtreeRedacted(t *testing.T) {
- // 敏感 key 对应对象/数组时,整个子树被脱敏
- in := []byte(`{"address": {"street": "secret", "no": 42}, "list_phone": ["a","b"]}`)
- // 注意 list_phone 不在规则表,仅测 address 子树
- out, err := MaskJSONBytes(in)
- if err != nil {
- t.Fatalf("err: %v", err)
- }
- var m map[string]any
- if err := json.Unmarshal(out, &m); err != nil {
- t.Fatalf("invalid JSON: %v\n%s", err, out)
- }
- addr := m["address"].(map[string]any)
- if addr["street"] != maskFull() {
- t.Errorf("address.street should be masked: %v", addr["street"])
- }
- // number 子字段被替换为掩码字符串
- if addr["no"] != maskFull() {
- t.Errorf("address.no should be masked: %v", addr["no"])
- }
- }
- func TestMaskJSONBytes_InvalidJSONFailsClosed(t *testing.T) {
- _, err := MaskJSONBytes([]byte(`{not valid json`))
- if err == nil {
- t.Fatal("expected error for invalid JSON (fail-closed)")
- }
- }
- func TestMaskJSONBytes_NullPreserved(t *testing.T) {
- in := []byte(`{"userName": null, "phone": null}`)
- out, err := MaskJSONBytes(in)
- if err != nil {
- t.Fatalf("err: %v", err)
- }
- if !strings.Contains(string(out), `"userName":null`) {
- t.Errorf("null should be preserved: %s", out)
- }
- }
- // ---------- 优化 A:强类型脱敏测试 ----------
- func TestSanitizeSiteInfoDTO_NoUpstreamPollution(t *testing.T) {
- name := "Alice"
- phone := "13812345678"
- addr := "123 Main St"
- iccid := "8986012345678901234"
- orig := &dto.SiteInfoDTO{
- Sn: "SN1",
- UserName: &name,
- ContactNumber: &phone,
- Address: &addr,
- Longitude: "121.5",
- Latitude: "31.2",
- City: "Shanghai",
- ICCID: iccid,
- IMEI: "356938035643809",
- }
- sanitized, ok := sanitizeStrong(orig)
- if !ok {
- t.Fatal("expected strong-type hit for *dto.SiteInfoDTO")
- }
- s := sanitized.(*dto.SiteInfoDTO)
- // 脱敏结果正确
- if *s.UserName != "A***" {
- t.Errorf("userName: %v", *s.UserName)
- }
- if *s.ContactNumber != maskFull() {
- t.Errorf("contactNumber: %v", *s.ContactNumber)
- }
- if *s.Address != maskFull() {
- t.Errorf("address: %v", *s.Address)
- }
- if s.Longitude != maskFull() || s.Latitude != maskFull() {
- t.Errorf("geo not masked: %v %v", s.Longitude, s.Latitude)
- }
- if s.IMEI != maskFull() {
- t.Errorf("imei: %v", s.IMEI)
- }
- // 保留字段
- if s.ICCID != iccid {
- t.Errorf("iccid should be preserved: %v", s.ICCID)
- }
- if s.City != "Shanghai" {
- t.Errorf("city should be preserved: %v", s.City)
- }
- // 上游未被污染
- if *orig.UserName != "Alice" || *orig.ContactNumber != "13812345678" || *orig.Address != "123 Main St" {
- t.Errorf("upstream polluted: name=%v phone=%v addr=%v", *orig.UserName, *orig.ContactNumber, *orig.Address)
- }
- if orig.Longitude != "121.5" || orig.IMEI != "356938035643809" {
- t.Errorf("upstream scalar polluted: lng=%v imei=%v", orig.Longitude, orig.IMEI)
- }
- }
- func TestSanitizeResPageSiteInfo(t *testing.T) {
- name := "Bob"
- orig := &dto.ResPage[dto.SiteInfoDTO]{
- Data: []dto.SiteInfoDTO{{Sn: "S1", UserName: &name, IMEI: "111"}},
- Total: 1,
- }
- sanitized, ok := sanitizeStrong(orig)
- if !ok {
- t.Fatal("expected hit")
- }
- s := sanitized.(*dto.ResPage[dto.SiteInfoDTO])
- if *s.Data[0].UserName != "B***" {
- t.Errorf("page item userName: %v", *s.Data[0].UserName)
- }
- // 上游 slice 未被污染
- if *orig.Data[0].UserName != "Bob" {
- t.Errorf("upstream page polluted: %v", *orig.Data[0].UserName)
- }
- }
- func TestSanitizeDomainSimCardInfo(t *testing.T) {
- orig := &domain.SimCardInfo{GICCID: "GIC19", IMEI: "imei1", IMSI: "imsi1", MSISDN: "msisdn1"}
- sanitized, ok := sanitizeStrong(orig)
- if !ok {
- t.Fatal("expected hit")
- }
- s := sanitized.(*domain.SimCardInfo)
- if s.IMEI != maskFull() || s.IMSI != maskFull() || s.MSISDN != maskFull() {
- t.Errorf("sim identifiers not masked: %+v", s)
- }
- if s.GICCID != "GIC19" {
- t.Errorf("giccid should be preserved: %v", s.GICCID)
- }
- // 上游未污染
- if orig.IMEI != "imei1" {
- t.Errorf("upstream sim polluted: %v", orig.IMEI)
- }
- }
- // ---------- A/B 一致性测试 ----------
- func TestAB_Consistency_SiteInfo(t *testing.T) {
- name := "Alice"
- phone := "13812345678"
- addr := "123 Main St"
- site := &dto.SiteInfoDTO{
- Sn: "SN1",
- UserName: &name,
- ContactNumber: &phone,
- Address: &addr,
- Longitude: "121.5",
- City: "Shanghai",
- ICCID: "8986012345678901234",
- IMEI: "356938035643809",
- }
- // A 路径
- sanitized, _ := sanitizeStrong(site)
- aBytes, err := json.Marshal(sanitized)
- if err != nil {
- t.Fatal(err)
- }
- // B 路径:直接对原始结构体序列化后走流式脱敏
- rawBytes, _ := json.Marshal(site)
- bBytes, err := MaskJSONBytes(rawBytes)
- if err != nil {
- t.Fatalf("B err: %v", err)
- }
- // 归一化比较(字段顺序可能不同,比较解析后的 map)
- var ma, mb map[string]any
- if err := json.Unmarshal(aBytes, &ma); err != nil {
- t.Fatal(err)
- }
- if err := json.Unmarshal(bBytes, &mb); err != nil {
- t.Fatal(err)
- }
- for _, k := range []string{"userName", "contactNumber", "address", "longitude", "imei", "iccId", "city", "sn"} {
- if ma[k] != mb[k] {
- t.Errorf("A/B mismatch on %q: A=%v B=%v", k, ma[k], mb[k])
- }
- }
- }
|