237 lines
12 KiB
Python
237 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()
|