Files
gaokao-volunteer-system/data/orders/tests/test_masking.py

261 lines
8.0 KiB
Python
Raw Normal View History

"""masking 模块单元测试 (T11.2).
覆盖
- mask 函数的正常路径与边界None / 空串 / 非字符串 / 非标准长度
- Order.to_dict mask 模式串联通路
- 默认安全未知 decrypt_sensitive 值回退 mask 而非明文
"""
from __future__ import annotations
import os
os.environ.setdefault("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-unit-tests")
from data.orders.models import Order
from data.orders.masking import (
mask_id_card,
mask_name,
mask_phone,
mask_sensitive_dict,
2026-06-13 14:49:58 +08:00
mask_wechat,
)
# ---------------------- mask_phone ----------------------
def test_mask_phone_standard_11_digits():
assert mask_phone("13800001234") == "138****1234"
def test_mask_phone_strip_prefix_and_separator():
assert mask_phone("+86 138-0000-1234") == "138****1234"
assert mask_phone(" 13800001234 ") == "138****1234"
def test_mask_phone_none_returns_none():
assert mask_phone(None) is None
def test_mask_phone_empty_returns_empty():
assert mask_phone("") == ""
assert mask_phone(" ") == ""
def test_mask_phone_non_string_returns_none():
assert mask_phone(13800001234) is None # type: ignore[arg-type]
assert mask_phone(["13800001234"]) is None # type: ignore[arg-type]
def test_mask_phone_short_falls_back_to_partial_mask():
"""7 位临界值:走遮罩(至少 1 个 *),8-10 位类似。"""
out7: str = mask_phone("1234567") or "" # type: ignore[assignment]
assert out7.startswith("123")
assert out7.endswith("4567")
assert "*" in out7
# 6 位及以下 — 长度不足,原样返回
assert mask_phone("123456") == "123456"
def test_mask_phone_keeps_non_standard_length_prefix_suffix():
"""长度 >= 7 但不是 11 位(如带分机 12 位)也走遮罩 — 至少遮 1 个 *。"""
# 12 位带分机号 — 至少 1 个 *,前 3 后 4 保留
out: str = mask_phone("138000012345") or "" # type: ignore[assignment]
assert out.startswith("138*")
assert out.endswith("2345")
assert "*" in out
# ---------------------- mask_id_card ----------------------
def test_mask_id_card_18_digits_keeps_district_and_tail():
assert mask_id_card("430102200501011234") == "430102********1234"
def test_mask_id_card_18_with_x_checksum_lowercased_ok():
"""末位 X/x 视为合法的身份证校验位 — 遮罩保留末 4 位(含 X"""
assert mask_id_card("43010220050101123X") == "430102********123X"
def test_mask_id_card_15_legacy_format():
assert mask_id_card("430102050101123") == "430102******123"
def test_mask_id_card_none_returns_none():
assert mask_id_card(None) is None
def test_mask_id_card_empty_returns_empty():
assert mask_id_card("") == ""
assert mask_id_card(" ") == ""
def test_mask_id_card_non_string_returns_none():
assert mask_id_card(123) is None # type: ignore[arg-type]
def test_mask_id_card_short_below_threshold_unchanged():
"""< 13 位视为非身份证 — 原样返回,避免误遮短串。"""
assert mask_id_card("4301021234") == "4301021234"
assert mask_id_card("12345") == "12345"
def test_mask_id_card_non_standard_length_partial_mask():
"""13-14 位非标准长度:前 6 后 4 保留,中段至少 2 个 *。"""
out: str = mask_id_card("4301022005011") or "" # type: ignore[assignment] # 13 位
assert out.startswith("430102")
assert out.endswith("5011") # 末 4 位
assert "*" in out
# ---------------------- mask_name ----------------------
def test_mask_name_one_char_unchanged():
assert mask_name("") == ""
def test_mask_name_two_chars_surname_plus_star():
assert mask_name("张三") == "张*"
def test_mask_name_three_chars_surname_given_name_partial():
"""3 字姓名 → 姓 + 1 个 * + 名末字(如"张三丰""张*丰")。"""
assert mask_name("张三丰") == "张*丰"
def test_mask_name_four_chars_collapsed_to_surname_two_stars():
"""4+ 字姓名 → 姓 + 2 个 *(不暴露名长度信息)。"""
assert mask_name("欧阳明月") == "欧**"
assert mask_name("上官婉儿") == "上**"
def test_mask_name_none_returns_none():
assert mask_name(None) is None
def test_mask_name_empty_returns_empty():
assert mask_name("") == ""
assert mask_name(" ") == ""
def test_mask_name_english_fully_masked():
assert mask_name("Alice") == "*****"
assert mask_name("Bo") == "**"
def test_mask_name_mixed_chinese_and_digits_treats_as_non_cjk():
"""含非中日韩字符 → 全遮,不暴露字符数。"""
assert mask_name("张3") == "**"
assert mask_name("John 张") == "******" # 6 字符全遮
# ---------------------- mask_sensitive_dict ----------------------
2026-06-13 14:49:58 +08:00
def test_mask_wechat_keeps_prefix_suffix():
assert mask_wechat("wx-li") == "wx*li"
assert mask_wechat("wechat_user") == "we*******er"
def test_mask_wechat_none_and_empty_are_safe():
assert mask_wechat(None) is None
assert mask_wechat("") == ""
assert mask_wechat("ab") == "**"
def test_mask_sensitive_dict_handles_all_known_fields():
data = {
"customer_phone": "13800001234",
"candidate_id_card": "430102200501011234",
"customer_name": "张三",
2026-06-13 14:49:58 +08:00
"customer_wechat": "wx-li",
"candidate_name": "李四光",
"customer_phone_hash": "abc",
"amount_cents": 1000,
}
out = mask_sensitive_dict(data)
assert out["customer_phone"] == "138****1234"
assert out["candidate_id_card"] == "430102********1234"
assert out["customer_name"] == "张*"
2026-06-13 14:49:58 +08:00
assert out["customer_wechat"] == "wx*li"
assert out["candidate_name"] == "李*光" # 3 字姓名 → 姓 + * + 名末字
assert out["customer_phone_hash"] == "abc"
assert out["amount_cents"] == 1000
def test_mask_sensitive_dict_keeps_none_fields_none():
data = {"customer_phone": None, "candidate_id_card": None}
out = mask_sensitive_dict(data)
assert out["customer_phone"] is None
assert out["candidate_id_card"] is None
def test_mask_sensitive_dict_does_not_mutate_input():
data = {"customer_phone": "13800001234"}
out = mask_sensitive_dict(data)
assert data["customer_phone"] == "13800001234" # 原字典未变
assert out["customer_phone"] == "138****1234"
def test_mask_sensitive_dict_non_dict_passthrough():
assert mask_sensitive_dict("not a dict") == "not a dict" # type: ignore[arg-type]
assert mask_sensitive_dict(None) is None # type: ignore[arg-type]
# ---------------------- Order.to_dict mask 集成 ----------------------
def _make_order() -> Order:
return Order(
id="GKO-20260612-MASK",
source="web",
service_version="basic",
customer_name="张三",
customer_phone="13800001234",
candidate_name="李四光",
candidate_id_card="430102200501011234",
)
def test_order_to_dict_default_is_mask():
"""不传参 = mask 模式 — 默认安全。"""
order = _make_order()
d = order.to_dict()
assert d["customer_phone"] == "138****1234"
assert d["candidate_id_card"] == "430102********1234"
assert d["customer_name"] == "张*"
assert d["candidate_name"] == "李*光" # 3 字姓名规则
# hash 字段保留
assert "customer_phone_hash" in d
def test_order_to_dict_mask_explicit_string():
order = _make_order()
d = order.to_dict(decrypt_sensitive="mask")
assert d["customer_phone"] == "138****1234"
def test_order_to_dict_unknown_string_falls_back_to_mask():
"""默认安全:未知策略值回退 mask,而非明文。"""
order = _make_order()
d = order.to_dict(decrypt_sensitive="plaintext-oops") # type: ignore[arg-type]
assert d["customer_phone"] == "138****1234"
def test_order_to_dict_true_returns_plaintext():
order = _make_order()
d = order.to_dict(decrypt_sensitive=True)
assert d["customer_phone"] == "13800001234"
assert d["candidate_id_card"] == "430102200501011234"
def test_order_to_dict_false_strips_sensitive():
order = _make_order()
d = order.to_dict(decrypt_sensitive=False)
assert "customer_phone" not in d
assert "candidate_id_card" not in d
assert "customer_phone_hash" in d