119 lines
4.3 KiB
Python
119 lines
4.3 KiB
Python
"""V1.2 Task 6 权限/范围看板收口盘点。
|
|||
|
|
|
||
|
|
默认在无活栈时运行静态/API contract 盘点,避免把不可达的真实 E2E 伪装成通过。
|
||
|
|
设置 ``E2E_CONTRACT_ONLY=1`` 后运行本文件;设置 ``E2E_BASE_URL`` 和
|
||
|
|
``E2E_OPERATOR_PASSWORD`` 并移除该变量,再运行同一文件才是活栈验收。
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[2]
|
||
|
|
BACKEND = ROOT / "backend" / "src" / "main" / "java"
|
||
|
|
ADMIN = ROOT / "admin-web" / "src"
|
||
|
|
WORKBENCH = BACKEND / "com/danyang/match/platform/matchmaker/dashboard"
|
||
|
|
|
||
|
|
pytestmark = pytest.mark.e2e
|
||
|
|
|
||
|
|
|
||
|
|
def _read(path: Path) -> str:
|
||
|
|
return path.read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def test_workbench_exposes_fixed_six_metrics_and_server_scope() -> None:
|
||
|
|
service = _read(WORKBENCH / "MatchmakerWorkbenchService.java")
|
||
|
|
controller = _read(WORKBENCH / "MatchmakerWorkbenchController.java")
|
||
|
|
for metric in (
|
||
|
|
"pendingReviews",
|
||
|
|
"matchmakerTodos",
|
||
|
|
"counselingTodos",
|
||
|
|
"serviceStationAppointments",
|
||
|
|
"effectiveMatches",
|
||
|
|
"activityRegistrations",
|
||
|
|
):
|
||
|
|
assert f'metrics.put("{metric}"' in service
|
||
|
|
assert '@RequestMapping("/operator/workbench")' in controller
|
||
|
|
assert '@GetMapping("/dashboard")' in controller
|
||
|
|
assert "operatorId" in service and "organizationId" in service
|
||
|
|
assert "@RequestParam" not in controller
|
||
|
|
|
||
|
|
|
||
|
|
def test_dashboard_does_not_accept_client_scope_overrides() -> None:
|
||
|
|
controller = _read(WORKBENCH / "MatchmakerWorkbenchController.java")
|
||
|
|
assert not re.search(r"(userId|matchmakerId|teamId|scope)\s*=", controller)
|
||
|
|
scope = _read(WORKBENCH / "MatchmakerDashboardScope.java")
|
||
|
|
assert '"ALL".equals(type)' in scope
|
||
|
|
assert '"TEAM".equals(type)' in scope
|
||
|
|
|
||
|
|
|
||
|
|
def test_admin_cards_have_stable_contract_keys_and_capability_status() -> None:
|
||
|
|
dashboard = _read(ADMIN / "services/dashboard.ts")
|
||
|
|
for key in (
|
||
|
|
"pending_reviews",
|
||
|
|
"matchmaker_todos",
|
||
|
|
"counseling_todos",
|
||
|
|
"station_appointments",
|
||
|
|
"effective_matches",
|
||
|
|
"activity_registrations",
|
||
|
|
):
|
||
|
|
assert f"key: '{key}'" in dashboard
|
||
|
|
assert "metricStatus(" in dashboard
|
||
|
|
assert "metricRoute(" in dashboard
|
||
|
|
assert "available" in dashboard
|
||
|
|
|
||
|
|
|
||
|
|
def test_task3_detail_scope_gap_is_detected_and_reported() -> None:
|
||
|
|
"""盘点当前缺口;详情统一 scope 不能被静态检查误报为已完成。"""
|
||
|
|
java_sources = "\n".join(
|
||
|
|
p.read_text(encoding="utf-8")
|
||
|
|
for p in BACKEND.rglob("*.java")
|
||
|
|
)
|
||
|
|
assert "class MatchmakerDashboardMapper" not in java_sources
|
||
|
|
assert "@RequestMapping(\"/operator/workbench/details\")" not in java_sources
|
||
|
|
|
||
|
|
|
||
|
|
def test_task3_gap_is_a_release_blocker() -> None:
|
||
|
|
report = ROOT / "docs" / "Phase-V12-Task3-Detail-Scope-Completion-Report.md"
|
||
|
|
text = _read(report)
|
||
|
|
assert "NOT_IMPLEMENTED" in text
|
||
|
|
assert "六类详情接口尚未统一" in text
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.skipif(os.environ.get("E2E_CONTRACT_ONLY") == "1", reason="contract-only: no live HTTP")
|
||
|
|
def test_user_cannot_access_operator_workbench(api, user_token) -> None:
|
||
|
|
response = api.call("GET", "/operator/workbench/dashboard", token=user_token)
|
||
|
|
assert response.status_code == 403, response.text[:400]
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.skipif(os.environ.get("E2E_CONTRACT_ONLY") == "1", reason="contract-only: no live HTTP")
|
||
|
|
def test_seeded_admin_dashboard_returns_six_metrics(api, admin_token) -> None:
|
||
|
|
data = api.ok("GET", "/operator/workbench/dashboard", token=admin_token)
|
||
|
|
assert data["scope"] == "ALL"
|
||
|
|
metrics = data["metrics"]
|
||
|
|
assert set(metrics) >= {
|
||
|
|
"pendingReviews",
|
||
|
|
"matchmakerTodos",
|
||
|
|
"counselingTodos",
|
||
|
|
"serviceStationAppointments",
|
||
|
|
"effectiveMatches",
|
||
|
|
"activityRegistrations",
|
||
|
|
}
|
||
|
|
for item in metrics.values():
|
||
|
|
assert "available" in item
|
||
|
|
assert "detailPermission" in item
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.skipif(os.environ.get("E2E_CONTRACT_ONLY") == "1", reason="contract-only: no live HTTP")
|
||
|
|
def test_matchmaker_dashboard_scope_is_server_derived(api, operator_token) -> None:
|
||
|
|
data = api.ok(
|
||
|
|
"GET",
|
||
|
|
"/operator/workbench/dashboard",
|
||
|
|
token=operator_token("e2e_matchmaker"),
|
||
|
|
params={"operatorId": "not-the-token-owner", "teamId": "other-team"},
|
||
|
|
)
|
||
|
|
assert data["scope"] in {"SELF", "TEAM"}
|