#!/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()