"""商品中心集成测试: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"