"""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_logistics")