Files
geMoldInsight/tests/test_api_product_service.py
T
cjw 64dc85bd14 批次5:inventory服务下沉收口 + Pydantic v2 / datetime弃用清零
- inventory业务层下沉(薄路由+service orchestration模式):
  - customer/supplier/warehouse -> master_data_service
  - material_routes(价格历史/趋势/供应商关联)-> material_service
  - product_routes(CRUD/BOM/from-task跨模块桥接)-> product_service
  - dashboard_routes(首页统计/低库存预警)-> dashboard_service
- inventory侧新增service回归覆盖(dashboard 2 / master_data 10 /
  material 10 / product 14),含跨模块桥接测试种子
- Pydantic v2弃用清零:全仓14处 class Config 全部迁移到
  model_config = ConfigDict(from_attributes=True)(含 shared auth)
- datetime.utcnow() 弃用清零:auth_service 3处统一改 datetime.now(timezone.utc)
- 同步文档:STATUS / ROADMAP / TECH_DEBT(D12清偿)/ AGENTS 代码地图

测试基线:126 passed, 4 skipped(无deprecation warning)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-22 16:13:45 +08:00

177 lines
5.4 KiB
Python

import pytest
@pytest.mark.anyio
async def test_list_products_returns_material_cost_for_finished_goods(client):
resp = await client.get("/api/products")
assert resp.status_code == 200
rows = resp.json()
finished = next(row for row in rows if row["sku"] == "MOLD-STD")
assert float(finished["material_cost"]) == 20.0
@pytest.mark.anyio
async def test_create_product_finished_resets_stock_bounds(client):
resp = await client.post(
"/api/products",
json={
"sku": "FG-NEW",
"name": "新成品",
"item_type": "finished",
"unit": "件",
"min_stock": 10,
"max_stock": 99,
"cost_price": 12,
"sale_price": 20,
},
)
assert resp.status_code == 201
body = resp.json()
assert body["sku"] == "FG-NEW"
assert body["min_stock"] == 0
assert body["max_stock"] == 0
@pytest.mark.anyio
async def test_update_product_rejects_duplicate_sku(client):
created = await client.post(
"/api/products",
json={
"sku": "FG-2",
"name": "成品2",
"item_type": "finished",
"unit": "件",
"cost_price": 0,
"sale_price": 1,
},
)
assert created.status_code == 201
product_id = created.json()["id"]
resp = await client.put(
f"/api/products/{product_id}",
json={
"sku": "MOLD-STD",
"name": "改名",
"item_type": "finished",
"unit": "件",
"cost_price": 0,
"sale_price": 1,
},
)
assert resp.status_code == 400
assert resp.json()["detail"] == "SKU已存在"
@pytest.mark.anyio
async def test_delete_product_soft_deletes_record(client):
created = await client.post(
"/api/products",
json={
"sku": "MAT-DEL",
"name": "待删物料",
"item_type": "material",
"unit": "kg",
"cost_price": 2,
"sale_price": 0,
},
)
assert created.status_code == 201
product_id = created.json()["id"]
deleted = await client.delete(f"/api/products/{product_id}")
assert deleted.status_code == 200
assert deleted.json()["message"] == "产品已删除"
listed = await client.get("/api/products")
assert listed.status_code == 200
assert all(row["id"] != product_id for row in listed.json())
@pytest.mark.anyio
async def test_get_product_bom_returns_line_costs(client):
resp = await client.get("/api/products/2/materials")
assert resp.status_code == 200
body = resp.json()
assert body["product_id"] == 2
assert float(body["total_material_cost"]) == 20.0
assert len(body["items"]) == 1
assert body["items"][0]["material_sku"] == "MAT-001"
@pytest.mark.anyio
async def test_replace_product_bom_rewrites_items(client):
created = await client.post(
"/api/products",
json={
"sku": "MAT-002",
"name": "铝材",
"item_type": "material",
"unit": "kg",
"cost_price": 5,
"sale_price": 0,
},
)
assert created.status_code == 201
material_id = created.json()["id"]
replaced = await client.put(
"/api/products/2/materials",
json={
"items": [
{"material_id": 1, "quantity": 1, "loss_rate": 0},
{"material_id": material_id, "quantity": 2, "loss_rate": 0.1},
]
},
)
assert replaced.status_code == 200
body = replaced.json()
assert len(body["items"]) == 2
assert float(body["total_material_cost"]) == 20.0
@pytest.mark.anyio
@pytest.mark.parametrize(
"url,payload,expected",
[
("/api/products", {"sku": "BAD", "name": "bad", "item_type": "unknown", "unit": "件", "cost_price": 0, "sale_price": 0}, 400),
("/api/products/2/materials", {"items": [{"material_id": 1, "quantity": 1}, {"material_id": 1, "quantity": 2}]}, 400),
("/api/products/2/materials", {"items": [{"material_id": 99999, "quantity": 1}]}, 400),
("/api/products/2/materials", {"items": [{"material_id": 2, "quantity": 1}]}, 400),
("/api/products/2/materials", {"items": [{"material_id": 1, "quantity": 0}]}, 400),
("/api/products/1/materials", {"items": []}, 400),
],
)
async def test_product_service_validation_paths(url, payload, expected, client):
method = client.post if url == "/api/products" else client.put
resp = await method(url, json=payload)
assert resp.status_code == expected
@pytest.mark.anyio
async def test_create_product_from_task_creates_and_binds_product(client):
resp = await client.post("/api/products/from-task/task-demo-1")
assert resp.status_code == 201
body = resp.json()
assert body["sku"] == "MI1"
assert body["name"] == "demo-mold"
assert body["category"] == "模具成品"
assert body["item_type"] == "finished"
assert body["min_stock"] == 0
assert body["max_stock"] == 0
assert "由模具分析创建" in body["description"]
again = await client.post("/api/products/from-task/task-demo-1")
assert again.status_code == 201
assert again.json()["id"] == body["id"]
@pytest.mark.anyio
@pytest.mark.parametrize(
"task_id,expected",
[("missing-task", 404)],
)
async def test_create_product_from_task_validation(task_id, expected, client):
resp = await client.post(f"/api/products/from-task/{task_id}")
assert resp.status_code == expected