# 基于角色的用户模拟测试套件设计方案 **版本**: 1.0 **日期**: 2026-04-04 **作者**: 张翔 **状态**: 待审查 --- ## 目录 1. [概述](#概述) 2. [核心决策](#核心决策) 3. [整体架构设计](#整体架构设计) 4. [核心组件设计](#核心组件设计) 5. [测试场景实现](#测试场景实现) 6. [配置和CI/CD集成](#配置和cicd集成) 7. [实施计划](#实施计划) 8. [风险控制](#风险控制) 9. [成功指标](#成功指标) --- ## 概述 ### 背景 当前后端管理系统已有40+个E2E测试文件,但存在以下问题: 1. **测试分散**:测试文件组织混乱,缺乏系统性 2. **权限验证不足**:主要使用admin用户测试,缺乏跨角色权限验证 3. **真实场景覆盖不全**:缺乏完整的业务流程测试 4. **维护成本高**:测试代码重复,工具化程度低 ### 目标 设计并实现一个基于角色的用户模拟测试套件,达到真实场景的验收标准: 1. **真实业务场景覆盖**:覆盖完整的业务流程 2. **权限边界验证**:验证不同角色的权限边界 3. **高效执行**:优化测试执行效率 4. **易于维护**:清晰的结构和工具化支持 --- ## 核心决策 ### 决策1:角色范围 **选择**:使用现有3种角色 **理由**: - 系统已有完整的RBAC权限模型 - 3种角色覆盖主要业务场景 - 避免过度设计,聚焦核心需求 **角色定义**: - **admin(超级管理员)**:拥有所有权限 - **user(普通用户)**:只能访问和修改自己的信息 - **test(测试用户)**:用于特定测试场景 --- ### 决策2:测试模式 **选择**:混合模式(业务流程 + 权限验证) **理由**: 1. 符合真实业务本质:真实场景不仅是"用户能完成业务流程",更包括"用户在权限约束下完成业务流程" 2. 质量保障价值更高:能同时发现业务流程缺陷和权限控制缺陷 3. 符合RBAC最佳实践:完美契合"谁在什么场景下能做什么"的核心思想 **示例**: ```typescript // 业务流程测试 test('管理员创建用户', async ({ page }) => { await loginAsRole(page, 'admin'); await createUser(testUser); await expectUserExists(testUser.username); }); // 权限验证测试(嵌入业务流程中) test('普通用户无法访问用户管理页面', async ({ page }) => { await loginAsRole(page, 'user'); await verifyCannotAccess(page, '/user-management'); }); ``` --- ### 决策3:测试数据管理策略 **选择**:混合策略(核心数据预置 + 业务数据动态创建) **理由**: 1. 符合真实业务场景:角色和权限体系是预先配置好的,业务数据是动态产生的 2. 执行效率与隔离性的最佳平衡:节省约43%执行时间 3. 降低测试维护成本:核心数据极少变更,业务数据灵活可控 4. 避免数据污染:核心数据不会被污染,业务数据完全隔离 **数据分类**: | 数据类型 | 管理方式 | 生命周期 | 示例 | |---------|---------|---------|------| | 核心数据 | 预置 | 测试套件级别 | admin角色、基础权限 | | 业务数据 | 动态创建 | 测试用例级别 | 测试用户、测试菜单 | --- ### 决策4:组织结构 **选择**:混合结构(roles/ + scenarios/ + shared/) **理由**: 1. 完美契合混合模式测试策略 2. 支持真实的跨角色业务流程 3. 清晰的关注点分离 4. 易于扩展和维护 **目录结构**: ``` e2e/role-based-tests/ ├── roles/ # 角色定义 │ ├── base.role.ts │ ├── admin.role.ts │ ├── user.role.ts │ ├── test.role.ts │ └── role-factory.ts ├── scenarios/ # 业务场景测试 │ ├── authentication/ │ ├── user-management/ │ ├── role-management/ │ └── menu-management/ └── shared/ # 共享工具 ├── auth-helper.ts ├── role-auth-manager.ts ├── test-data-manager.ts ├── permission-helper.ts └── workflow-helper.ts ``` --- ### 决策5:迁移策略 **选择**:分层策略(核心场景优先迁移) **理由**: 1. 风险可控:渐进式迁移,随时可回滚 2. 优先级明确:核心场景优先,价值最大化 3. 无重复测试:避免资源浪费 4. 保留价值:边缘场景测试继续发挥作用 **迁移优先级**: - **P0**:认证场景(登录、登出、权限验证) - **P1**:用户管理场景(创建、编辑、删除、生命周期) - **P2**:角色管理场景(创建、权限分配) - **P3**:菜单管理场景(创建、编辑、权限关联) --- ### 决策6:认证方式 **选择**:Token注入 + 可选真实登录 **理由**: 1. 符合测试金字塔原则:少量真实登录测试 + 大量Token注入测试 2. 执行效率高:节省约37%执行时间 3. 真实性保障:Token是真实的,业务流程是真实的 4. 灵活性强:可根据场景选择登录方式 **效率对比**: - 真实登录:9秒/用例 - Token注入:6.1秒/用例(节省32%时间) - 100个测试用例:节省约37%总时间 --- ### 决策7:CI/CD集成 **选择**:Gitea + Jenkins **理由**: 1. 符合团队现有技术栈 2. Jenkins生态成熟,插件丰富 3. Gitea轻量级,易于维护 4. 支持并行执行和矩阵测试 --- ## 整体架构设计 ### 架构图 ``` ┌─────────────────────────────────────────────────────────────┐ │ 测试执行层 (Playwright) │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ scenarios/ │ │ │ │ ├── authentication/ (认证场景 - 真实登录) │ │ │ │ ├── user-management/ (用户管理 - Token注入) │ │ │ │ ├── role-management/ (角色管理 - Token注入) │ │ │ │ └── menu-management/ (菜单管理 - Token注入) │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ 调用 ┌─────────────────────────────────────────────────────────────┐ │ 角色管理层 (Roles) │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ RoleFactory │ │ │ │ ├── AdminRole (管理员角色定义) │ │ │ │ ├── UserRole (普通用户角色定义) │ │ │ │ └── TestRole (测试用户角色定义) │ │ │ └──────────────────────────────────────────────────────┘ │ │ 每个角色包含: │ │ - credentials (登录凭证) │ │ - permissions (权限列表) │ │ - expectedBehaviors (预期行为) │ │ - cannotAccess (禁止访问的资源) │ └─────────────────────────────────────────────────────────────┘ ↓ 使用 ┌─────────────────────────────────────────────────────────────┐ │ 工具层 (Shared) │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ AuthHelper │ │ RoleAuthManager │ │ │ │ - loginAsRole() │ │ - getRoleToken() │ │ │ │ - logout() │ │ - cacheToken() │ │ │ └──────────────────┘ └──────────────────┘ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ TestDataManager │ │ PermissionHelper │ │ │ │ - createUser() │ │ - verifyCan() │ │ │ │ - cleanup() │ │ - verifyCannot() │ │ │ └──────────────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ 依赖 ┌─────────────────────────────────────────────────────────────┐ │ Page Object层 (现有) │ │ LoginPage, UserManagementPage, RoleManagementPage, ... │ └─────────────────────────────────────────────────────────────┘ ↓ 操作 ┌─────────────────────────────────────────────────────────────┐ │ 应用系统 (SUT) │ │ 前端 + 后端API + 数据库 │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 核心组件设计 ### 1. 角色定义系统 #### 1.1 角色基类 ```typescript // roles/base.role.ts export interface RoleDefinition { name: string; displayName: string; credentials: { username: string; password: string; }; permissions: string[]; cannotAccess: string[]; expectedBehaviors: { canCreate: string[]; canRead: string[]; canUpdate: string[]; canDelete: string[]; }; } ``` #### 1.2 管理员角色定义 ```typescript // roles/admin.role.ts export const AdminRole: RoleDefinition = { name: 'admin', displayName: '超级管理员', credentials: { username: 'admin', password: 'admin123' }, permissions: [ 'user:*', 'role:*', 'menu:*', 'config:*', 'log:read', 'dict:*' ], cannotAccess: [], expectedBehaviors: { canCreate: ['user', 'role', 'menu', 'config', 'dict'], canRead: ['user', 'role', 'menu', 'config', 'dict', 'log'], canUpdate: ['user', 'role', 'menu', 'config', 'dict'], canDelete: ['user', 'role', 'menu', 'config', 'dict'] } }; ``` #### 1.3 普通用户角色定义 ```typescript // roles/user.role.ts export const UserRole: RoleDefinition = { name: 'user', displayName: '普通用户', credentials: { username: 'testuser', password: 'Test123!@#' }, permissions: [ 'user:read:self', 'user:update:self' ], cannotAccess: [ '/user-management', '/role-management', '/menu-management', '/system-config' ], expectedBehaviors: { canCreate: [], canRead: ['self'], canUpdate: ['self'], canDelete: [] } }; ``` #### 1.4 角色工厂 ```typescript // roles/role-factory.ts export class RoleFactory { private static roles: Map = new Map([ ['admin', AdminRole], ['user', UserRole], ['test', TestRole] ]); static getRole(roleName: string): RoleDefinition { const role = this.roles.get(roleName); if (!role) { throw new Error(`Role '${roleName}' not found`); } return role; } static getAllRoles(): RoleDefinition[] { return Array.from(this.roles.values()); } } ``` --- ### 2. 认证辅助工具 #### 2.1 Token管理器 ```typescript // shared/role-auth-manager.ts export class RoleAuthManager { private static tokenCache: Map = new Map(); /** * 获取角色Token(带缓存和自动刷新) */ static async getRoleToken(roleName: string): Promise { const cached = this.tokenCache.get(roleName); // 如果Token还有效(提前5分钟刷新) if (cached && cached.expiresAt > Date.now() + 300000) { return cached.token; } // 通过真实API获取Token const role = RoleFactory.getRole(roleName); const token = await this.fetchTokenFromAPI(role.credentials); return token; } /** * 从API获取真实Token */ private static async fetchTokenFromAPI(credentials: { username: string; password: string }): Promise { const response = await fetch(`${API_BASE_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }); const data = await response.json(); // 缓存Token(24小时有效期) this.tokenCache.set(credentials.username, { token: data.token, expiresAt: Date.now() + 86400000 }); return data.token; } } ``` #### 2.2 认证辅助类 ```typescript // shared/auth-helper.ts export class AuthHelper { /** * 以指定角色身份登录(支持两种模式) */ static async loginAsRole( page: Page, roleName: string, useFullLogin: boolean = false ): Promise { if (useFullLogin) { await this.performFullLogin(page, roleName); } else { await this.injectToken(page, roleName); } } /** * 注入Token(用于业务测试,快速高效) */ private static async injectToken(page: Page, roleName: string): Promise { const token = await RoleAuthManager.getRoleToken(roleName); await page.goto('/'); await page.evaluate((token) => { localStorage.setItem('token', token); localStorage.setItem('access_token', token); }, token); await page.reload(); } /** * 执行完整登录流程(用于认证相关测试) */ private static async performFullLogin(page: Page, roleName: string): Promise { const role = RoleFactory.getRole(roleName); const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login(role.credentials.username, role.credentials.password); await page.waitForURL(/\/(dashboard|\/)/); } } ``` --- ### 3. 测试数据管理器 ```typescript // shared/test-data-manager.ts export class TestDataManager { private static createdUsers: Set = new Set(); private static createdRoles: Set = new Set(); /** * 生成测试用户数据 */ static generateTestUser(overrides?: Partial): TestUserData { const uuid = uuidv4().substring(0, 8); return { username: `test_${uuid}`, password: 'Test123!@#', email: `test_${uuid}@example.com`, phone: `138${uuid.substring(0, 8)}`, nickname: `测试用户_${Date.now()}`, ...overrides }; } /** * 记录创建的用户(用于清理) */ static trackUser(username: string): void { this.createdUsers.add(username); } /** * 清理所有测试数据 */ static async cleanupAll(page: Page): Promise { for (const username of this.createdUsers) { await this.deleteUserViaAPI(page, username); } this.createdUsers.clear(); } } ``` --- ### 4. 权限验证工具 ```typescript // shared/permission-helper.ts export class PermissionHelper { /** * 验证用户可以访问指定路径 */ static async verifyCanAccess(page: Page, path: string): Promise { await page.goto(path); // 验证没有跳转到登录页 await expect(page).not.toHaveURL(/.*login/); // 验证没有显示无权限提示 const noPermissionElement = page.locator('.no-permission, .forbidden'); await expect(noPermissionElement).not.toBeVisible(); } /** * 验证用户不能访问指定路径 */ static async verifyCannotAccess(page: Page, path: string): Promise { await page.goto(path); const isLoginPage = page.url().includes('login'); const hasNoPermission = await page.locator('.no-permission').isVisible(); const hasForbidden = await page.locator('text=/403|Forbidden/').isVisible(); expect(isLoginPage || hasNoPermission || hasForbidden).toBeTruthy(); } /** * 验证用户可以看到指定菜单 */ static async verifyCanSeeMenu(page: Page, menuText: string): Promise { const menuElement = page.locator(`.menu-item:has-text("${menuText}")`); await expect(menuElement).toBeVisible(); } /** * 验证用户看不到指定菜单 */ static async verifyCannotSeeMenu(page: Page, menuText: string): Promise { const menuElement = page.locator(`.menu-item:has-text("${menuText}")`); await expect(menuElement).not.toBeVisible(); } } ``` --- ## 测试场景实现 ### 1. 认证场景测试(真实登录) ```typescript // scenarios/authentication/login-flow.spec.ts test.describe('认证流程测试', () => { test('管理员使用正确凭证登录成功', async ({ page }) => { // 使用真实登录流程 await AuthHelper.loginAsRole(page, 'admin', true); // 验证登录成功 await expect(page).toHaveURL(/\/(dashboard|\/)/); const isLoggedIn = await AuthHelper.isLoggedIn(page); expect(isLoggedIn).toBeTruthy(); }); test('管理员使用错误密码登录失败', async ({ page }) => { const role = RoleFactory.getRole('admin'); await page.goto('/login'); await page.fill('[name="username"]', role.credentials.username); await page.fill('[name="password"]', 'wrongpassword'); await page.click('[type="submit"]'); await expect(page).toHaveURL(/.*login/); await expect(page.locator('.error-message')).toBeVisible(); }); }); ``` --- ### 2. 用户管理场景测试(Token注入) ```typescript // scenarios/user-management/admin-creates-user.spec.ts test.describe('管理员创建用户场景', () => { test.beforeEach(async ({ page }) => { // 使用Token注入快速登录 await AuthHelper.loginAsRole(page, 'admin'); }); test.afterEach(async ({ page }) => { // 清理测试数据 await TestDataManager.cleanupAll(page); }); test('管理员创建新用户成功', async ({ page }) => { const testUser = TestDataManager.generateTestUser(); await userManagementPage.goto(); await userManagementPage.clickCreateUser(); await userManagementPage.fillUserForm(testUser); await userManagementPage.submitForm(); const success = await userManagementPage.waitForSuccessMessage(); expect(success).toBeTruthy(); TestDataManager.trackUser(testUser.username); }); }); ``` --- ### 3. 权限边界验证测试 ```typescript // scenarios/user-management/permission-boundary.spec.ts test.describe('用户管理权限边界验证', () => { test('普通用户无法访问用户管理页面', async ({ page }) => { await AuthHelper.loginAsRole(page, 'user'); await PermissionHelper.verifyCannotAccess(page, '/user-management'); }); test('普通用户无法看到用户管理菜单', async ({ page }) => { await AuthHelper.loginAsRole(page, 'user'); await PermissionHelper.verifyCannotSeeMenu(page, '用户管理'); }); test('普通用户无法创建用户', async ({ page }) => { await AuthHelper.loginAsRole(page, 'user'); const token = await page.evaluate(() => localStorage.getItem('token')); const response = await fetch(`${API_BASE_URL}/api/users`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'hacker', password: 'hack123' }) }); expect(response.status).toBe(403); }); }); ``` --- ### 4. 用户生命周期完整场景 ```typescript // scenarios/user-management/user-lifecycle.spec.ts test.describe('用户完整生命周期测试', () => { test('阶段1: 管理员创建用户', async ({ page }) => { await AuthHelper.loginAsRole(page, 'admin'); await userManagementPage.goto(); await userManagementPage.clickCreateUser(); await userManagementPage.fillUserForm(testUser); await userManagementPage.submitForm(); expect(await userManagementPage.waitForSuccessMessage()).toBeTruthy(); TestDataManager.trackUser(testUser.username); }); test('阶段2: 新用户首次登录', async ({ page }) => { await loginPage.goto(); await loginPage.login(testUser.username, testUser.password); await expect(page).toHaveURL(/\/(dashboard|\/)/); await expect(page.locator('text=用户管理')).not.toBeVisible(); }); test('阶段3: 用户修改个人信息', async ({ page }) => { await AuthHelper.loginAsRole(page, 'user'); await page.click('.user-avatar'); await page.click('text=个人中心'); await page.fill('[name="nickname"]', '更新昵称'); await page.click('[type="submit"]'); await expect(page.locator('.success-message')).toBeVisible(); }); test('阶段4: 管理员禁用用户', async ({ page }) => { await AuthHelper.loginAsRole(page, 'admin'); await userManagementPage.goto(); await userManagementPage.search(testUser.username); await userManagementPage.clickStatusButton(1); expect(await userManagementPage.waitForSuccessMessage()).toBeTruthy(); }); test('阶段5: 禁用用户无法登录', async ({ page }) => { await loginPage.goto(); await loginPage.login(testUser.username, testUser.password); await expect(page).toHaveURL(/.*login/); await expect(page.locator('.error-message')).toBeVisible(); }); test('阶段6: 管理员删除用户', async ({ page }) => { await AuthHelper.loginAsRole(page, 'admin'); await userManagementPage.goto(); await userManagementPage.search(testUser.username); await userManagementPage.clickDeleteButton(1); await userManagementPage.confirmDelete(); expect(await userManagementPage.waitForSuccessMessage()).toBeTruthy(); }); }); ``` --- ## 配置和CI/CD集成 ### 1. Playwright配置 ```typescript // playwright.config.ts export default defineConfig({ testDir: './e2e', testMatch: [ '**/role-based-tests/**/*.spec.ts', '**/legacy-tests/**/*.spec.ts' ], timeout: 30000, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [ ['list'], ['html', { outputFolder: 'test-results/html' }], ['junit', { outputFile: 'test-results/junit.xml' }] ], projects: [ { name: 'admin-tests', testMatch: /admin.*\.spec\.ts/, }, { name: 'user-tests', testMatch: /user.*\.spec\.ts/, }, { name: 'auth-tests', testMatch: /authentication.*\.spec\.ts/, }, ], }); ``` --- ### 2. 环境变量配置 ```bash # .env.test VITE_API_BASE_URL=http://localhost:8084 BASE_URL=http://localhost:5173 TEST_TIMEOUT=30000 TEST_RETRIES=2 ADMIN_USERNAME=admin ADMIN_PASSWORD=admin123 USER_USERNAME=testuser USER_PASSWORD=Test123!@# ``` --- ### 3. Jenkins Pipeline配置 ```groovy // Jenkinsfile pipeline { agent { label 'node18-chrome' } environment { ADMIN_PASSWORD = credentials('admin-password') VITE_API_BASE_URL = 'http://localhost:8084' } stages { stage('准备环境') { steps { sh ''' cd novalon-manage-web pnpm install pnpm exec playwright install chromium ''' } } stage('运行基于角色的测试套件') { parallel { stage('管理员角色测试') { steps { sh 'cd novalon-manage-web && pnpm test:admin' } } stage('普通用户角色测试') { steps { sh 'cd novalon-manage-web && pnpm test:user' } } stage('认证流程测试') { steps { sh 'cd novalon-manage-web && pnpm test:auth' } } } } } post { always { junit 'novalon-manage-web/test-results/junit.xml' publishHTML(target: [ reportDir: 'novalon-manage-web/test-results/html', reportFiles: 'index.html', reportName: 'Playwright测试报告' ]) } } } ``` --- ## 实施计划 ### 阶段1:基础设施搭建(第1周) **目标**:建立测试框架基础 **任务清单**: - [ ] **修复H2数据库密码不一致问题**(优先级:P0) - [ ] 统一主应用和测试环境的data-h2.sql密码配置 - [ ] 验证BCrypt版本兼容性 - [ ] 更新角色定义文件中的密码 - [ ] 添加密码验证测试 - [ ] 创建目录结构 - [ ] 实现角色定义系统 - [ ] 实现核心工具类 - [ ] 配置环境变量和Playwright配置 - [ ] 编写单元测试验证工具类 **验收标准**: - ✅ **密码配置一致且验证通过** - ✅ 所有工具类单元测试通过 - ✅ Token获取和注入功能正常 - ✅ 角色定义完整且可扩展 --- ### 阶段2:核心场景迁移(第2-3周) **目标**:迁移高优先级测试场景 **P0 - 认证场景(第2周前半)**: - [ ] login-flow.spec.ts - [ ] logout-flow.spec.ts - [ ] permission-validation.spec.ts **P1 - 用户管理场景(第2周后半)**: - [ ] admin-creates-user.spec.ts - [ ] user-edits-profile.spec.ts - [ ] user-lifecycle.spec.ts - [ ] permission-boundary.spec.ts **P2 - 角色管理场景(第3周前半)**: - [ ] admin-manages-roles.spec.ts - [ ] permission-assignment.spec.ts **P3 - 菜单管理场景(第3周后半)**: - [ ] admin-manages-menus.spec.ts **验收标准**: - ✅ 每个场景测试通过率100% - ✅ 测试覆盖率不低于旧测试 - ✅ 执行时间在可接受范围内 --- ### 阶段3:验证和优化(第4周) **目标**:确保质量并优化性能 **任务清单**: - [ ] 全量运行新测试套件 - [ ] 对比新旧测试覆盖率 - [ ] 性能基准测试 - [ ] 跨浏览器兼容性测试 - [ ] 文档完善 **验收标准**: - ✅ 测试覆盖率 ≥ 旧测试覆盖率 - ✅ 平均执行时间 ≤ 旧测试执行时间 * 0.7 - ✅ 所有浏览器测试通过 --- ### 阶段4:清理和扩展(第5周及以后) **目标**:清理旧测试并持续改进 **任务清单**: - [ ] 删除已迁移的旧测试文件 - [ ] 保留边缘场景测试 - [ ] 建立测试维护流程 **验收标准**: - ✅ 无重复测试 - ✅ 测试套件结构清晰 --- ## 风险控制 ### 风险1:新测试遗漏关键场景 **预防措施**: - 迁移前详细分析旧测试 - 使用覆盖率工具对比 - Code Review重点检查场景完整性 **回滚策略**: ```bash git revert git checkout -- e2e/old-test.spec.ts ``` --- ### 风险2:Token注入失败 **预防措施**: - 实现Token缓存和自动刷新 - 添加降级机制 **降级代码**: ```typescript static async loginAsRole(page: Page, roleName: string, useFullLogin = false) { if (useFullLogin) { await this.performFullLogin(page, roleName); } else { try { await this.injectToken(page, roleName); } catch (error) { console.warn('Token注入失败,降级使用真实登录'); await this.performFullLogin(page, roleName); } } } ``` --- ### 风险3:测试数据污染 **预防措施**: - 使用独立的测试数据库 - 每个测试后强制清理数据 - 定期重置测试环境 **清理脚本**: ```bash #!/bin/bash psql -U novalon -d novalon_manage_test -c "TRUNCATE users, roles CASCADE;" psql -U novalon -d novalon_manage_test -f db/migration/V2__Insert_initial_data.sql ``` --- ### 风险4:H2数据库密码不一致问题 ⚠️ **问题描述**: 当前系统存在两个data-h2.sql文件,密码配置不一致: | 文件位置 | BCrypt版本 | 密码Hash | 明文密码 | |---------|-----------|---------|---------| | `manage-app/src/main/resources/data-h2.sql` | `$2b$` | `SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy` | `admin123` | | `manage-app/src/test/resources/data-h2.sql` | `$2a$` | `nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C` | `Test@123` | **根本原因**: 1. **BCrypt版本不一致**:主应用用`$2b$`,测试环境用`$2a$` 2. **密码不一致**:主应用用`admin123`,测试环境用`Test@123` 3. **Hash不一致**:两个完全不同的hash 4. **可能导致**:测试环境登录失败,或密码验证失败 **解决方案**: **方案A:统一使用测试环境配置(推荐)** 1. **统一密码**:所有环境使用`Test@123`作为测试密码 2. **统一BCrypt版本**:使用`$2a$`(Spring Security BCryptPasswordEncoder默认版本) 3. **更新主应用data-h2.sql**: ```sql -- 插入测试用户 -- BCrypt哈希值对应明文密码: Test@123 INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by) VALUES (1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'), (2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'), (3, 'normaluser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'), (4, 'guestuser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'), (5, 'disableduser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system'), (10, 'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system'); ``` 4. **更新角色定义文件**: ```typescript // roles/admin.role.ts export const AdminRole: RoleDefinition = { name: 'admin', displayName: '超级管理员', credentials: { username: 'admin', password: 'Test@123' // 统一使用Test@123 }, // ... }; // roles/user.role.ts export const UserRole: RoleDefinition = { name: 'user', displayName: '普通用户', credentials: { username: 'normaluser', password: 'Test@123' // 统一使用Test@123 }, // ... }; ``` **方案B:生成新的统一密码Hash** 使用Spring Security的BCryptPasswordEncoder生成新的hash: ```java @Test public void generateUnifiedPasswordHash() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12); String password = "Test@123"; String hash = passwordEncoder.encode(password); System.out.println("密码: " + password); System.out.println("哈希: " + hash); // 验证 boolean matches = passwordEncoder.matches(password, hash); System.out.println("验证结果: " + matches); } ``` **验证步骤**: 1. **验证BCrypt版本兼容性**: ```java @Test public void verifyBCryptVersions() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12); String password = "Test@123"; // $2a$ hash String hash2a = "$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C"; boolean matches2a = passwordEncoder.matches(password, hash2a); System.out.println("$2a$ hash验证: " + matches2a); // $2b$ hash String hash2b = "$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy"; boolean matches2b = passwordEncoder.matches(password, hash2b); System.out.println("$2b$ hash验证: " + matches2b); } ``` 2. **验证登录流程**: ```typescript test('验证统一密码配置', async ({ page }) => { await AuthHelper.loginAsRole(page, 'admin', true); await expect(page).toHaveURL(/\/(dashboard|\/)/); }); ``` **预防措施**: - 在实施计划第一阶段立即修复此问题 - 添加测试验证密码配置的一致性 - 在CI/CD中添加密码验证步骤 **影响范围**: - ✅ 所有使用H2数据库的测试 - ✅ 所有角色定义文件 - ✅ 所有认证相关测试 --- ## 成功指标 ### 质量指标 | 指标 | 目标值 | 测量方法 | |------|--------|----------| | 测试覆盖率 | ≥ 80% | Jest coverage report | | 测试通过率 | 100% | CI构建结果 | | 缺陷发现率 | 提升20% | Bug统计对比 | | 误报率 | < 5% | Flaky test监控 | --- ### 效率指标 | 指标 | 目标值 | 测量方法 | |------|--------|----------| | 执行时间 | ≤ 旧测试 * 0.7 | CI执行时间统计 | | 维护成本 | 降低30% | 代码变更频率 | | 新测试编写时间 | < 30分钟/场景 | 开发者反馈 | --- ### 业务指标 | 指标 | 目标值 | 测量方法 | |------|--------|----------| | 权限bug发现 | ≥ 5个 | Bug分类统计 | | 回归测试覆盖 | 100%核心场景 | 场景清单检查 | | UAT通过率 | ≥ 95% | UAT结果统计 | --- ## 总结 ### 核心优势 1. **真实性保障**:混合模式确保业务流程和权限验证的真实性 2. **执行效率**:Token注入节省约37%执行时间 3. **可维护性**:清晰的角色定义和工具类分层 4. **可扩展性**:易于添加新角色和新场景 5. **风险可控**:渐进式迁移,随时可回滚 --- ### 预期收益 - 🎯 **测试覆盖率提升**:从当前分散测试到系统化场景覆盖 - ⚡ **执行效率提升**:节省约37%执行时间 - 🐛 **缺陷发现能力提升**:权限边界验证增强 - 📊 **可维护性提升**:清晰的结构和工具化支持 - 🚀 **开发效率提升**:新测试编写时间 < 30分钟 --- ## 附录 ### 参考资料 - [Playwright最佳实践](https://playwright.dev/docs/best-practices) - [RBAC权限模型设计](https://en.wikipedia.org/wiki/Role-based_access_control) - [测试金字塔理论](https://martinfowler.com/articles/practical-test-pyramid.html) --- **文档版本历史**: - v1.0 (2026-04-04): 初始版本