Files
fangke/server/tracker/tests/test_sites.py
T
浅念 eb27bc830b fix: sites 标量 domains 防御/_to_bool/403 锁定/注释
- _norm_domains 入口类型守卫:domains 传数字曾 500、字符串曾逐字符
  迭代成单字符域名静默落库,统一走 invalid_domains 400
- bool() 强转换 _to_bool:"false" 字符串曾是 True,对 allow_no_origin
  这类放宽开关方向不利,改按字面解析(其余未知值仍 False)
- 补 member DELETE 不存在 pk → 403(先于 404)锁定测试
- SiteDetailView 注释删去 GET 字样(无 detail GET,前端走列表+Dialog)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 14:43:26 +08:00

144 lines
6.1 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.
import pytest
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from tracker.models import Site
pytestmark = pytest.mark.django_db
User = get_user_model()
@pytest.fixture
def client(): # 覆盖 pytest-django 的 Django Client——认证用例需要 credentials()/format="json"
return APIClient()
@pytest.fixture
def admin_user():
return User.objects.create_user(username="admin", password="adminpass123", is_staff=True)
@pytest.fixture
def member_user():
return User.objects.create_user(username="member", password="memberpass123")
def _auth(client, user):
from rest_framework_simplejwt.tokens import RefreshToken
token = RefreshToken.for_user(user)
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token.access_token}")
def test_sites_require_auth(client):
assert client.get("/api/admin/sites").status_code == 401
def test_create_site_normalizes_domains(client, member_user):
_auth(client, member_user)
r = client.post("/api/admin/sites",
{"name": "官网", "domains": ["https://Example.com/", "example.com", " bad.com ",
"http://Sub.Example.com:8443/path?q=1"]}, format="json")
assert r.status_code == 200 and r.json()["code"] == 200
d = r.json()["data"]
assert d["domains"] == ["example.com", "bad.com", "sub.example.com"] # 去协议/端口/路径/去重/strip/小写
assert d["site_id"].startswith("site_") and d["is_active"] is True
assert Site.objects.filter(site_id=d["site_id"]).exists() # 真落库
def test_create_site_validates(client, member_user):
_auth(client, member_user)
assert client.post("/api/admin/sites", {"name": "x", "domains": []},
format="json").status_code == 400
assert client.post("/api/admin/sites", {"domains": ["a.com"]},
format="json").status_code == 400
assert client.post("/api/admin/sites", ["arr"], format="json").status_code == 400 # 畸形 body 不 500
def test_list_and_patch(client, member_user):
_auth(client, member_user)
site = Site.objects.create(name="a", domains=["a.com"])
r = client.get("/api/admin/sites")
assert len(r.json()["data"]) == 1 and r.json()["data"][0]["id"] == site.pk
r = client.patch(f"/api/admin/sites/{site.pk}",
{"domains": ["new.com"], "is_active": False}, format="json")
assert r.status_code == 200
site.refresh_from_db()
assert site.domains == ["new.com"] and site.is_active is False
# name/allow_no_origin 也可更新
r = client.patch(f"/api/admin/sites/{site.pk}",
{"name": " 新名 ", "allow_no_origin": True}, format="json")
site.refresh_from_db()
assert site.name == "新名" and site.allow_no_origin is True
def test_patch_validates_and_404_envelope(client, member_user):
_auth(client, member_user)
site = Site.objects.create(name="a", domains=["a.com"])
assert client.patch(f"/api/admin/sites/{site.pk}",
{"domains": []}, format="json").status_code == 400
assert client.patch(f"/api/admin/sites/{site.pk}",
{"name": " "}, format="json").status_code == 400
r = client.patch("/api/admin/sites/99999", {"name": "x"}, format="json")
assert r.status_code == 404
body = r.json() # 404 同样走信封(Task17 前端单条解析路径)
assert body["code"] == 404 and body["data"] is None and body["message"]
def test_delete_member_403_admin_ok(client, member_user, admin_user):
_auth(client, member_user)
site = Site.objects.create(name="a", domains=["a.com"])
r = client.delete(f"/api/admin/sites/{site.pk}")
assert r.status_code == 403 and r.json()["code"] == 403 # 权限拒绝也带信封
_auth(client, admin_user)
r = client.delete(f"/api/admin/sites/{site.pk}")
assert r.status_code == 200
site.refresh_from_db()
assert site.is_active is False # 软删:行还在
assert Site.objects.filter(pk=site.pk).exists()
def test_delete_soft_keeps_collect_rejects(client, admin_user):
"""软删与 collect 联动:is_active=False 后上报 403 not_allowed(Task7 口径)。"""
import json
_auth(client, admin_user)
site = Site.objects.create(name="a", domains=["a.com"], allow_no_origin=True)
assert client.delete(f"/api/admin/sites/{site.pk}").status_code == 200
r = client.post("/api/collect/batch",
data=json.dumps({"site_id": site.site_id, "visitor_id": "v1",
"session_id": "s1", "events": []}),
content_type="text/plain", HTTP_ORIGIN="https://a.com")
assert r.status_code == 403 and r.json()["message"] == "not_allowed"
# ---- 审查修复锁定测试 ----
def test_scalar_domains_400_not_500(client, member_user):
"""domains 传标量:数字曾直接 500(for d in 123)、字符串曾逐字符迭代成单字符域名静默落库——统一 400。"""
_auth(client, member_user)
assert client.post("/api/admin/sites",
{"name": "x", "domains": "example.com"}, format="json").status_code == 400
assert client.post("/api/admin/sites",
{"name": "x", "domains": 5}, format="json").status_code == 400
def test_bool_string_parsed_by_literal(client, member_user):
"""bool("false") 是 True,对 allow_no_origin 这类放宽开关方向不利——"false" 字符串须按字面落库 False。"""
_auth(client, member_user)
r = client.post("/api/admin/sites",
{"name": "x", "domains": ["a.com"], "allow_no_origin": "false"}, format="json")
assert r.status_code == 200
site = Site.objects.get(pk=r.json()["data"]["id"])
assert site.allow_no_origin is False
client.patch(f"/api/admin/sites/{site.pk}", {"is_active": "false"}, format="json")
site.refresh_from_db()
assert site.is_active is False
client.patch(f"/api/admin/sites/{site.pk}", {"allow_no_origin": "true"}, format="json")
site.refresh_from_db()
assert site.allow_no_origin is True
def test_delete_member_403_before_404(client, member_user):
"""权限先于存在性:member DELETE 不存在的 pk 也是 403——不泄露站点是否存在。"""
_auth(client, member_user)
r = client.delete("/api/admin/sites/99999")
assert r.status_code == 403 and r.json()["code"] == 403