Files

133 lines
6.8 KiB
Python
Raw Permalink Normal View History

import json
import pathlib
import pytest
from datetime import datetime, timedelta
from django.utils import timezone
from tracker import constants as C
from tracker.services.validation import validate_event, sanitize_url
VECTORS = json.loads((pathlib.Path(__file__).parent / "fixtures" / "sanitize_vectors.json").read_text(encoding="utf-8"))
NOW = timezone.now()
def _ev(**kw):
base = dict(event_id="e1", event_name="page_view",
occurred_at=NOW, path="/p", title="t", properties={})
base.update(kw)
return base
def test_ok_event():
assert validate_event(_ev()) is None # None = 通过
def test_unknown_event_name():
assert validate_event(_ev(event_name="xxx")) == "event_name_not_allowed"
def test_sensitive_property_rejected():
assert validate_event(_ev(properties={"user_password": "1"})) == "sensitive_field"
def test_shape_type_and_whitelist():
assert validate_event(["x"]) == "event_invalid_shape"
assert validate_event(_ev(event_id=123)) == "invalid_event_id"
assert validate_event(_ev(properties={"foo": "bar"})) == "property_not_allowed"
def test_too_many_props_and_long_string():
assert validate_event(_ev(properties={f"k{i}": "v" for i in range(40)})) == "properties_invalid"
assert validate_event(_ev(properties={"referrer": "x" * 600})) == "properties_too_long"
def test_future_and_stale_time():
assert validate_event(_ev(occurred_at=NOW + timedelta(days=2))) == "occurred_at_out_of_window" # 超窗拒绝
assert validate_event(_ev(occurred_at=NOW - timedelta(hours=2))) is None # 窗内迟到可收
def test_sanitize_url_drops_sensitive_params():
u = sanitize_url("/p?email=a@b.com&token=x&id=3&utm_source=google&monkey=1")
assert "email=" not in u and "token=" not in u and "utm_source=google" in u
assert "monkey=1" in u # fullmatch:含 key 子串的正常参数不误伤
def test_pageview_referrer_url_sanitized():
"""page_view 的 properties.referrer 是完整 URL:敏感参数整键剔除后写回。"""
ev = _ev(properties={"referrer": "https://www.bing.com/search?q=x&email=a@b.com&token=t"})
assert validate_event(ev) is None
assert ev["properties"]["referrer"] == "https://www.bing.com/search?q=x"
def test_pageview_referrer_truncated_after_reencode():
"""清洗重编码(中文 → %XX 九倍膨胀)可能超出原长:出门统一截 MAX_STR_LEN=500。
用 utm 值驱动:utm 参数三轨制下无条件保留(不走值判别),膨胀路径仍可达。"""
ev = _ev(properties={"referrer": "https://www.bing.com/search?utm_term=" + "词" * 200})
assert validate_event(ev) is None # 原文 237 字符过长度门
ref = ev["properties"]["referrer"]
assert len(ref) == 500 and ref.startswith("https://www.bing.com/search?utm_term=%E8%AF%8D")
def test_nested_sensitive_key_rejected():
assert validate_event(_ev(properties={"utm": {"utm_a": 1, "user_password": "x"}})) == "sensitive_field" # utm 在白名单内,嵌套敏感键仍须命中
def test_naive_datetime_rejected():
assert validate_event(_ev(occurred_at=NOW.replace(tzinfo=None))) == "invalid_occurred_at"
def test_prop_size_gate_counts_bytes():
r = validate_event(_ev(properties={"utm": {"a": "汉" * 1500}}))
assert r == "properties_too_large" # 字符数约 2K 未超,UTF-8 约 4.5KB 超 4096,须按字节算
def test_nested_string_length_capped():
assert validate_event(_ev(properties={"utm": {"a": "x" * 600}})) == "properties_too_long"
def test_falsy_non_dict_properties_rejected():
assert validate_event(_ev(properties=[])) == "properties_invalid"
def test_lone_surrogate_event_id_rejected():
assert validate_event(_ev(event_id="\ud800")) == "invalid_event_id"
def test_lone_surrogate_field_rejected():
assert validate_event(_ev(path="/p\ud800")) == "invalid_field_type"
assert validate_event(_ev(title="t\ud800")) == "invalid_field_type"
def test_lone_surrogate_props_rejected():
assert validate_event(_ev(properties={"utm": {"a": "\ud800"}})) == "properties_invalid"
def test_non_str_path_rejected():
assert validate_event(_ev(path=123)) == "invalid_field_type"
def test_prop_depth_exceeded():
assert validate_event(_ev(properties={"utm": {"a": {"b": 1}}})) == "properties_depth"
def test_sanitize_url_vectors():
for v in VECTORS["vectors"]:
assert sanitize_url(v["in"]) == v["out"], v["in"]
def test_sanitize_rule_version_exported():
assert C.SANITIZE_RULE_VERSION == VECTORS["rule_version"]
# ---------------------------------------------------------------------------
# 业务事件契约(spec §1/§2):+6 事件白名单与属性枚举/格式校验
# ---------------------------------------------------------------------------
SITE_ID = "site_test" # validate_event 不查 DB,本地定义即可
def _biz(name, **props):
return {"event_id": f"biz-{name}", "event_name": name, "occurred_at": timezone.now(),
"path": "/p", "title": "t", "site_id": SITE_ID, "visitor_id": "v1",
"session_id": "s1", "page_id": "p1", "seq": 1, "properties": props}
@pytest.mark.parametrize("ev,expect_reason", [
(_biz("contact_click", channel="whatsapp"), None),
(_biz("contact_click", channel="im"), "property_not_allowed"),
(_biz("form_start", form_id="cf7-156"), None),
(_biz("form_start", form_id="wpcf7-f156-p20"), "property_not_allowed"),
(_biz("form_submit", form_id="cf7-156", submission_id="9c8b7a6d-1111-4222-8333-444455556666"), None),
(_biz("form_submit", form_id="cf7-156", submission_id=""), None),
(_biz("form_submit", form_id="cf7-156", submission_id="not-uuid"), "property_not_allowed"),
(_biz("form_error", form_id="cf7-156", submission_id="", error_code="invalid", field_count=2), None),
(_biz("form_error", form_id="cf7-156", submission_id="", error_code="bogus"), "property_not_allowed"),
(_biz("form_error", form_id="cf7-156", submission_id="", error_code="invalid", field_count=-1), "property_not_allowed"),
(_biz("form_submit_result", form_id="cf7-156", submission_id="", result_code="mail_sent"), None),
(_biz("form_submit_result", form_id="cf7-156", submission_id="", result_code="ok"), "property_not_allowed"),
(_biz("product_view", product_id="byd-equation-leopard-5", inferred=True, rule_version="v1"), None),
(_biz("product_view", product_id="x"*101, inferred=True, rule_version="v1"), "property_not_allowed"),
# 尾换行脏值:$ 锚匹配串尾换行前,\Z 才是真正串尾——collect 端点公开,须拒
(_biz("form_start", form_id="cf7-156\n"), "property_not_allowed"),
(_biz("form_submit", form_id="cf7-156", submission_id="9c8b7a6d-1111-4222-8333-444455556666\n"), "property_not_allowed"),
])
def test_biz_event_validation(ev, expect_reason):
assert validate_event(ev, v2=True, batch_site_id=SITE_ID, batch_visitor_id="v1") == expect_reason