Files
gaokao-volunteer-system/data/orders/tests/test_masking.py
Hermes Agent 3f46811e49
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
release: cut v2.1
2026-06-13 14:49:58 +08:00

261 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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,
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 ----------------------
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": "张三",
"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"] == "张*"
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