Files
railwaysystem/scripts/qa/role_navigation_matrix_browser_validator.py
T

238 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
角色专属自服务菜单可见性与未授权访问浏览器矩阵验证
"""
import os
import sys
import time
import subprocess
import requests
from playwright.sync_api import sync_playwright
FRONTEND_URL = "http://127.0.0.1:1024"
BACKEND_URL = "http://127.0.0.1:8096"
SCREENSHOT_DIR = os.path.abspath("docs/reports/preview-role-navigation-20260922/screenshots")
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
def load_qa_env():
env = {}
env_file = os.path.expanduser("~/.railwaysystem-qa.env")
if os.path.exists(env_file):
with open(env_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
env[k.strip()] = v.strip().strip('"')
return env
def get_captcha_code_from_redis(uuid):
res = subprocess.check_output(['redis-cli', 'get', f'captcha_codes:{uuid}']).decode('utf-8').strip()
return res.strip('"')
class RoleNavValidator:
def __init__(self):
self.env = load_qa_env()
self.qa_password = self.env.get("QA_ADMIN_PASSWORD")
if not self.qa_password:
raise RuntimeError("QA_ADMIN_PASSWORD is required in ~/.railwaysystem-qa.env")
self.results = {}
def log(self, msg):
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True)
def browser_login(self, page, username):
self.log(f"Logging in as {username}...")
page.context.clear_cookies()
try:
page.goto(f"{FRONTEND_URL}/login", wait_until="domcontentloaded")
page.evaluate("() => { localStorage.clear(); sessionStorage.clear(); }")
page.reload(wait_until="domcontentloaded")
except Exception:
page.goto(f"{FRONTEND_URL}/login", wait_until="domcontentloaded")
page.wait_for_selector('input[placeholder="账号"]', timeout=10000)
last_uuid = [None]
def handle_res(res):
if 'captchaImage' in res.url:
try:
last_uuid[0] = res.json().get('uuid')
except Exception:
pass
page.on('response', handle_res)
for _ in range(50):
if last_uuid[0]:
break
page.wait_for_timeout(100)
uuid = last_uuid[0]
code = get_captcha_code_from_redis(uuid)
page.fill('input[placeholder="账号"]', username)
page.fill('input[placeholder="密码"]', self.qa_password)
page.fill('input[placeholder="验证码"]', code)
page.click('button:has-text("登 录")')
for _ in range(40):
page.wait_for_timeout(250)
if '/login' not in page.url:
break
page.wait_for_timeout(1500)
def get_sidebar_menu_titles(self, page):
page.wait_for_selector(".sidebar-container", timeout=10000)
# 获取全部可见或DOM中的菜单项标题
items = page.evaluate("""() => {
const elements = document.querySelectorAll('.sidebar-container .el-menu-item, .sidebar-container .el-submenu__title');
return Array.from(elements).map(el => el.innerText.trim()).filter(t => t.length > 0);
}""")
return items
def run(self):
self.log("==================================================")
self.log("STARTING ROLE NAVIGATION BROWSER MATRIX VALIDATION")
self.log("==================================================")
with sync_playwright() as p:
browser = p.chromium.launch(channel='chrome', headless=True)
context = browser.new_context(viewport={"width": 1440, "height": 900})
page = context.new_page()
# ----------------------------------------------------
# 1. 验证 admin
# ----------------------------------------------------
self.log(">>> [1/4] Validating admin ...")
self.browser_login(page, "admin")
admin_menus = self.get_sidebar_menu_titles(page)
self.log(f"Admin Sidebar Menus: {admin_menus}")
admin_teacher_hidden = "教师工作台" not in admin_menus
admin_student_hidden = "学员中心" not in admin_menus
self.log(f"Admin: 教师工作台 HIDDEN = {admin_teacher_hidden}")
self.log(f"Admin: 学员中心 HIDDEN = {admin_student_hidden}")
# 验证管理类菜单回归
admin_people_visible = any("人员管理" in m for m in admin_menus)
admin_portrait_visible = any("学员画像" in m for m in admin_menus)
admin_system_visible = any("系统管理" in m for m in admin_menus)
self.log(f"Admin: 人员管理保留 = {admin_people_visible}, 学员画像保留 = {admin_portrait_visible}, 系统管理保留 = {admin_system_visible}")
shot_admin_sidebar = os.path.join(SCREENSHOT_DIR, "admin_sidebar_menu.png")
page.screenshot(path=shot_admin_sidebar)
# 测试直接未授权访问 /teacher
self.log("Admin direct navigation test to /teacher ...")
page.goto(f"{FRONTEND_URL}/teacher", wait_until="domcontentloaded")
page.wait_for_timeout(2000)
admin_direct_teacher_url = page.url
admin_teacher_toast_count = page.evaluate("() => document.querySelectorAll('.el-notification').length")
self.log(f"Admin /teacher URL: {admin_direct_teacher_url}, Notification Toast Count: {admin_teacher_toast_count}")
shot_admin_direct_teacher = os.path.join(SCREENSHOT_DIR, "admin_direct_teacher_blocked.png")
page.screenshot(path=shot_admin_direct_teacher)
# 测试直接未授权访问 /student
self.log("Admin direct navigation test to /student ...")
page.goto(f"{FRONTEND_URL}/student", wait_until="domcontentloaded")
page.wait_for_timeout(2000)
admin_direct_student_url = page.url
admin_student_toast_count = page.evaluate("() => document.querySelectorAll('.el-notification').length")
self.log(f"Admin /student URL: {admin_direct_student_url}, Notification Toast Count: {admin_student_toast_count}")
shot_admin_direct_student = os.path.join(SCREENSHOT_DIR, "admin_direct_student_blocked.png")
page.screenshot(path=shot_admin_direct_student)
assert admin_teacher_hidden, "Admin 菜单中不得显示【教师工作台】"
assert admin_student_hidden, "Admin 菜单中不得显示【学员中心】"
assert admin_people_visible, "Admin 菜单必须保留【人员管理】"
assert admin_portrait_visible, "Admin 菜单必须保留【学员画像】"
assert admin_teacher_toast_count <= 1, f"Admin 直接访问 /teacher 产生的 Toast 必须 <= 1,当前为 {admin_teacher_toast_count}"
assert admin_student_toast_count <= 1, f"Admin 直接访问 /student 产生的 Toast 必须 <= 1,当前为 {admin_student_toast_count}"
# ----------------------------------------------------
# 2. 验证 qa_train_admin
# ----------------------------------------------------
self.log(">>> [2/4] Validating qa_train_admin ...")
self.browser_login(page, "qa_train_admin")
train_admin_menus = self.get_sidebar_menu_titles(page)
self.log(f"Train Admin Sidebar Menus: {train_admin_menus}")
train_admin_teacher_hidden = "教师工作台" not in train_admin_menus
train_admin_student_hidden = "学员中心" not in train_admin_menus
train_admin_people_visible = any("人员管理" in m for m in train_admin_menus)
self.log(f"Train Admin: 教师工作台 HIDDEN = {train_admin_teacher_hidden}")
self.log(f"Train Admin: 学员中心 HIDDEN = {train_admin_student_hidden}")
self.log(f"Train Admin: 人员管理保留 = {train_admin_people_visible}")
shot_train_admin_sidebar = os.path.join(SCREENSHOT_DIR, "train_admin_sidebar_menu.png")
page.screenshot(path=shot_train_admin_sidebar)
assert train_admin_teacher_hidden, "Train Admin 菜单中不得显示【教师工作台】"
assert train_admin_student_hidden, "Train Admin 菜单中不得显示【学员中心】"
assert train_admin_people_visible, "Train Admin 菜单必须保留【人员管理】业务菜单"
# ----------------------------------------------------
# 3. 验证 qa_teacher_a
# ----------------------------------------------------
self.log(">>> [3/4] Validating qa_teacher_a ...")
self.browser_login(page, "qa_teacher_a")
teacher_menus = self.get_sidebar_menu_titles(page)
self.log(f"Teacher Sidebar Menus: {teacher_menus}")
teacher_workbench_visible = any("教师工作台" in m for m in teacher_menus)
teacher_student_hidden = "学员中心" not in teacher_menus
self.log(f"Teacher: 教师工作台 VISIBLE = {teacher_workbench_visible}")
self.log(f"Teacher: 学员中心 HIDDEN = {teacher_student_hidden}")
assert teacher_workbench_visible, "Teacher 用户必须显示【教师工作台】"
assert teacher_student_hidden, "Teacher 用户不得显示【学员中心】"
# 访问并验证教师工作台页面正常加载
self.log("Visiting Teacher Workbench page ...")
page.goto(f"{FRONTEND_URL}/teacher", wait_until="networkidle")
page.wait_for_timeout(1500)
teacher_page_content = page.content()
teacher_page_loaded_ok = ("QA_R2_讲师_01" in teacher_page_content or "教师工作台" in teacher_page_content or "任教班级" in teacher_page_content)
self.log(f"Teacher Workbench Page loaded OK: {teacher_page_loaded_ok}")
shot_teacher_page = os.path.join(SCREENSHOT_DIR, "teacher_workbench_loaded.png")
page.screenshot(path=shot_teacher_page)
assert teacher_page_loaded_ok, "教师工作台页面内容必须正常加载"
# ----------------------------------------------------
# 4. 验证 qa_student_a
# ----------------------------------------------------
self.log(">>> [4/4] Validating qa_student_a ...")
self.browser_login(page, "qa_student_a")
student_menus = self.get_sidebar_menu_titles(page)
self.log(f"Student Sidebar Menus: {student_menus}")
student_center_visible = any("学员中心" in m for m in student_menus)
student_teacher_hidden = "教师工作台" not in student_menus
self.log(f"Student: 学员中心 VISIBLE = {student_center_visible}")
self.log(f"Student: 教师工作台 HIDDEN = {student_teacher_hidden}")
assert student_center_visible, "Student 用户必须显示【学员中心】"
assert student_teacher_hidden, "Student 用户不得显示【教师工作台】"
# 访问并验证学员中心页面正常加载
self.log("Visiting Student Center page ...")
page.goto(f"{FRONTEND_URL}/student", wait_until="networkidle")
page.wait_for_timeout(1500)
student_page_content = page.content()
student_page_loaded_ok = ("QA_R2_学员_001" in student_page_content or "个人中心" in student_page_content or "参训记录" in student_page_content or "学员姓名" in student_page_content)
self.log(f"Student Center Page loaded OK: {student_page_loaded_ok}")
shot_student_page = os.path.join(SCREENSHOT_DIR, "student_center_loaded.png")
page.screenshot(path=shot_student_page)
assert student_page_loaded_ok, "学员中心页面内容必须正常加载"
browser.close()
self.log("==================================================")
self.log("ALL 4 ROLES BROWSER MATRIX VALIDATIONS PASSED!")
self.log("==================================================")
if __name__ == "__main__":
validator = RoleNavValidator()
validator.run()