pii_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package pii
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. )
  7. // ---------- 掩码函数单元测试 ----------
  8. func TestMaskName(t *testing.T) {
  9. cases := map[string]string{
  10. "": "",
  11. "张三": "张***",
  12. "John": "J***",
  13. "李": "李***",
  14. }
  15. for in, want := range cases {
  16. if got := maskName(in); got != want {
  17. t.Errorf("maskName(%q)=%q, want %q", in, got, want)
  18. }
  19. }
  20. }
  21. func TestMaskEmail(t *testing.T) {
  22. if got := maskEmail("john@corp.com"); got != "j***@corp.com" {
  23. t.Errorf("maskEmail got %q", got)
  24. }
  25. // 非法邮箱退化为完全脱敏
  26. if got := maskEmail("notanemail"); got != maskFull() {
  27. t.Errorf("maskEmail(invalid) got %q", got)
  28. }
  29. if got := maskEmail(""); got != "" {
  30. t.Errorf("maskEmail(empty) got %q", got)
  31. }
  32. }
  33. func TestMaskAccount(t *testing.T) {
  34. if got := maskAccount("abcdefgh"); got != "ab***gh" {
  35. t.Errorf("maskAccount got %q", got)
  36. }
  37. // 过短完全脱敏
  38. if got := maskAccount("abc"); got != maskFull() {
  39. t.Errorf("maskAccount(short) got %q", got)
  40. }
  41. }
  42. func TestMaskFullFixedLength(t *testing.T) {
  43. if got := maskFull(); got != strings.Repeat(MaskChar, fullMaskLen) {
  44. t.Errorf("maskFull got %q", got)
  45. }
  46. }
  47. // ---------- 优化 B:流式 token 脱敏测试 ----------
  48. func TestMaskJSONBytes_FlatFields(t *testing.T) {
  49. in := []byte(`{
  50. "sn": "SN123",
  51. "userName": "Alice",
  52. "contactNumber": "13812345678",
  53. "email": "a@b.com",
  54. "longitude": 121.5,
  55. "latitude": 31.2,
  56. "address": "123 Main St",
  57. "city": "Shanghai",
  58. "province": "SH",
  59. "country": "CN",
  60. "iccid": "8986012345678901234",
  61. "imei": "356938035643809",
  62. "account": "installer01"
  63. }`)
  64. out, err := MaskJSONBytes(in)
  65. if err != nil {
  66. t.Fatalf("MaskJSONBytes err: %v", err)
  67. }
  68. var m map[string]any
  69. if err := json.Unmarshal(out, &m); err != nil {
  70. t.Fatalf("output not valid JSON: %v\n%s", err, out)
  71. }
  72. // 保留字段
  73. if m["sn"] != "SN123" {
  74. t.Errorf("sn should be preserved, got %v", m["sn"])
  75. }
  76. if m["iccid"] != "8986012345678901234" {
  77. t.Errorf("iccid should be preserved, got %v", m["iccid"])
  78. }
  79. if m["city"] != "Shanghai" || m["province"] != "SH" || m["country"] != "CN" {
  80. t.Errorf("admin region should be preserved: city=%v province=%v country=%v", m["city"], m["province"], m["country"])
  81. }
  82. // 完全脱敏字段
  83. full := maskFull()
  84. for _, k := range []string{"contactNumber", "longitude", "latitude", "address", "imei"} {
  85. if m[k] != full {
  86. t.Errorf("%s should be fully masked, got %v", k, m[k])
  87. }
  88. }
  89. // 姓名
  90. if m["userName"] != "A***" {
  91. t.Errorf("userName masked wrong: %v", m["userName"])
  92. }
  93. // 邮箱
  94. if m["email"] != "a***@b.com" {
  95. t.Errorf("email masked wrong: %v", m["email"])
  96. }
  97. // 账号
  98. if m["account"] != "in***01" {
  99. t.Errorf("account masked wrong: %v", m["account"])
  100. }
  101. }
  102. func TestMaskJSONBytes_Nested(t *testing.T) {
  103. in := []byte(`{
  104. "data": [
  105. {"userName": "Bob", "sn": "S1", "nested": {"phone": "111", "keep": "ok"}},
  106. {"userName": "Carol", "sn": "S2"}
  107. ],
  108. "total": 2
  109. }`)
  110. out, err := MaskJSONBytes(in)
  111. if err != nil {
  112. t.Fatalf("err: %v", err)
  113. }
  114. var m map[string]any
  115. if err := json.Unmarshal(out, &m); err != nil {
  116. t.Fatalf("invalid JSON: %v\n%s", err, out)
  117. }
  118. arr := m["data"].([]any)
  119. first := arr[0].(map[string]any)
  120. if first["userName"] != "B***" {
  121. t.Errorf("nested list userName wrong: %v", first["userName"])
  122. }
  123. if first["sn"] != "S1" {
  124. t.Errorf("sn should be preserved in list: %v", first["sn"])
  125. }
  126. nested := first["nested"].(map[string]any)
  127. if nested["phone"] != maskFull() {
  128. t.Errorf("deeply nested phone should be masked: %v", nested["phone"])
  129. }
  130. if nested["keep"] != "ok" {
  131. t.Errorf("non-sensitive nested field should be preserved: %v", nested["keep"])
  132. }
  133. if m["total"].(float64) != 2 {
  134. t.Errorf("total wrong: %v", m["total"])
  135. }
  136. }
  137. func TestMaskJSONBytes_SensitiveSubtreeRedacted(t *testing.T) {
  138. // 敏感 key 对应对象/数组时,整个子树被脱敏
  139. in := []byte(`{"address": {"street": "secret", "no": 42}, "list_phone": ["a","b"]}`)
  140. // 注意 list_phone 不在规则表,仅测 address 子树
  141. out, err := MaskJSONBytes(in)
  142. if err != nil {
  143. t.Fatalf("err: %v", err)
  144. }
  145. var m map[string]any
  146. if err := json.Unmarshal(out, &m); err != nil {
  147. t.Fatalf("invalid JSON: %v\n%s", err, out)
  148. }
  149. addr := m["address"].(map[string]any)
  150. if addr["street"] != maskFull() {
  151. t.Errorf("address.street should be masked: %v", addr["street"])
  152. }
  153. // number 子字段被替换为掩码字符串
  154. if addr["no"] != maskFull() {
  155. t.Errorf("address.no should be masked: %v", addr["no"])
  156. }
  157. }
  158. func TestMaskJSONBytes_InvalidJSONFailsClosed(t *testing.T) {
  159. _, err := MaskJSONBytes([]byte(`{not valid json`))
  160. if err == nil {
  161. t.Fatal("expected error for invalid JSON (fail-closed)")
  162. }
  163. }
  164. func TestMaskJSONBytes_NullPreserved(t *testing.T) {
  165. in := []byte(`{"userName": null, "phone": null}`)
  166. out, err := MaskJSONBytes(in)
  167. if err != nil {
  168. t.Fatalf("err: %v", err)
  169. }
  170. if !strings.Contains(string(out), `"userName":null`) {
  171. t.Errorf("null should be preserved: %s", out)
  172. }
  173. }