诚实性修复: API限流Retry-After(wait=60s)+断言 / purge定时接线(04:00 cron+actor+测试) / Channel表单校验(ref+rules+validate) / README PG数字513

This commit is contained in:
seagull
2026-09-12 02:20:50 +08:00
parent b3f3095d53
commit 67540e8d30
40 changed files with 1805 additions and 31 deletions
+91
View File
@@ -0,0 +1,91 @@
"""P0-4 · 重复编码 400 回归(28 模型一处修复)。
根因:DRF 的 `get_unique_together_validators()` 要求 unique_together 字段
全部出现在 `Meta.fields` 中;本项目 `tenant` 由 `BaseTenantViewSet.acreate()`
注入、不在 fields 里 → 校验器被静默丢弃 → 唯一冲突直达 DB → 500。
修复落点:`apps/core/viewset.py` 的 `acreate()` —— 租户感知的唯一性预检
(友好 400)+ `IntegrityError` 并发兜底(转 400)。
注意:会触发 DB 异常的回归测试一律用
`@pytest.mark.django_db(transaction=True)`,否则测试自带的外层 atomic
被污染,后续 ORM 全变 `TransactionManagementError`(测试假象,非生产问题)。
"""
import pytest
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
pytestmark = pytest.mark.django_db(transaction=True)
def _jwt_client(user, tenant_code):
c = APIClient()
c.credentials(
HTTP_AUTHORIZATION=f"Bearer {RefreshToken.for_user(user).access_token}",
HTTP_X_TENANT_ID=tenant_code,
)
return c
# (url, payload) —— 覆盖主数据 8 类高频手输编码
DUPLICATE_CASES = [
("/api/v1/catalog/categories/", {"code": "DUP01", "name": "分类A"}),
("/api/v1/catalog/brands/", {"code": "DUP01", "name": "品牌A"}),
("/api/v1/catalog/units/", {"code": "DUP01", "name": "单位A"}),
("/api/v1/catalog/products/", {"code": "DUP01", "name": "商品A"}),
("/api/v1/partner/price-levels/", {"code": "DUP01", "name": "等级A"}),
("/api/v1/partner/customers/", {"code": "DUP01", "name": "客户A"}),
("/api/v1/partner/suppliers/", {"code": "DUP01", "name": "供应商A"}),
("/api/v1/inventory/warehouses/", {"code": "DUP01", "name": "仓库A"}),
]
@pytest.mark.parametrize("url,payload", DUPLICATE_CASES)
def test_duplicate_code_is_400_not_500(db, tenant, user, url, payload):
"""重复编码 → 400 + 字段级错误,不含 exc_type/堆栈。"""
c = _jwt_client(user, tenant.code)
r1 = c.post(url, {**payload, "name": payload["name"] + "一"}, format="json")
assert r1.status_code == 201, (url, r1.content)
r2 = c.post(url, {**payload, "name": payload["name"] + "二"}, format="json")
assert r2.status_code == 400, (url, r2.content)
body = r2.json()
assert "exc_type" not in body
assert "traceback" not in str(body).lower()
def test_duplicate_code_is_tenant_scoped(db, tenant, other_tenant, user):
"""跨租户同编码允许:唯一性是租户内的,不是全局的。"""
from apps.core.models import TenantMembership
TenantMembership.objects.get_or_create(
user=user, tenant=other_tenant,
defaults={"role": "member", "is_active": True},
)
c1 = _jwt_client(user, tenant.code)
c2 = _jwt_client(user, other_tenant.code)
r1 = c1.post(
"/api/v1/catalog/products/",
{"code": "SHARED01", "name": "甲租户商品"}, format="json",
)
assert r1.status_code == 201, r1.content
r2 = c2.post(
"/api/v1/catalog/products/",
{"code": "SHARED01", "name": "乙租户商品"}, format="json",
)
assert r2.status_code == 201, r2.content
def test_unknown_error_has_no_exc_type(db, tenant, user):
"""兜底 500 不泄露 exc_type/原始消息(P0-4 附带收尾,已在 exceptions 落地)。"""
from rest_framework.test import APIRequestFactory
from apps.core.exceptions import api_exception_handler
factory = APIRequestFactory()
req = factory.get("/api/v1/catalog/products/")
resp = api_exception_handler(RuntimeError("boom-secret"), {"request": req, "view": None})
assert resp.status_code == 500
assert resp.data["code"] == "server_error"
assert "exc_type" not in resp.data
assert "boom-secret" not in str(resp.data)