# 基于角色的用户模拟测试套件设计方案

**版本**: 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<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();
    
    // 缓存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<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
```

---

### 风险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): 初始版本
