Files
dealerhub/backend/tests/test_duplicate_code.py

92 lines
3.7 KiB
Python
Raw Permalink 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.
"""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)