批次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>
This commit is contained in:
+32
-2
@@ -20,9 +20,10 @@ from inventory.api import inventory_router
|
||||
from shared.models.base import Base
|
||||
from shared.models.identity import User
|
||||
from inventory.models import Customer, Warehouse, Supplier, Product, ProductMaterial, Inventory, MaterialSupplier, SalesOrder, SalesOrderItem
|
||||
from moldinsight.models import STPFile, ProcessingTask
|
||||
import moldinsight.models # noqa: F401 # 全量注册:create_all 需含 moldinsight 表(stp_files 等)
|
||||
from shared.database.database import get_db_session
|
||||
from shared.services.auth_service import get_current_active_user
|
||||
from shared.services.auth_service import get_current_active_user, get_current_admin_user
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -133,7 +134,30 @@ async def seeded_db(async_engine):
|
||||
lead_time=7,
|
||||
)
|
||||
|
||||
session.add_all([user, customer, supplier, warehouse, material, finished, finished_no_bom, bom, inv, ms])
|
||||
stp_file = STPFile(
|
||||
id=1,
|
||||
original_filename="demo-mold.stp",
|
||||
object_key="uploads/demo.stp",
|
||||
storage_bucket="test-bucket",
|
||||
file_size=123,
|
||||
file_hash="hash-demo-1",
|
||||
mime_type="application/step",
|
||||
user_id=user.id,
|
||||
volume=1000.0,
|
||||
surface_area=200.0,
|
||||
product_weight=50.0,
|
||||
)
|
||||
task = ProcessingTask(
|
||||
id=1,
|
||||
task_id="task-demo-1",
|
||||
status="completed",
|
||||
task_type="stp_parsing",
|
||||
progress=100,
|
||||
current_step="done",
|
||||
stp_file_id=stp_file.id,
|
||||
)
|
||||
|
||||
session.add_all([user, customer, supplier, warehouse, material, finished, finished_no_bom, bom, inv, ms, stp_file, task])
|
||||
await session.commit()
|
||||
|
||||
async with session_factory() as session:
|
||||
@@ -156,8 +180,14 @@ async def client(async_engine, seeded_db):
|
||||
result = await session.execute(select(User).where(User.username == "tester"))
|
||||
return result.scalar_one()
|
||||
|
||||
async def override_get_current_admin_user():
|
||||
async with session_factory() as session:
|
||||
result = await session.execute(select(User).where(User.username == "tester"))
|
||||
return result.scalar_one()
|
||||
|
||||
test_app.dependency_overrides[get_db_session] = override_get_db_session
|
||||
test_app.dependency_overrides[get_current_active_user] = override_get_current_active_user
|
||||
test_app.dependency_overrides[get_current_admin_user] = override_get_current_admin_user
|
||||
|
||||
transport = ASGITransport(app=test_app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_returns_seeded_summary(client):
|
||||
resp = await client.get("/api/dashboard")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["material_count"] == 1
|
||||
assert body["finished_product_count"] == 2
|
||||
assert body["supplier_count"] == 1
|
||||
assert body["customer_count"] == 1
|
||||
assert body["warehouse_count"] == 1
|
||||
assert body["total_stock"] == 1000
|
||||
assert float(body["total_value"]) == 10000.0
|
||||
assert body["pending_purchase"] == 0
|
||||
assert body["pending_sales"] == 0
|
||||
assert body["low_stock_products"] == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_low_stock_products_are_listed(client):
|
||||
inventory = await client.get("/api/inventory")
|
||||
assert inventory.status_code == 200
|
||||
inv_id = next(row["id"] for row in inventory.json()["items"] if row["product_sku"] == "MAT-001")
|
||||
|
||||
updated = await client.put(
|
||||
f"/api/inventory/{inv_id}",
|
||||
json={"quantity": 0, "locked_quantity": 0},
|
||||
)
|
||||
assert updated.status_code == 200
|
||||
|
||||
resp = await client.get("/api/dashboard")
|
||||
assert resp.status_code == 200
|
||||
low_stock = resp.json()["low_stock_products"]
|
||||
assert len(low_stock) == 1
|
||||
assert low_stock[0]["sku"] == "MAT-001"
|
||||
assert low_stock[0]["quantity"] == 0
|
||||
assert low_stock[0]["min_stock"] == 0
|
||||
@@ -0,0 +1,180 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_customer_list_search_filters_active_rows(client):
|
||||
create_resp = await client.post(
|
||||
"/api/customers",
|
||||
json={"name": "目标客户Alpha", "contact_person": "张三"},
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
customer_id = create_resp.json()["id"]
|
||||
|
||||
delete_resp = await client.delete(f"/api/customers/{customer_id}")
|
||||
assert delete_resp.status_code == 200
|
||||
|
||||
kept_resp = await client.post(
|
||||
"/api/customers",
|
||||
json={"name": "目标客户Beta", "contact_person": "李四"},
|
||||
)
|
||||
assert kept_resp.status_code == 201
|
||||
|
||||
resp = await client.get("/api/customers", params={"search": "目标客户"})
|
||||
assert resp.status_code == 200
|
||||
rows = resp.json()
|
||||
names = [row["name"] for row in rows]
|
||||
assert "目标客户Beta" in names
|
||||
assert "目标客户Alpha" not in names
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_customer_create_auto_generates_code(client):
|
||||
resp = await client.post(
|
||||
"/api/customers",
|
||||
json={"name": "自动编码客户", "phone": "123456"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["code"].startswith("C")
|
||||
assert body["name"] == "自动编码客户"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_customer_update_returns_latest_fields(client):
|
||||
create_resp = await client.post(
|
||||
"/api/customers",
|
||||
json={"name": "旧客户名", "email": "old@example.com"},
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
customer_id = create_resp.json()["id"]
|
||||
|
||||
update_resp = await client.put(
|
||||
f"/api/customers/{customer_id}",
|
||||
json={"code": "C-CUSTOM", "name": "新客户名", "email": "new@example.com"},
|
||||
)
|
||||
assert update_resp.status_code == 200
|
||||
body = update_resp.json()
|
||||
assert body["code"] == "C-CUSTOM"
|
||||
assert body["name"] == "新客户名"
|
||||
assert body["email"] == "new@example.com"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_customer_delete_soft_deletes_row(client):
|
||||
create_resp = await client.post(
|
||||
"/api/customers",
|
||||
json={"name": "待删除客户"},
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
customer_id = create_resp.json()["id"]
|
||||
|
||||
delete_resp = await client.delete(f"/api/customers/{customer_id}")
|
||||
assert delete_resp.status_code == 200
|
||||
assert delete_resp.json()["message"] == "客户已删除"
|
||||
|
||||
list_resp = await client.get("/api/customers", params={"search": "待删除客户"})
|
||||
assert list_resp.status_code == 200
|
||||
assert all(row["id"] != customer_id for row in list_resp.json())
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_supplier_list_search_filters_active_rows(client):
|
||||
create_resp = await client.post(
|
||||
"/api/suppliers",
|
||||
json={"name": "目标供应商Alpha", "contact_person": "王五"},
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
supplier_id = create_resp.json()["id"]
|
||||
|
||||
delete_resp = await client.delete(f"/api/suppliers/{supplier_id}")
|
||||
assert delete_resp.status_code == 200
|
||||
|
||||
kept_resp = await client.post(
|
||||
"/api/suppliers",
|
||||
json={"name": "目标供应商Beta", "contact_person": "赵六"},
|
||||
)
|
||||
assert kept_resp.status_code == 201
|
||||
|
||||
resp = await client.get("/api/suppliers", params={"search": "目标供应商"})
|
||||
assert resp.status_code == 200
|
||||
rows = resp.json()
|
||||
names = [row["name"] for row in rows]
|
||||
assert "目标供应商Beta" in names
|
||||
assert "目标供应商Alpha" not in names
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_supplier_create_auto_generates_code(client):
|
||||
resp = await client.post(
|
||||
"/api/suppliers",
|
||||
json={"name": "自动编码供应商", "phone": "123456"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["code"].startswith("S")
|
||||
assert body["name"] == "自动编码供应商"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_supplier_update_returns_latest_fields(client):
|
||||
create_resp = await client.post(
|
||||
"/api/suppliers",
|
||||
json={"name": "旧供应商名", "email": "old-s@example.com"},
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
supplier_id = create_resp.json()["id"]
|
||||
|
||||
update_resp = await client.put(
|
||||
f"/api/suppliers/{supplier_id}",
|
||||
json={"code": "S-CUSTOM", "name": "新供应商名", "email": "new-s@example.com"},
|
||||
)
|
||||
assert update_resp.status_code == 200
|
||||
body = update_resp.json()
|
||||
assert body["code"] == "S-CUSTOM"
|
||||
assert body["name"] == "新供应商名"
|
||||
assert body["email"] == "new-s@example.com"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_supplier_delete_soft_deletes_row(client):
|
||||
create_resp = await client.post(
|
||||
"/api/suppliers",
|
||||
json={"name": "待删除供应商"},
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
supplier_id = create_resp.json()["id"]
|
||||
|
||||
delete_resp = await client.delete(f"/api/suppliers/{supplier_id}")
|
||||
assert delete_resp.status_code == 200
|
||||
assert delete_resp.json()["message"] == "供应商已删除"
|
||||
|
||||
list_resp = await client.get("/api/suppliers", params={"search": "待删除供应商"})
|
||||
assert list_resp.status_code == 200
|
||||
assert all(row["id"] != supplier_id for row in list_resp.json())
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_warehouse_list_orders_default_first(client):
|
||||
create_resp = await client.post(
|
||||
"/api/warehouses",
|
||||
json={"code": "W002", "name": "普通仓库"},
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
|
||||
resp = await client.get("/api/warehouses")
|
||||
assert resp.status_code == 200
|
||||
rows = resp.json()
|
||||
assert rows[0]["is_default"] is True
|
||||
assert rows[0]["name"] == "默认仓库"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_warehouse_create_auto_generates_code(client):
|
||||
resp = await client.post(
|
||||
"/api/warehouses",
|
||||
json={"name": "自动编码仓库", "manager": "管理员A"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["code"].startswith("W")
|
||||
assert body["name"] == "自动编码仓库"
|
||||
@@ -0,0 +1,100 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_material_price_history_create_updates_latest_cost(client):
|
||||
resp = await client.post(
|
||||
"/api/materials/1/price-history",
|
||||
json={"price": 12.5, "supplier_id": 1, "remark": "latest"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["product_id"] == 1
|
||||
assert body["price"] == 12.5
|
||||
assert body["supplier_name"] == "供应商A"
|
||||
|
||||
history = await client.get("/api/materials/1/price-history")
|
||||
assert history.status_code == 200
|
||||
assert history.json()[0]["price"] == 12.5
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_material_price_trend_returns_change_summary(client):
|
||||
resp1 = await client.post(
|
||||
"/api/materials/1/price-history",
|
||||
json={"price": 10.0, "supplier_id": 1},
|
||||
)
|
||||
assert resp1.status_code == 201
|
||||
resp2 = await client.post(
|
||||
"/api/materials/1/price-history",
|
||||
json={"price": 15.0, "supplier_id": 1},
|
||||
)
|
||||
assert resp2.status_code == 201
|
||||
|
||||
trend = await client.get("/api/materials/1/price-trend", params={"months": 6})
|
||||
assert trend.status_code == 200
|
||||
body = trend.json()
|
||||
prices = [item["price"] for item in body["price_history"]]
|
||||
assert sorted(prices) == [10.0, 15.0]
|
||||
assert body["current_price"] in {10.0, 15.0}
|
||||
assert body["price_change"] in {5.0, -5.0}
|
||||
assert body["price_change_percent"] in {50.0, -33.33}
|
||||
assert len(body["price_history"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_material_price_trend_requires_history(client):
|
||||
resp = await client.get("/api/materials/1/price-trend")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add_material_supplier_and_list_by_material(client):
|
||||
resp = await client.post(
|
||||
"/api/materials/1/suppliers",
|
||||
json={"supplier_id": 1, "is_primary": True, "lead_time": 7, "min_order_quantity": 10},
|
||||
)
|
||||
assert resp.status_code == 400 # seeded_db 已存在主关联
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_material_suppliers_and_supplier_materials(client):
|
||||
by_material = await client.get("/api/materials/1/suppliers")
|
||||
assert by_material.status_code == 200
|
||||
material_rows = by_material.json()
|
||||
assert len(material_rows) == 1
|
||||
assert material_rows[0]["supplier_id"] == 1
|
||||
assert material_rows[0]["supplier_name"] == "供应商A"
|
||||
|
||||
by_supplier = await client.get("/api/materials/suppliers/1/materials")
|
||||
assert by_supplier.status_code == 200
|
||||
supplier_rows = by_supplier.json()
|
||||
assert len(supplier_rows) == 1
|
||||
assert supplier_rows[0]["product_id"] == 1
|
||||
assert supplier_rows[0]["product_sku"] == "MAT-001"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_remove_material_supplier_deletes_association(client):
|
||||
resp = await client.delete("/api/materials/suppliers/1")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "物料供应商关联已删除"
|
||||
|
||||
by_material = await client.get("/api/materials/1/suppliers")
|
||||
assert by_material.status_code == 200
|
||||
assert by_material.json() == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"url,payload,expected",
|
||||
[
|
||||
("/api/materials/2/price-history", {"price": 10.0}, 400),
|
||||
("/api/materials/99999/price-history", {"price": 10.0}, 404),
|
||||
("/api/materials/1/price-history", {"price": 10.0, "supplier_id": 99999}, 400),
|
||||
("/api/materials/1/suppliers", {"supplier_id": 99999}, 400),
|
||||
],
|
||||
)
|
||||
async def test_material_endpoints_validation(url, payload, expected, client):
|
||||
resp = await client.post(url, json=payload)
|
||||
assert resp.status_code == expected
|
||||
@@ -0,0 +1,176 @@
|
||||
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
|
||||
Reference in New Issue
Block a user