142 lines
5.1 KiB
Python
142 lines
5.1 KiB
Python
|
|
"""浏览器端到端验证:普通用户落地页与路由守卫回退。
|
|||
|
|
|
|||
|
|
工作台收紧为 admin/finance/leader 后,登录默认页不能硬编码 `/dashboard`
|
|||
|
|
(普通用户会被无限重定向)。本脚本验证:
|
|||
|
|
|
|||
|
|
- 普通用户登录后落业务数据页,不出现「无权访问」提示
|
|||
|
|
- 侧边菜单不出现首页/系统管理/报表统计
|
|||
|
|
- 刷新后仍保持登录态与落地页
|
|||
|
|
- 直连 `/dashboard` 或报表页时回退业务数据页并给出权限提示
|
|||
|
|
|
|||
|
|
运行前置:容器、后端 8000、前端 5173 均在运行;设置 ``E2E_ADMIN_PASSWORD``。
|
|||
|
|
|
|||
|
|
运行::
|
|||
|
|
|
|||
|
|
cd frontend && E2E_ADMIN_PASSWORD=<密码> python e2e/role_home_flow.py
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import time
|
|||
|
|
import urllib.error
|
|||
|
|
import urllib.request
|
|||
|
|
import uuid
|
|||
|
|
|
|||
|
|
from playwright.sync_api import Page, sync_playwright
|
|||
|
|
|
|||
|
|
from common import BASE_URL, E2EFailure, current_path, fail, login, toast_text
|
|||
|
|
|
|||
|
|
ADMIN = "admin"
|
|||
|
|
USERNAME = f"e2e_role_{int(time.time())}_{uuid.uuid4().hex[:4]}"
|
|||
|
|
PASSWORD = "E2eRole@2026"
|
|||
|
|
HOME_PATH = "/business/businesses"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _api(method: str, path: str, token: str, body: dict | None = None) -> dict:
|
|||
|
|
"""带令牌的轻量 API 调用(urllib,避免额外依赖)。"""
|
|||
|
|
request = urllib.request.Request(
|
|||
|
|
f"{BASE_URL}{path}",
|
|||
|
|
method=method,
|
|||
|
|
headers={
|
|||
|
|
"Authorization": f"Bearer {token}",
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
},
|
|||
|
|
data=json.dumps(body).encode() if body is not None else None,
|
|||
|
|
)
|
|||
|
|
try:
|
|||
|
|
with urllib.request.urlopen(request, timeout=20) as response:
|
|||
|
|
payload = json.loads(response.read().decode())
|
|||
|
|
except urllib.error.HTTPError as exc:
|
|||
|
|
detail = exc.read().decode()[:200]
|
|||
|
|
raise E2EFailure(f"API {method} {path} HTTP {exc.code}:{detail}") from exc
|
|||
|
|
if payload.get("code") != 200:
|
|||
|
|
raise E2EFailure(f"API {method} {path} 业务失败:{payload.get('message')}")
|
|||
|
|
return payload["data"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _token(page: Page) -> str:
|
|||
|
|
token = page.evaluate("localStorage.getItem('ys_token')")
|
|||
|
|
if not token:
|
|||
|
|
fail("API", "未取到登录令牌")
|
|||
|
|
return token
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_user(page: Page) -> dict:
|
|||
|
|
return _api(
|
|||
|
|
"POST",
|
|||
|
|
"/api/v1/system/users",
|
|||
|
|
_token(page),
|
|||
|
|
{
|
|||
|
|
"username": USERNAME,
|
|||
|
|
"password": PASSWORD,
|
|||
|
|
"real_name": "E2E普通用户",
|
|||
|
|
"roles": ["user"],
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run(page: Page) -> None:
|
|||
|
|
admin_password = os.environ.get("E2E_ADMIN_PASSWORD", "")
|
|||
|
|
if not admin_password:
|
|||
|
|
fail("环境变量", "未设置 E2E_ADMIN_PASSWORD(开发库 admin 密码)")
|
|||
|
|
|
|||
|
|
login(page, ADMIN, admin_password)
|
|||
|
|
user = _create_user(page)
|
|||
|
|
try:
|
|||
|
|
page.evaluate("localStorage.clear()")
|
|||
|
|
login(page, USERNAME, PASSWORD, expected_path=HOME_PATH)
|
|||
|
|
page.wait_for_timeout(500)
|
|||
|
|
print("✓ 1. 普通用户登录 落地业务数据页")
|
|||
|
|
|
|||
|
|
menu_text = page.locator(".app-menu").inner_text()
|
|||
|
|
for forbidden in ("首页", "系统管理", "报表统计"):
|
|||
|
|
if forbidden in menu_text:
|
|||
|
|
fail("菜单权限", f"普通用户看到了「{forbidden}」菜单")
|
|||
|
|
print("✓ 2. 菜单权限 首页/系统管理/报表统计均不可见")
|
|||
|
|
|
|||
|
|
page.reload(wait_until="networkidle")
|
|||
|
|
page.wait_for_timeout(600)
|
|||
|
|
if current_path(page) != HOME_PATH:
|
|||
|
|
fail("刷新恢复", f"刷新后未保持落地页,当前 {page.url}")
|
|||
|
|
print("✓ 3. 刷新恢复 登录态与落地页保持")
|
|||
|
|
|
|||
|
|
for path in ("/dashboard", "/reports/personal-detail"):
|
|||
|
|
page.goto(f"{BASE_URL}{path}", wait_until="networkidle")
|
|||
|
|
page.wait_for_timeout(800)
|
|||
|
|
if current_path(page) != HOME_PATH:
|
|||
|
|
fail("路由守卫", f"直连 {path} 未回退业务数据页,当前 {page.url}")
|
|||
|
|
if "无权访问该页面" not in toast_text(page):
|
|||
|
|
fail("路由守卫", f"直连 {path} 未给出权限提示")
|
|||
|
|
print("✓ 4. 路由守卫 受限页均回退业务数据页并提示")
|
|||
|
|
finally:
|
|||
|
|
# 无论用例是否失败都清理测试账号(重新以 admin 登录取令牌)
|
|||
|
|
page.evaluate("localStorage.clear()")
|
|||
|
|
login(page, ADMIN, admin_password)
|
|||
|
|
_api("DELETE", f"/api/v1/system/users/{user['id']}", _token(page))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> int:
|
|||
|
|
with sync_playwright() as playwright:
|
|||
|
|
browser = playwright.chromium.launch(headless=True)
|
|||
|
|
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
|||
|
|
page.set_default_timeout(15000)
|
|||
|
|
try:
|
|||
|
|
run(page)
|
|||
|
|
except E2EFailure as exc:
|
|||
|
|
page.screenshot(path="e2e/failure.png", full_page=True)
|
|||
|
|
print(f"\n ✗ {exc}")
|
|||
|
|
print(" 已保存截图 e2e/failure.png")
|
|||
|
|
return 1
|
|||
|
|
except Exception as exc: # noqa: BLE001 — 测试脚本需打印完整失败原因
|
|||
|
|
page.screenshot(path="e2e/failure.png", full_page=True)
|
|||
|
|
print(f"\n ✗ 执行中断:{type(exc).__name__}: {exc}")
|
|||
|
|
return 1
|
|||
|
|
finally:
|
|||
|
|
browser.close()
|
|||
|
|
print("\n全部通过。")
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
raise SystemExit(main())
|