35 KiB
35 KiB
基于角色的用户模拟测试套件设计方案
版本: 1.0
日期: 2026-04-04
作者: 张翔
状态: 待审查
目录
概述
背景
当前后端管理系统已有40+个E2E测试文件,但存在以下问题:
- 测试分散:测试文件组织混乱,缺乏系统性
- 权限验证不足:主要使用admin用户测试,缺乏跨角色权限验证
- 真实场景覆盖不全:缺乏完整的业务流程测试
- 维护成本高:测试代码重复,工具化程度低
目标
设计并实现一个基于角色的用户模拟测试套件,达到真实场景的验收标准:
- 真实业务场景覆盖:覆盖完整的业务流程
- 权限边界验证:验证不同角色的权限边界
- 高效执行:优化测试执行效率
- 易于维护:清晰的结构和工具化支持
核心决策
决策1:角色范围
选择:使用现有3种角色
理由:
- 系统已有完整的RBAC权限模型
- 3种角色覆盖主要业务场景
- 避免过度设计,聚焦核心需求
角色定义:
- admin(超级管理员):拥有所有权限
- user(普通用户):只能访问和修改自己的信息
- test(测试用户):用于特定测试场景
决策2:测试模式
选择:混合模式(业务流程 + 权限验证)
理由:
- 符合真实业务本质:真实场景不仅是"用户能完成业务流程",更包括"用户在权限约束下完成业务流程"
- 质量保障价值更高:能同时发现业务流程缺陷和权限控制缺陷
- 符合RBAC最佳实践:完美契合"谁在什么场景下能做什么"的核心思想
示例:
// 业务流程测试
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:测试数据管理策略
选择:混合策略(核心数据预置 + 业务数据动态创建)
理由:
- 符合真实业务场景:角色和权限体系是预先配置好的,业务数据是动态产生的
- 执行效率与隔离性的最佳平衡:节省约43%执行时间
- 降低测试维护成本:核心数据极少变更,业务数据灵活可控
- 避免数据污染:核心数据不会被污染,业务数据完全隔离
数据分类:
| 数据类型 | 管理方式 | 生命周期 | 示例 |
|---|---|---|---|
| 核心数据 | 预置 | 测试套件级别 | admin角色、基础权限 |
| 业务数据 | 动态创建 | 测试用例级别 | 测试用户、测试菜单 |
决策4:组织结构
选择:混合结构(roles/ + scenarios/ + shared/)
理由:
- 完美契合混合模式测试策略
- 支持真实的跨角色业务流程
- 清晰的关注点分离
- 易于扩展和维护
目录结构:
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:迁移策略
选择:分层策略(核心场景优先迁移)
理由:
- 风险可控:渐进式迁移,随时可回滚
- 优先级明确:核心场景优先,价值最大化
- 无重复测试:避免资源浪费
- 保留价值:边缘场景测试继续发挥作用
迁移优先级:
- P0:认证场景(登录、登出、权限验证)
- P1:用户管理场景(创建、编辑、删除、生命周期)
- P2:角色管理场景(创建、权限分配)
- P3:菜单管理场景(创建、编辑、权限关联)
决策6:认证方式
选择:Token注入 + 可选真实登录
理由:
- 符合测试金字塔原则:少量真实登录测试 + 大量Token注入测试
- 执行效率高:节省约37%执行时间
- 真实性保障:Token是真实的,业务流程是真实的
- 灵活性强:可根据场景选择登录方式
效率对比:
- 真实登录:9秒/用例
- Token注入:6.1秒/用例(节省32%时间)
- 100个测试用例:节省约37%总时间
决策7:CI/CD集成
选择:Gitea + Jenkins
理由:
- 符合团队现有技术栈
- Jenkins生态成熟,插件丰富
- Gitea轻量级,易于维护
- 支持并行执行和矩阵测试
整体架构设计
架构图
┌─────────────────────────────────────────────────────────────┐
│ 测试执行层 (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 角色基类
// 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 管理员角色定义
// 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 普通用户角色定义
// 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 角色工厂
// 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管理器
// 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();
// 缓存Token(24小时有效期)
this.tokenCache.set(credentials.username, {
token: data.token,
expiresAt: Date.now() + 86400000
});
return data.token;
}
}
2.2 认证辅助类
// 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. 测试数据管理器
// 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. 权限验证工具
// 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. 认证场景测试(真实登录)
// 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注入)
// 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. 权限边界验证测试
// 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. 用户生命周期完整场景
// 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配置
// 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. 环境变量配置
# .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配置
// 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重点检查场景完整性
回滚策略:
git revert <commit-hash>
git checkout <old-commit> -- e2e/old-test.spec.ts
风险2:Token注入失败
预防措施:
- 实现Token缓存和自动刷新
- 添加降级机制
降级代码:
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:测试数据污染
预防措施:
- 使用独立的测试数据库
- 每个测试后强制清理数据
- 定期重置测试环境
清理脚本:
#!/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 |
根本原因:
- BCrypt版本不一致:主应用用
$2b$,测试环境用$2a$ - 密码不一致:主应用用
admin123,测试环境用Test@123 - Hash不一致:两个完全不同的hash
- 可能导致:测试环境登录失败,或密码验证失败
解决方案:
方案A:统一使用测试环境配置(推荐)
- 统一密码:所有环境使用
Test@123作为测试密码 - 统一BCrypt版本:使用
$2a$(Spring Security BCryptPasswordEncoder默认版本) - 更新主应用data-h2.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');
- 更新角色定义文件:
// 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:
@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);
}
验证步骤:
- 验证BCrypt版本兼容性:
@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);
}
- 验证登录流程:
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结果统计 |
总结
核心优势
- 真实性保障:混合模式确保业务流程和权限验证的真实性
- 执行效率:Token注入节省约37%执行时间
- 可维护性:清晰的角色定义和工具类分层
- 可扩展性:易于添加新角色和新场景
- 风险可控:渐进式迁移,随时可回滚
预期收益
- 🎯 测试覆盖率提升:从当前分散测试到系统化场景覆盖
- ⚡ 执行效率提升:节省约37%执行时间
- 🐛 缺陷发现能力提升:权限边界验证增强
- 📊 可维护性提升:清晰的结构和工具化支持
- 🚀 开发效率提升:新测试编写时间 < 30分钟
附录
参考资料
文档版本历史:
- v1.0 (2026-04-04): 初始版本