fix(service-appointment): 状态冲突/越权改为 4xx,修并发下 500 与弱断言漏判
CI E2E (live stack) 在 main@63c18d8 上失败:test_appointment_concurrent_safety 并发「用户取消」与「运营确认」得到 [200, 500]。根因是 ServiceAppointmentService 四处状态/权限守卫抛裸 RuntimeException,GlobalExceptionHandler 无对应映射, 只能兜底成 HTTP 500 —— 「预约不存在」「状态不允许」「越权」被报成服务端故障。 改动: - ServiceAppointmentService:四处守卫改用 BusinessException + ApiResponseCode (不存在→404 NOT_FOUND,越权→403 FORBIDDEN,状态冲突→409 REDUNDANT_OPERATION)。 - ReferralService.confirmReferral:同一家族的四条守卫同样收敛(预约/报名的 不存在、越权、非转介类型)。 - tests/e2e/service_appointment_test.py:把 3 处 `!= 200` 的弱断言收紧为 4xx。 弱断言会把 500 当成「越权被拒」放行 —— 这正是该缺陷长期未被发现的原因, 此前只有并发用例的 `< 500` 断言咬了它一口。 - ServiceAppointmentOwnerGuardTest:新增「终态不可再确认/取消」「预约不存在」 用例,并把越权断言从 RuntimeException 收紧到具体业务码(403)。 验证: - 定向后端测试 35 passed(ServiceAppointment* / MatchmakerReferralPhase13Test)。 - 本机真实栈(重建 backend 镜像):service_appointment_test.py 8/8、全量 E2E 65 passed。 - 负向注入(把守卫改回裸 RuntimeException):2 例精准变红,工作区哈希前后一致。
This commit is contained in:
@@ -296,22 +296,27 @@ def test_cancel_before_confirmation(api, new_user, station_id):
|
||||
|
||||
|
||||
def test_owner_scope_on_detail_and_cancel(api, new_user, station_id):
|
||||
"""归属隔离(V1.1 计划第 488 行):详情与取消只能操作本人预约。"""
|
||||
"""归属隔离(V1.1 计划第 488 行):详情与取消只能操作本人预约。
|
||||
|
||||
断言必须是 **4xx**,不能只写 `!= 200`:后者会把 500 当成「越权被拒」放行。
|
||||
此类服务端错误曾长期被这两处弱断言掩盖(`cancelAppointment` 抛裸
|
||||
RuntimeException → 兜底 500),只有并发用例的 `< 500` 断言才咬了它一口。
|
||||
"""
|
||||
owner = new_user()
|
||||
stranger = new_user()
|
||||
created = _create(api, owner["token"], station_id)
|
||||
|
||||
detail = api.call("GET", f"/service-appointments/{created['id']}", token=stranger["token"])
|
||||
assert detail.status_code != 200, (
|
||||
f"他人不得读取我的预约详情(实际 HTTP {detail.status_code})"
|
||||
assert 400 <= detail.status_code < 500, (
|
||||
f"他人读取我的预约详情应被 4xx 拒绝(实际 HTTP {detail.status_code})"
|
||||
)
|
||||
|
||||
cancel = api.call(
|
||||
"POST", f"/service-appointments/{created['id']}/cancel",
|
||||
token=stranger["token"], json={"reason": "越权取消"},
|
||||
)
|
||||
assert cancel.status_code != 200, (
|
||||
f"他人不得取消我的预约(实际 HTTP {cancel.status_code})"
|
||||
assert 400 <= cancel.status_code < 500, (
|
||||
f"他人取消我的预约应被 4xx 拒绝(实际 HTTP {cancel.status_code})"
|
||||
)
|
||||
|
||||
still = _by_id(api, owner["token"], created["id"])
|
||||
@@ -325,6 +330,8 @@ def test_operator_cannot_cancel_completed(api, new_user, operator_token, station
|
||||
|
||||
这是 `cancelAppointmentByOperator` 明确实现的规则(拒绝 completed / cancelled),
|
||||
与用户侧不校验状态形成对照 —— 仅断言**已实现**的那一侧。
|
||||
|
||||
断言 4xx 而非 `!= 200`:状态冲突应当是 409,不是 500。
|
||||
"""
|
||||
user = new_user()
|
||||
op_token = operator_token(STATION_OPERATOR)
|
||||
@@ -338,8 +345,8 @@ def test_operator_cannot_cancel_completed(api, new_user, operator_token, station
|
||||
"POST", f"/service-appointments/operator/{created['id']}/cancel",
|
||||
token=op_token, json={"reason": "试图取消已完成预约"},
|
||||
)
|
||||
assert resp.status_code != 200, (
|
||||
f"已完成预约不得被运营取消(实际 HTTP {resp.status_code}: {resp.text[:200]})"
|
||||
assert 400 <= resp.status_code < 500, (
|
||||
f"已完成预约不得被运营取消,且必须是 4xx(实际 HTTP {resp.status_code}: {resp.text[:200]})"
|
||||
)
|
||||
|
||||
final = _by_id(api, user["token"], created["id"])
|
||||
|
||||
Reference in New Issue
Block a user