cf04db794b
POST /api/collect/batch:text/plain JSON 解析(ParseError 400 防 500)、 域名白名单/站点停用/未知站点统一 403 not_allowed 防枚举、 Anon 限流(collect scope,get_rate 读实时 api_settings 修 import 固化)、 occurred_at ISO 归一(naive 补 UTC)、XFF 末段取 IP + ::ffff: 归一、 成功/拒绝分支均回显 ACAO 供 SDK 读响应。 Co-Authored-By: Claude Code <noreply@anthropic.com>
64 lines
2.8 KiB
Python
64 lines
2.8 KiB
Python
import json
|
|
import pytest
|
|
from django.core.cache import cache
|
|
from django.utils import timezone
|
|
from tracker.models import Site
|
|
|
|
NOW = timezone.now()
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_cache():
|
|
cache.clear() # locmem 限流历史跨测试残留,会让 429 用例假红
|
|
|
|
def _post(client, body, origin="https://www.a.com", extra=None):
|
|
return client.post("/api/collect/batch", data=json.dumps(body),
|
|
content_type="text/plain", HTTP_ORIGIN=origin, **(extra or {}))
|
|
|
|
def _body(n=1, sid="s1", vid="v1"):
|
|
return {"site_id": "site_x", "visitor_id": vid, "session_id": sid,
|
|
"context": {"referrer_domain": "", "path": "/", "device_type": "PC"},
|
|
"events": [{"event_id": f"e{i}", "event_name": "page_view",
|
|
"occurred_at": NOW.isoformat(), "path": "/", "title": "t",
|
|
"properties": {}} for i in range(n)]}
|
|
|
|
def test_collect_ok_creates_events(db, client):
|
|
Site.objects.create(name="s", domains=["www.a.com"], site_id="site_x")
|
|
r = _post(client, _body(2))
|
|
assert r.status_code == 200 and r.json() == {"code": 200, "data": {"accepted": 2, "duplicated": 0}, "message": "ok"}
|
|
assert r["Access-Control-Allow-Origin"] == "https://www.a.com"
|
|
|
|
def test_origin_not_whitelisted_rejected(db, client):
|
|
Site.objects.create(name="s", domains=["www.a.com"], site_id="site_x")
|
|
r = _post(client, _body(1), origin="https://evil.com")
|
|
assert r.status_code == 403 and r.json()["code"] == 403
|
|
assert r.json()["message"] == "not_allowed" # 与未知站点同 message,防枚举
|
|
|
|
def test_inactive_site_rejected(db, client):
|
|
Site.objects.create(name="s", domains=["www.a.com"], site_id="site_x", is_active=False)
|
|
assert _post(client, _body(1)).status_code == 403
|
|
|
|
def test_unknown_site_403_not_404(db, client): # 不泄露站点是否存在
|
|
r = _post(client, _body(1))
|
|
assert r.status_code == 403
|
|
assert r.json()["message"] == "not_allowed" # message 与上例一致
|
|
|
|
def test_replay_duplicated(db, client):
|
|
Site.objects.create(name="s", domains=["www.a.com"], site_id="site_x")
|
|
_post(client, _body(1))
|
|
r = _post(client, _body(1))
|
|
assert r.json()["data"]["duplicated"] == 1
|
|
|
|
def test_invalid_json_400_not_500(db, client):
|
|
Site.objects.create(name="s", domains=["www.a.com"], site_id="site_x")
|
|
r = client.post("/api/collect/batch", data="{bad json",
|
|
content_type="text/plain", HTTP_ORIGIN="https://www.a.com")
|
|
assert r.status_code == 400
|
|
|
|
def test_throttle_429(db, client, settings):
|
|
settings.REST_FRAMEWORK = {**settings.REST_FRAMEWORK,
|
|
"DEFAULT_THROTTLE_RATES": {"collect": "3/min"}}
|
|
Site.objects.create(name="s", domains=["www.a.com"], site_id="site_x")
|
|
for _ in range(3):
|
|
assert _post(client, _body(1)).status_code == 200
|
|
assert _post(client, _body(1)).status_code == 429
|