diff --git a/novalon-manage-web/e2e/role-based-tests/README.md b/novalon-manage-web/e2e/role-based-tests/README.md deleted file mode 100644 index 16950c6..0000000 --- a/novalon-manage-web/e2e/role-based-tests/README.md +++ /dev/null @@ -1,317 +0,0 @@ -# 基于角色的用户模拟测试套件 - -## 概述 - -本测试套件实现了基于角色的用户模拟测试,用于验证后端管理系统的权限边界和业务流程。 - -## 架构设计 - -### 核心组件 - -1. **角色定义系统** (`roles/`) - - `base.role.ts` - 角色定义基类 - - `admin.role.ts` - 管理员角色 - - `user.role.ts` - 普通用户角色 - - `test.role.ts` - 测试用户角色 - - `role-factory.ts` - 角色工厂 - -2. **共享工具** (`shared/`) - - `role-auth-manager.ts` - Token管理器 - - `auth-helper.ts` - 认证辅助工具 - - `test-data-manager.ts` - 测试数据管理器 - - `permission-helper.ts` - 权限验证工具 - -3. **测试场景** (`scenarios/`) - - `authentication/` - 认证场景测试 - - `user-management/` - 用户管理场景测试 - -## 快速开始 - -### 环境准备 - -1. 确保后端服务运行在 `http://localhost:8084` -2. 确保前端服务运行在 `http://localhost:3002` -3. 确保H2数据库已初始化测试数据 - -### 运行测试 - -```bash -# 运行所有单元测试 -pnpm test - -# 运行角色测试项目 -pnpm exec playwright test --project=role-based-tests - -# 运行特定测试文件 -pnpm exec playwright test e2e/role-based-tests/scenarios/authentication/login-flow.spec.ts - -# 运行特定角色的测试 -pnpm exec playwright test --project=role-based-tests --grep "管理员" -``` - -## 角色配置 - -### 测试用户 - -所有测试用户统一使用密码:`Test@123` - -| 用户名 | 角色 | 说明 | -|--------|------|------| -| admin | 超级管理员 | 拥有所有权限 | -| normaluser | 普通用户 | 只能访问个人信息 | -| e2e_test_user | 测试用户 | 用于E2E测试 | - -### 权限定义 - -每个角色定义包含: -- `permissions` - 拥有的权限列表 -- `cannotAccess` - 无法访问的路径 -- `expectedBehaviors` - 预期行为(CRUD权限) - -## 测试场景 - -### 认证场景 - -- 登录流程测试(6个测试用例) - - 管理员用户登录成功 - - 普通用户登录成功 - - 错误密码登录失败 - - 空用户名登录失败 - - 空密码登录失败 - - Token注入登录 - -- 登出流程测试(4个测试用例) - - 用户登出成功 - - 登出后无法访问受保护页面 - - 登出后Token被清除 - - 多角色登出测试 - -### 用户管理场景 - -- 管理员创建用户测试(5个测试用例) - - 管理员可以创建新用户 - - 管理员可以编辑用户信息 - - 管理员可以删除用户 - - 创建用户时用户名重复验证 - - 创建用户时邮箱格式验证 - -- 权限边界验证测试(11个测试用例) - - 管理员权限验证(5个) - - 普通用户权限验证(4个) - - 测试用户权限验证(2个) - - 跨角色权限对比测试 - -## 测试数据管理 - -### 自动清理 - -测试数据管理器会自动跟踪创建的测试数据,并在测试结束后清理: - -```typescript -import { getTestDataManager } from '../shared/test-data-manager'; - -test.afterEach(async () => { - await getTestDataManager().cleanup('user'); -}); -``` - -### 手动创建测试数据 - -```typescript -const testDataManager = getTestDataManager(); - -const user = await testDataManager.createUser({ - username: 'testuser', - password: 'Test@123', - email: 'test@example.com', -}); -``` - -## 认证方式 - -### Token注入(推荐) - -```typescript -import { createAuthenticatedPage } from '../shared/auth-helper'; - -test.beforeEach(async ({ page, context }) => { - await createAuthenticatedPage(page, context, 'admin'); -}); -``` - -### 真实登录 - -```typescript -import { AuthHelper } from '../shared/auth-helper'; - -const authHelper = new AuthHelper(page, context); -await authHelper.loginAsRole('admin', false); // false表示使用真实登录 -``` - -## 权限验证 - -```typescript -import { createPermissionHelper } from '../shared/permission-helper'; - -const permissionHelper = createPermissionHelper(page); - -// 验证可以访问 -await permissionHelper.verifyCanAccess('/user-management'); - -// 验证无法访问 -await permissionHelper.verifyCannotAccess('/role-management'); - -// 验证角色权限边界 -const role = RoleFactory.getRole('admin'); -await permissionHelper.verifyRolePermissions(role); -``` - -## 最佳实践 - -1. **使用Token注入**:提升测试执行效率 -2. **遵循TDD原则**:先写测试,再实现功能 -3. **测试数据隔离**:每个测试独立创建和清理数据 -4. **权限边界验证**:确保每个角色的权限边界清晰 -5. **跨浏览器测试**:在Chrome、Firefox、Safari上运行测试 - -## 故障排查 - -### 登录失败 - -1. 检查后端服务是否运行 -2. 检查数据库是否初始化 -3. 检查密码是否正确(应为 `Test@123`) - -### 权限验证失败 - -1. 检查角色定义是否正确 -2. 检查后端权限配置 -3. 检查前端路由守卫 - -### 测试数据清理失败 - -1. 检查数据库连接 -2. 检查API权限 -3. 手动清理测试数据 - -## CI/CD集成 - -### Jenkins Pipeline示例 - -```groovy -stage('Role-Based Tests') { - steps { - sh 'pnpm install' - sh 'pnpm exec playwright test --project=role-based-tests' - } - post { - always { - publishHTML([ - allowMissing: false, - alwaysLinkToLastBuild: true, - keepAll: true, - reportDir: 'playwright-report', - reportFiles: 'index.html', - reportName: 'Playwright Report' - ]) - } - } -} -``` - -## 维护指南 - -### 添加新角色 - -1. 在 `roles/` 目录创建新的角色定义文件 -2. 在 `role-factory.ts` 中注册新角色 -3. 在 `data-h2.sql` 中添加测试用户数据 -4. 编写对应的测试用例 - -### 添加新测试场景 - -1. 在 `scenarios/` 目录创建新的测试文件 -2. 使用现有的工具类(认证、数据管理、权限验证) -3. 确保测试数据隔离和清理 -4. 更新文档 - -## 统计信息 - -- **单元测试**:172个测试用例 -- **E2E测试**:26个测试场景(角色基础) + 18个测试用例(用户旅程) -- **角色定义**:3个角色 -- **用户旅程测试**:5个工作流 -- **测试覆盖率**:核心功能100% - -## 用户旅程测试 - -### 概述 - -用户旅程测试(User Journey Tests)位于 `e2e/journeys/` 目录,模拟真实用户的完整操作流程,提供更贴近实际使用的测试覆盖。 - -### 测试文件 - -| 文件 | 测试用例数 | 描述 | -|------|-----------|------| -| `admin-complete-workflow.spec.ts` | 5 | 管理员完整工作流(登录、创建角色、创建用户、验证、清理) | -| `user-permission-boundary.spec.ts` | 3 | 用户权限边界验证 | -| `audit-workflow.spec.ts` | 3 | 审计工作流(操作日志、登录日志、搜索筛选) | -| `file-management-workflow.spec.ts` | 3 | 文件管理工作流(上传、搜索、删除) | -| `system-config-workflow.spec.ts` | 4 | 系统配置工作流(配置查看、修改、字典管理、参数管理) | - -### 运行用户旅程测试 - -```bash -# 运行所有用户旅程测试 -pnpm run test:e2e:journeys - -# 运行特定测试文件 -pnpm exec playwright test journeys/admin-complete-workflow.spec.ts - -# 有头模式运行 -pnpm run test:e2e:headed --project=journeys - -# 调试模式 -pnpm run test:e2e:debug journeys/admin-complete-workflow.spec.ts -``` - -### 测试优化成果 - -通过用户旅程测试重构,实现了: - -- **测试文件减少 70%**:从 50 个文件减少到 15 个文件 -- **测试用例减少 64%**:从 418 个用例减少到 150 个用例 -- **执行时间减少 67%**:从 ~30 分钟减少到 ~10 分钟 -- **维护成本降低 60%**:更清晰的测试结构,更少的重复代码 - -### 测试架构对比 - -| 维度 | 优化前 | 优化后 | -|------|--------|--------| -| 测试文件数 | 50 | 15 | -| 测试用例数 | 418 | 150 | -| 执行时间 | ~30分钟 | ~10分钟 | -| 重复测试 | 多个登录测试 | 统一登录流程 | -| 测试类型 | 功能点测试 | 用户旅程测试 | - -## 更新日志 - -### v2.0.0 (2026-04-07) - -- ✅ 实现用户旅程测试架构 -- ✅ 创建 5 个核心用户旅程测试 -- ✅ 删除 18 个冗余测试文件 -- ✅ 启用测试并行执行 -- ✅ 添加测试脚本命令 -- ✅ 优化测试执行效率 3 倍 - -### v1.0.0 (2026-04-04) - -- ✅ 实现角色定义系统 -- ✅ 实现认证辅助工具 -- ✅ 实现测试数据管理器 -- ✅ 实现权限验证工具 -- ✅ 实现认证场景测试 -- ✅ 实现用户管理场景测试 -- ✅ 统一H2数据库密码配置 -- ✅ 配置Playwright测试项目 diff --git a/novalon-manage-web/e2e/role-based-tests/scenarios/authentication/login-flow.spec.ts b/novalon-manage-web/e2e/role-based-tests/scenarios/authentication/login-flow.spec.ts deleted file mode 100644 index e97ccd8..0000000 --- a/novalon-manage-web/e2e/role-based-tests/scenarios/authentication/login-flow.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { RoleFactory } from '@/role-based-tests/roles/role-factory'; -import { createAuthenticatedPage } from '@/role-based-tests/shared/auth-helper'; - -test.describe('登录流程测试', () => { - test('管理员用户登录成功', async ({ page, context }) => { - const role = RoleFactory.getRole('admin'); - - await page.goto('/login'); - - await page.fill('input[placeholder*="用户名"]', role.credentials.username); - await page.fill('input[placeholder*="密码"]', role.credentials.password); - await page.click('button:has-text("登录")'); - - await expect(page).toHaveURL(/\/(dashboard|\/)?/, { timeout: 10000 }); - - await page.waitForLoadState('networkidle'); - }); - - test('普通用户登录成功', async ({ page, context }) => { - const role = RoleFactory.getRole('user'); - - await page.goto('/login'); - - await page.fill('input[placeholder*="用户名"]', role.credentials.username); - await page.fill('input[placeholder*="密码"]', role.credentials.password); - await page.click('button:has-text("登录")'); - - await expect(page).toHaveURL(/\/(dashboard|\/)?/, { timeout: 10000 }); - }); - - test('错误密码登录失败', async ({ page }) => { - await page.goto('/login'); - - await page.fill('input[placeholder*="用户名"]', 'admin'); - await page.fill('input[placeholder*="密码"]', 'wrongpassword'); - - await Promise.all([ - page.waitForResponse(resp => resp.url().includes('/auth/login') && resp.status() === 401), - page.click('button:has-text("登录")') - ]); - - const errorMessage = page.locator('.el-message'); - await expect(errorMessage).toBeVisible({ timeout: 10000 }); - await expect(errorMessage).toContainText(/用户名或密码错误|登录失败/i); - - await expect(page).toHaveURL(/\/login/); - }); - - test('空用户名登录失败', async ({ page }) => { - await page.goto('/login'); - - await page.fill('input[placeholder*="密码"]', 'Test@123'); - await page.click('input[placeholder*="用户名"]'); - await page.click('input[placeholder*="密码"]'); - await page.click('button:has-text("登录")'); - - const validationMessage = page.locator('.el-form-item__error'); - await expect(validationMessage).toBeVisible({ timeout: 5000 }); - }); - - test('空密码登录失败', async ({ page }) => { - await page.goto('/login'); - - await page.fill('input[placeholder*="用户名"]', 'admin'); - await page.click('input[placeholder*="密码"]'); - await page.click('input[placeholder*="用户名"]'); - await page.click('button:has-text("登录")'); - - const validationMessage = page.locator('.el-form-item__error'); - await expect(validationMessage).toBeVisible({ timeout: 5000 }); - }); - - test('Token注入登录', async ({ page, context }) => { - await createAuthenticatedPage(page, context, 'admin'); - - await page.goto('/dashboard'); - - await expect(page).toHaveURL(/\/dashboard/); - - await page.waitForLoadState('networkidle'); - }); -}); diff --git a/novalon-manage-web/e2e/role-based-tests/scenarios/authentication/logout-flow.spec.ts b/novalon-manage-web/e2e/role-based-tests/scenarios/authentication/logout-flow.spec.ts deleted file mode 100644 index 45a7331..0000000 --- a/novalon-manage-web/e2e/role-based-tests/scenarios/authentication/logout-flow.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { RoleFactory } from '@/role-based-tests/roles/role-factory'; -import { AuthHelper } from '@/role-based-tests/shared/auth-helper'; - -test.describe('登出流程测试', () => { - let authHelper: AuthHelper; - - test.beforeEach(async ({ page, context }) => { - authHelper = new AuthHelper(page, context); - await authHelper.loginAsRole('admin'); - }); - - test('用户登出成功', async ({ page }) => { - await page.goto('/dashboard'); - - await page.waitForSelector('.el-dropdown', { state: 'visible' }); - await page.click('.el-dropdown .el-avatar'); - await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 }); - await page.click('.el-dropdown-menu-item:has-text("退出登录")'); - - await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); - - const loginButton = page.locator('button:has-text("登录")'); - await expect(loginButton).toBeVisible(); - }); - - test('登出后无法访问受保护页面', async ({ page }) => { - await page.goto('/dashboard'); - - await page.waitForSelector('.el-dropdown', { state: 'visible' }); - await page.click('.el-dropdown .el-avatar'); - await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 }); - await page.click('.el-dropdown-menu-item:has-text("退出登录")'); - - await expect(page).toHaveURL(/\/login/); - - await page.goto('/users'); - - await expect(page).toHaveURL(/\/login/); - }); - - test('登出后Token被清除', async ({ page, context }) => { - await page.goto('/dashboard'); - - await page.waitForSelector('.el-dropdown', { state: 'visible' }); - await page.click('.el-dropdown .el-avatar'); - await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 }); - await page.click('.el-dropdown-menu-item:has-text("退出登录")'); - - await expect(page).toHaveURL(/\/login/); - - const cookies = await context.cookies(); - const tokenCookie = cookies.find(c => c.name === 'token'); - expect(tokenCookie).toBeUndefined(); - - const localStorageToken = await page.evaluate(() => { - return localStorage.getItem('token'); - }); - expect(localStorageToken).toBeNull(); - }); - - test('多角色登出测试', async ({ page, context }) => { - const roles = ['admin', 'user', 'test']; - - for (const roleName of roles) { - const helper = new AuthHelper(page, context); - await helper.clearAuth(); - await helper.loginAsRole(roleName); - - await page.goto('/dashboard'); - - await page.waitForSelector('.el-dropdown', { state: 'visible' }); - await page.click('.el-dropdown .el-avatar'); - await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 }); - await page.click('.el-dropdown-menu-item:has-text("退出登录")'); - - await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); - } - }); -}); diff --git a/novalon-manage-web/e2e/role-based-tests/scenarios/user-management/admin-creates-user.spec.ts b/novalon-manage-web/e2e/role-based-tests/scenarios/user-management/admin-creates-user.spec.ts deleted file mode 100644 index 9a8101d..0000000 --- a/novalon-manage-web/e2e/role-based-tests/scenarios/user-management/admin-creates-user.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { RoleFactory } from '@/role-based-tests/roles/role-factory'; -import { createAuthenticatedPage } from '@/role-based-tests/shared/auth-helper'; -import { getTestDataManager } from '@/role-based-tests/shared/test-data-manager'; - -test.describe('管理员创建用户测试', () => { - test.beforeEach(async ({ page, context }) => { - await createAuthenticatedPage(page, context, 'admin'); - getTestDataManager().setPage(page); - }); - - test.afterEach(async () => { - await getTestDataManager().cleanup('user'); - }); - - test('管理员可以创建新用户', async ({ page }) => { - await page.goto('/users'); - - await page.click('button:has-text("新增")'); - - const timestamp = Date.now(); - const userData = { - username: `testuser_${timestamp}`, - password: 'Test@123', - email: `testuser_${timestamp}@test.com`, - phone: '13800138000', - nickname: '测试用户', - }; - - await page.fill('input[placeholder*="用户名"]', userData.username); - await page.fill('input[placeholder*="密码"]', userData.password); - await page.fill('input[placeholder*="邮箱"]', userData.email); - await page.fill('input[placeholder*="手机号"]', userData.phone); - await page.fill('input[placeholder*="昵称"]', userData.nickname); - - await page.click('button:has-text("确定")'); - - const successMessage = page.locator('text=/创建成功|操作成功/i'); - await expect(successMessage).toBeVisible({ timeout: 10000 }); - - const createdUser = page.locator(`text=${userData.username}`); - await expect(createdUser).toBeVisible(); - }); - - test('管理员可以编辑用户信息', async ({ page }) => { - await page.goto('/users'); - - const firstEditButton = page.locator('button:has-text("编辑")').first(); - await firstEditButton.click(); - - const nicknameInput = page.locator('input[placeholder*="昵称"]'); - await nicknameInput.fill('更新后的昵称'); - - await page.click('button:has-text("确定")'); - - const successMessage = page.locator('text=/更新成功|操作成功/i'); - await expect(successMessage).toBeVisible({ timeout: 10000 }); - }); - - test('管理员可以删除用户', async ({ page }) => { - await page.goto('/users'); - - const firstDeleteButton = page.locator('button:has-text("删除")').first(); - await firstDeleteButton.click(); - - const confirmButton = page.locator('button:has-text("确定")'); - await confirmButton.click(); - - const successMessage = page.locator('text=/删除成功|操作成功/i'); - await expect(successMessage).toBeVisible({ timeout: 10000 }); - }); - - test('创建用户时用户名重复验证', async ({ page }) => { - await page.goto('/users'); - - await page.click('button:has-text("新增")'); - - await page.fill('input[placeholder*="用户名"]', 'admin'); - await page.fill('input[placeholder*="密码"]', 'Test@123'); - await page.fill('input[placeholder*="邮箱"]', 'admin@test.com'); - - await page.click('button:has-text("确定")'); - - const errorMessage = page.locator('text=/用户名已存在|用户名重复/i'); - await expect(errorMessage).toBeVisible({ timeout: 5000 }); - }); - - test('创建用户时邮箱格式验证', async ({ page }) => { - await page.goto('/users'); - - await page.click('button:has-text("新增")'); - - await page.fill('input[placeholder*="用户名"]', 'testuser'); - await page.fill('input[placeholder*="密码"]', 'Test@123'); - await page.fill('input[placeholder*="邮箱"]', 'invalid-email'); - - await page.click('button:has-text("确定")'); - - const errorMessage = page.locator('text=/邮箱格式不正确|请输入正确的邮箱/i'); - await expect(errorMessage).toBeVisible({ timeout: 5000 }); - }); -}); diff --git a/novalon-manage-web/e2e/role-based-tests/scenarios/user-management/permission-boundary.spec.ts b/novalon-manage-web/e2e/role-based-tests/scenarios/user-management/permission-boundary.spec.ts deleted file mode 100644 index 61c604f..0000000 --- a/novalon-manage-web/e2e/role-based-tests/scenarios/user-management/permission-boundary.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { RoleFactory } from '@/role-based-tests/roles/role-factory'; -import { createAuthenticatedPage } from '@/role-based-tests/shared/auth-helper'; -import { createPermissionHelper } from '@/role-based-tests/shared/permission-helper'; - -test.describe('权限边界验证测试', () => { - test.describe('管理员权限', () => { - test.beforeEach(async ({ page, context }) => { - await createAuthenticatedPage(page, context, 'admin'); - }); - - test('管理员可以访问用户管理页面', async ({ page }) => { - const permissionHelper = createPermissionHelper(page); - const adminRole = RoleFactory.getRole('admin'); - - await permissionHelper.verifyCanAccess('/users'); - }); - - test('管理员可以访问角色管理页面', async ({ page }) => { - const permissionHelper = createPermissionHelper(page); - - await permissionHelper.verifyCanAccess('/roles'); - }); - - test('管理员可以创建用户', async ({ page }) => { - await page.goto('/users'); - - const createButton = page.locator('button:has-text("新增用户")'); - await expect(createButton).toBeVisible(); - await expect(createButton).toBeEnabled(); - }); - - test('管理员可以编辑用户', async ({ page }) => { - await page.goto('/users'); - await page.waitForLoadState('networkidle'); - - const editButton = page.locator('button:has-text("编辑")').first(); - await expect(editButton).toBeVisible({ timeout: 5000 }); - }); - - test('管理员可以删除用户', async ({ page }) => { - await page.goto('/users'); - await page.waitForLoadState('networkidle'); - - const deleteButton = page.locator('button:has-text("删除")').first(); - await expect(deleteButton).toBeVisible({ timeout: 5000 }); - }); - }); - - test.describe('普通用户权限', () => { - test.beforeEach(async ({ page, context }) => { - await createAuthenticatedPage(page, context, 'user'); - }); - - test('普通用户无法访问用户管理页面', async ({ page }) => { - const permissionHelper = createPermissionHelper(page); - const userRole = RoleFactory.getRole('user'); - - await permissionHelper.verifyCannotAccess('/users'); - }); - - test('普通用户无法访问角色管理页面', async ({ page }) => { - const permissionHelper = createPermissionHelper(page); - - await permissionHelper.verifyCannotAccess('/roles'); - }); - - test('普通用户可以访问个人中心', async ({ page }) => { - await page.goto('/profile'); - - await expect(page).not.toHaveURL(/\/login/); - await expect(page).not.toHaveURL(/\/403/); - }); - - test('普通用户可以修改个人信息', async ({ page }) => { - await page.goto('/profile'); - - const editButton = page.locator('button:has-text("编辑")'); - const count = await editButton.count(); - - if (count > 0) { - await expect(editButton.first()).toBeVisible(); - } - }); - }); - - test.describe('测试用户权限', () => { - test.beforeEach(async ({ page, context }) => { - await createAuthenticatedPage(page, context, 'test'); - }); - - test('测试用户无法访问用户管理页面', async ({ page }) => { - const permissionHelper = createPermissionHelper(page); - - await permissionHelper.verifyCannotAccess('/users'); - }); - - test('测试用户可以访问测试页面', async ({ page }) => { - await page.goto('/test'); - - await expect(page).not.toHaveURL(/\/login/); - await expect(page).not.toHaveURL(/\/403/); - }); - }); - - test.describe('跨角色权限对比', () => { - test('不同角色访问权限对比', async ({ page, context }) => { - const roles = ['admin', 'user', 'test']; - const protectedPaths = ['/users', '/roles', '/menus']; - - for (const roleName of roles) { - const role = RoleFactory.getRole(roleName); - const helper = new (await import('../../shared/auth-helper')).AuthHelper(page, context); - await helper.clearAuth(); - await helper.loginAsRole(roleName); - - for (const path of protectedPaths) { - await page.goto(path); - - const isForbidden = role.cannotAccess.includes(path); - const url = page.url(); - - if (isForbidden) { - expect(url.includes('/403') || url.includes('/login')).toBeTruthy(); - } else { - expect(url.includes('/403')).toBeFalsy(); - } - } - } - }); - }); -});