Files

119 lines
3.8 KiB
Python

"""商品中心集成测试:CRUD + 多租户隔离。
使用同步 APIClient(走 WSGI handler + adrf view),因为:
- DRF/adrf 的 auth/permissions/pagination 都是同步触发的
- 测试 client 同步请求 adrf 的 async view 时 Django 自动用 async_to_sync 包装
- 比 AsyncClient 稳定,async_to_sync 不会触发 RuntimeError
"""
import pytest
from model_bakery import baker
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from apps.catalog.models import Product, Unit, UnitConversion
@pytest.fixture
def auth_client(db, user, tenant):
client = APIClient()
refresh = RefreshToken.for_user(user)
client.credentials(
HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}",
HTTP_X_TENANT_ID=tenant.code,
)
return client
@pytest.fixture
def anon(db):
return APIClient()
@pytest.mark.django_db
def test_create_product(auth_client, tenant, org):
payload = {
"code": "P001",
"name": "测试商品",
"spec": "500ml",
"barcode": "6901234567890",
"cost_price": "10.0000",
"sale_price": "15.0000",
"status": "active",
}
resp = auth_client.post("/api/v1/catalog/products/", payload, format="json")
assert resp.status_code == 201, resp.content
data = resp.json()
assert data["code"] == "P001"
@pytest.mark.django_db
def test_list_products(auth_client, tenant):
baker.make(Product, tenant=tenant, code="P001", name="商品1")
baker.make(Product, tenant=tenant, code="P002", name="商品2")
resp = auth_client.get("/api/v1/catalog/products/")
assert resp.status_code == 200
assert resp.json()["count"] == 2
@pytest.mark.django_db
def test_tenant_isolation(auth_client, tenant, other_tenant):
baker.make(Product, tenant=other_tenant, code="OTHER_P1")
baker.make(Product, tenant=tenant, code="MY_P1")
resp = auth_client.get("/api/v1/catalog/products/")
codes = {p["code"] for p in resp.json()["results"]}
assert "MY_P1" in codes
assert "OTHER_P1" not in codes
@pytest.mark.django_db
def test_soft_delete_excluded(auth_client, tenant):
baker.make(Product, tenant=tenant, code="DEL_P", is_deleted=True)
resp = auth_client.get("/api/v1/catalog/products/")
codes = {x["code"] for x in resp.json()["results"]}
assert "DEL_P" not in codes
@pytest.mark.django_db
def test_search(auth_client, tenant):
baker.make(Product, tenant=tenant, code="P001", name="苹果")
baker.make(Product, tenant=tenant, code="P002", name="香蕉")
resp = auth_client.get("/api/v1/catalog/products/?search=苹果")
assert resp.json()["count"] == 1
@pytest.mark.django_db
def test_anon_cannot_access(anon, tenant):
anon.credentials(HTTP_X_TENANT_ID=tenant.code)
resp = anon.get("/api/v1/catalog/products/")
assert resp.status_code == 401
@pytest.mark.django_db
def test_product_units_expose_sale_and_cost_price_per_source_unit(
auth_client, tenant, org
):
base = baker.make(Unit, tenant=tenant, code="bottle", name="瓶", is_base=True)
box = baker.make(Unit, tenant=tenant, code="box", name="箱", is_base=False)
product = baker.make(
Product,
tenant=tenant,
code="PRICE-UNIT",
name="单价换算测试",
base_unit=base,
cost_price="3.0000",
sale_price="5.0000",
)
UnitConversion.objects.create(
tenant=tenant, product=product, unit=box, rate="24"
)
response = auth_client.get(f"/api/v1/catalog/products/{product.id}/units/")
assert response.status_code == 200
by_unit = {item["unit_code"]: item for item in response.json()["units"]}
assert by_unit["bottle"]["cost_price"] == "3.0000"
assert by_unit["box"]["cost_price"] == "72.0000"
assert by_unit["box"]["price"] == "120.0000"