Files
gym-manage/docs/superpowers/specs/2026-04-04-role-based-test-suite-design.md
T

1184 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 基于角色的用户模拟测试套件设计方案
**版本**: 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%总时间
---
### 决策7CI/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<string, RoleDefinition> = 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<string, {
token: string;
expiresAt: number;
}> = new Map();
/**
* 获取角色Token(带缓存和自动刷新)
*/
static async getRoleToken(roleName: string): Promise<string> {
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<string> {
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();
// 缓存Token24小时有效期)
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<void> {
if (useFullLogin) {
await this.performFullLogin(page, roleName);
} else {
await this.injectToken(page, roleName);
}
}
/**
* 注入Token(用于业务测试,快速高效)
*/
private static async injectToken(page: Page, roleName: string): Promise<void> {
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<void> {
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<string> = new Set();
private static createdRoles: Set<string> = new Set();
/**
* 生成测试用户数据
*/
static generateTestUser(overrides?: Partial<TestUserData>): 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<void> {
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<void> {
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<void> {
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<void> {
const menuElement = page.locator(`.menu-item:has-text("${menuText}")`);
await expect(menuElement).toBeVisible();
}
/**
* 验证用户看不到指定菜单
*/
static async verifyCannotSeeMenu(page: Page, menuText: string): Promise<void> {
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 <commit-hash>
git checkout <old-commit> -- e2e/old-test.spec.ts
```
---
### 风险2Token注入失败
**预防措施**
- 实现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): 初始版本