316 lines
12 KiB
Python
316 lines
12 KiB
Python
"""TASK-001 ~ TASK-014: 司机任务管理与履约测试。
|
||
|
||
覆盖功能点:
|
||
- 管理层创建司机任务
|
||
- 司机任务列表(数据隔离)
|
||
- 任务详情脱敏
|
||
- 司机接单、揽货、送达
|
||
- 状态流转验证(合法/非法)
|
||
- 司机越权操作
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskCreation:
|
||
"""TASK-001: 管理层创建司机任务。"""
|
||
|
||
def test_create_task_success(self, client, manager_headers, make_order):
|
||
"""TASK-001: 管理层创建任务成功。
|
||
|
||
实际接口: POST /api/logistics/tasks
|
||
权限: require_roles("manager", "admin") + require_permissions("logistics:task:create")
|
||
"""
|
||
order = make_order(order_status="approved")
|
||
resp = client.post("/api/logistics/tasks", headers=manager_headers, json={
|
||
"order_id": order.id,
|
||
"driver_id": 5,
|
||
"pickup_address": "泰兴工厂",
|
||
"delivery_address": "上海仓库",
|
||
"pickup_content": "测试货物",
|
||
"quantity": 10,
|
||
"factory_id": order.factory_id,
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["code"] == 0
|
||
|
||
def test_create_task_invalid_order(self, client, manager_headers, make_order):
|
||
"""非 approved 状态订单不能创建任务。"""
|
||
order = make_order(order_status="draft")
|
||
resp = client.post("/api/logistics/tasks", headers=manager_headers, json={
|
||
"order_id": order.id,
|
||
"driver_id": 5,
|
||
"pickup_address": "泰兴工厂",
|
||
"delivery_address": "上海仓库",
|
||
"pickup_content": "测试货物",
|
||
"quantity": 10,
|
||
})
|
||
assert resp.status_code in (400, 409)
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskList:
|
||
"""TASK-002: 司机任务列表测试。"""
|
||
|
||
def test_driver_only_see_own_tasks(self, client, driver_headers, make_task):
|
||
"""TASK-002: 司机只能看到自己的任务。"""
|
||
task = make_task(driver_id=5)
|
||
resp = client.get("/api/driver/tasks", headers=driver_headers)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
# 所有返回的任务都应该是当前司机的
|
||
for t in data["data"]["list"]:
|
||
assert t["driver_id"] == 5
|
||
|
||
def test_driver_cannot_see_other_tasks(self, client, driver_headers, driver2_headers, make_task):
|
||
"""TASK-010: 司机不能看到其他司机的任务。"""
|
||
# 创建分配给 driver02 的任务
|
||
task = make_task(driver_id=6)
|
||
|
||
# driver01 查看任务列表
|
||
resp = client.get("/api/driver/tasks", headers=driver_headers)
|
||
data = resp.json()
|
||
task_ids = [t["task_id"] for t in data["data"]["list"]]
|
||
assert task.id not in task_ids
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskDetail:
|
||
"""TASK-003: 任务详情脱敏测试。"""
|
||
|
||
def test_driver_task_detail_no_financial(self, client, driver_headers, make_task):
|
||
"""TASK-003: 司机任务详情不显示财务字段。"""
|
||
task = make_task(driver_id=5)
|
||
resp = client.get(f"/api/driver/tasks/{task.id}", headers=driver_headers)
|
||
assert resp.status_code == 200
|
||
data = resp.json()["data"]
|
||
# 司机不应看到客户敏感信息和财务字段
|
||
assert "customer_mobile" not in data or data.get("customer_mobile") is None
|
||
assert "profit_total" not in data or data.get("profit_total") is None
|
||
|
||
def test_driver_cannot_view_other_task_detail(self, client, driver_headers, driver2_headers, make_task):
|
||
"""TASK-010: 司机不能查看其他司机的任务详情。"""
|
||
task = make_task(driver_id=6)
|
||
resp = client.get(f"/api/driver/tasks/{task.id}", headers=driver_headers)
|
||
assert resp.status_code in (403, 404)
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskAccept:
|
||
"""TASK-004 ~ TASK-005: 司机接单测试。"""
|
||
|
||
def test_accept_task_success(self, client, driver_headers, make_task):
|
||
"""TASK-004: 司机接单成功。
|
||
|
||
实际接口: POST /api/driver/tasks/{task_id}/accept
|
||
请求体: DriverTaskOperateRequest (photo_files, video_files, remark)
|
||
"""
|
||
task = make_task(status="pending", driver_id=5)
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/accept", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["code"] == 0
|
||
|
||
def test_double_accept(self, client, driver_headers, make_task):
|
||
"""TASK-005: 重复接单应返回错误。"""
|
||
task = make_task(status="accepted", driver_id=5)
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/accept", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
assert resp.status_code in (400, 409)
|
||
|
||
def test_accept_other_driver_task(self, client, driver_headers, driver2_headers, make_task):
|
||
"""TASK-010: 司机不能接其他司机的任务。"""
|
||
task = make_task(status="pending", driver_id=6)
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/accept", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
assert resp.status_code in (403, 404)
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskPickup:
|
||
"""TASK-006 ~ TASK-007: 司机揽货测试。"""
|
||
|
||
def test_pickup_success(self, client, driver_headers, make_task):
|
||
"""TASK-006: 司机揽货成功(含照片)。
|
||
|
||
实际接口: POST /api/driver/tasks/{task_id}/pickup
|
||
请求体: DriverTaskOperateRequest
|
||
|
||
注意: 揽货前需要先提交运单号(waybill)
|
||
"""
|
||
task = make_task(status="accepted", driver_id=5)
|
||
|
||
# 先提交运单号
|
||
client.post(f"/api/driver/tasks/{task.id}/waybills", headers=driver_headers, json={
|
||
"waybills": [{
|
||
"tracking_number": "SF1234567890",
|
||
"express_company": "shunfeng",
|
||
"express_name": "顺丰速运",
|
||
}],
|
||
})
|
||
|
||
# 再揽货
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/pickup", headers=driver_headers, json={
|
||
"photo_files": [{"file_url": "https://oss.example.com/pickup.jpg", "file_name": "pickup.jpg"}],
|
||
"video_files": [],
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["code"] == 0
|
||
|
||
def test_pickup_without_photo(self, client, driver_headers, make_task):
|
||
"""TASK-007: 揽货无照片应返回错误。"""
|
||
task = make_task(status="accepted", driver_id=5)
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/pickup", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
# 无照片揽货可能被拒绝或允许(取决于业务规则)
|
||
assert resp.status_code in (200, 400, 422)
|
||
|
||
def test_pickup_from_wrong_status(self, client, driver_headers, make_task):
|
||
"""TASK-009: pending 状态不能直接揽货。"""
|
||
task = make_task(status="pending", driver_id=5)
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/pickup", headers=driver_headers, json={
|
||
"photo_files": [{"file_url": "https://oss.example.com/pickup.jpg", "file_name": "pickup.jpg"}],
|
||
"video_files": [],
|
||
})
|
||
assert resp.status_code in (400, 409)
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskDeliver:
|
||
"""TASK-008: 司机送达测试。"""
|
||
|
||
def test_deliver_success(self, client, driver_headers, make_task):
|
||
"""TASK-008: 司机确认送达成功。
|
||
|
||
实际接口: POST /api/driver/tasks/{task_id}/deliver
|
||
请求体: DriverTaskOperateRequest
|
||
|
||
注意: 送达前需要先揽货,揽货前需要先提交运单号
|
||
"""
|
||
task = make_task(status="accepted", driver_id=5)
|
||
|
||
# 先提交运单号
|
||
client.post(f"/api/driver/tasks/{task.id}/waybills", headers=driver_headers, json={
|
||
"waybills": [{
|
||
"tracking_number": "SF1234567890",
|
||
"express_company": "shunfeng",
|
||
"express_name": "顺丰速运",
|
||
}],
|
||
})
|
||
|
||
# 揽货
|
||
client.post(f"/api/driver/tasks/{task.id}/pickup", headers=driver_headers, json={
|
||
"photo_files": [{"file_url": "https://oss.example.com/pickup.jpg", "file_name": "pickup.jpg"}],
|
||
"video_files": [],
|
||
})
|
||
|
||
# 送达
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/deliver", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["code"] == 0
|
||
|
||
def test_deliver_from_wrong_status(self, client, driver_headers, make_task):
|
||
"""TASK-009: pending 状态不能直接送达。"""
|
||
task = make_task(status="pending", driver_id=5)
|
||
resp = client.post(f"/api/driver/tasks/{task.id}/deliver", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
assert resp.status_code in (400, 409)
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskStatusFlow:
|
||
"""TASK-012: 任务状态流转完整性测试。"""
|
||
|
||
def test_full_task_flow(self, client, driver_headers, make_task):
|
||
"""完整状态流转: pending → accepted → picked_up → delivered
|
||
|
||
注意: 揽货前需要先提交运单号
|
||
"""
|
||
# 1. 创建任务
|
||
task = make_task(status="pending", driver_id=5)
|
||
assert task.status == "pending"
|
||
|
||
# 2. 接单
|
||
accept_resp = client.post(f"/api/driver/tasks/{task.id}/accept", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
assert accept_resp.status_code == 200
|
||
|
||
# 3. 提交运单号
|
||
client.post(f"/api/driver/tasks/{task.id}/waybills", headers=driver_headers, json={
|
||
"waybills": [{
|
||
"tracking_number": "SF1234567890",
|
||
"express_company": "shunfeng",
|
||
"express_name": "顺丰速运",
|
||
}],
|
||
})
|
||
|
||
# 4. 揽货
|
||
pickup_resp = client.post(f"/api/driver/tasks/{task.id}/pickup", headers=driver_headers, json={
|
||
"photo_files": [{"file_url": "https://oss.example.com/pickup.jpg", "file_name": "pickup.jpg"}],
|
||
"video_files": [],
|
||
})
|
||
assert pickup_resp.status_code == 200
|
||
|
||
# 5. 送达
|
||
deliver_resp = client.post(f"/api/driver/tasks/{task.id}/deliver", headers=driver_headers, json={
|
||
"photo_files": [],
|
||
"video_files": [],
|
||
})
|
||
assert deliver_resp.status_code == 200
|
||
|
||
|
||
@pytest.mark.logistics
|
||
@pytest.mark.p0
|
||
class TestTaskOrderSync:
|
||
"""TASK-012: 任务与订单状态联动测试。"""
|
||
|
||
def test_task_creation_updates_order(self, client, manager_headers, make_order):
|
||
"""创建任务后订单状态应更新。"""
|
||
order = make_order(order_status="approved")
|
||
|
||
# 创建任务
|
||
task_resp = client.post("/api/logistics/tasks", headers=manager_headers, json={
|
||
"order_id": order.id,
|
||
"driver_id": 5,
|
||
"pickup_address": "泰兴工厂",
|
||
"delivery_address": "上海仓库",
|
||
"pickup_content": "测试货物",
|
||
"quantity": 10,
|
||
"factory_id": order.factory_id,
|
||
})
|
||
assert task_resp.status_code == 200
|
||
|
||
# 检查订单状态是否更新
|
||
order_resp = client.get(f"/api/orders/{order.id}", headers=manager_headers)
|
||
order_status = order_resp.json()["data"]["order_status"]
|
||
# 订单状态应该更新为履约相关状态
|
||
assert order_status in ("pending_driver", "pending_driver_accept", "accepted", "in_progress", "pending_factory")
|