refactor(frontend): 重命名前端项目为 gym-manage-web

This commit is contained in:
张翔
2026-04-17 18:37:45 +08:00
parent deb961c427
commit 45bb89fc7f
140 changed files with 2 additions and 2 deletions
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { AdminRole } from '../admin.role';
describe('AdminRole', () => {
it('should have admin credentials', () => {
expect(AdminRole.name).toBe('admin');
expect(AdminRole.displayName).toBe('超级管理员');
expect(AdminRole.credentials.username).toBe('admin');
expect(AdminRole.credentials.password).toBe('Test@123');
});
it('should have all permissions', () => {
expect(AdminRole.permissions).toContain('user:*');
expect(AdminRole.permissions).toContain('role:*');
expect(AdminRole.permissions).toContain('menu:*');
expect(AdminRole.cannotAccess).toHaveLength(0);
});
it('should be able to create all resources', () => {
expect(AdminRole.expectedBehaviors.canCreate).toContain('user');
expect(AdminRole.expectedBehaviors.canCreate).toContain('role');
expect(AdminRole.expectedBehaviors.canCreate).toContain('menu');
});
});
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import type { RoleDefinition } from '../base.role';
describe('RoleDefinition', () => {
it('should define required role properties', () => {
const role: RoleDefinition = {
name: 'test',
displayName: '测试角色',
credentials: {
username: 'testuser',
password: 'Test@123'
},
permissions: ['test:read', 'test:write'],
cannotAccess: ['/admin'],
expectedBehaviors: {
canCreate: ['test'],
canRead: ['test'],
canUpdate: ['test'],
canDelete: []
}
};
expect(role.name).toBe('test');
expect(role.displayName).toBe('测试角色');
expect(role.credentials.username).toBe('testuser');
expect(role.credentials.password).toBe('Test@123');
expect(role.permissions).toHaveLength(2);
expect(role.cannotAccess).toHaveLength(1);
});
});
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { RoleFactory } from '../role-factory';
describe('RoleFactory', () => {
it('should get admin role', () => {
const role = RoleFactory.getRole('admin');
expect(role.name).toBe('admin');
expect(role.credentials.username).toBe('admin');
});
it('should get user role', () => {
const role = RoleFactory.getRole('user');
expect(role.name).toBe('user');
expect(role.credentials.username).toBe('normaluser');
});
it('should throw error for unknown role', () => {
expect(() => RoleFactory.getRole('unknown')).toThrow("Role 'unknown' not found");
});
it('should get all roles', () => {
const roles = RoleFactory.getAllRoles();
expect(roles).toHaveLength(3);
expect(roles.map(r => r.name)).toContain('admin');
expect(roles.map(r => r.name)).toContain('user');
expect(roles.map(r => r.name)).toContain('test');
});
});
@@ -0,0 +1,25 @@
import type { RoleDefinition } from './base.role';
export const AdminRole: RoleDefinition = {
name: 'admin',
displayName: '超级管理员',
credentials: {
username: 'admin',
password: 'Test@123'
},
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']
}
};
@@ -0,0 +1,16 @@
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[];
};
}
@@ -0,0 +1,24 @@
import type { RoleDefinition } from './base.role';
import { AdminRole } from './admin.role';
import { UserRole } from './user.role';
import { TestRole } from './test.role';
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());
}
}
@@ -0,0 +1,24 @@
import type { RoleDefinition } from './base.role';
export const TestRole: RoleDefinition = {
name: 'test',
displayName: '测试用户',
credentials: {
username: 'e2e_test_user',
password: 'Test@123'
},
permissions: [
'test:read',
'test:write'
],
cannotAccess: [
'/user-management',
'/role-management'
],
expectedBehaviors: {
canCreate: ['test'],
canRead: ['test'],
canUpdate: ['test'],
canDelete: []
}
};
@@ -0,0 +1,26 @@
import type { RoleDefinition } from './base.role';
export const UserRole: RoleDefinition = {
name: 'user',
displayName: '普通用户',
credentials: {
username: 'normaluser',
password: 'Test@123'
},
permissions: [
'user:read:self',
'user:update:self'
],
cannotAccess: [
'/user-management',
'/role-management',
'/menu-management',
'/system-config'
],
expectedBehaviors: {
canCreate: [],
canRead: ['self'],
canUpdate: ['self'],
canDelete: []
}
};
@@ -0,0 +1,68 @@
import { describe, it, expect, vi } from 'vitest';
import { PermissionHelper } from '../permission-helper';
// Mock Playwright
vi.mock('@playwright/test', () => ({
expect: Object.assign(vi.fn(), {
extend: vi.fn().mockReturnValue(expect),
}),
}));
describe('PermissionHelper', () => {
it('should create PermissionHelper instance', () => {
const mockPage = {
goto: vi.fn(),
url: vi.fn().mockReturnValue('http://localhost:3000/dashboard'),
locator: vi.fn().mockReturnValue({
count: vi.fn().mockResolvedValue(0),
}),
} as any;
const helper = new PermissionHelper(mockPage);
expect(helper).toBeDefined();
});
it('should have verifyCanAccess method', () => {
const mockPage = {
goto: vi.fn(),
url: vi.fn().mockReturnValue('http://localhost:3000/dashboard'),
locator: vi.fn(),
} as any;
const helper = new PermissionHelper(mockPage);
expect(typeof helper.verifyCanAccess).toBe('function');
});
it('should have verifyCannotAccess method', () => {
const mockPage = {
goto: vi.fn(),
url: vi.fn(),
locator: vi.fn(),
} as any;
const helper = new PermissionHelper(mockPage);
expect(typeof helper.verifyCannotAccess).toBe('function');
});
it('should have verifyRolePermissions method', () => {
const mockPage = {
goto: vi.fn(),
url: vi.fn(),
locator: vi.fn(),
} as any;
const helper = new PermissionHelper(mockPage);
expect(typeof helper.verifyRolePermissions).toBe('function');
});
it('should have verifyPermissionBoundary method', () => {
const mockPage = {
goto: vi.fn(),
url: vi.fn(),
locator: vi.fn(),
} as any;
const helper = new PermissionHelper(mockPage);
expect(typeof helper.verifyPermissionBoundary).toBe('function');
});
});
@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { RoleAuthManager } from '../role-auth-manager';
// Mock fetch
global.fetch = vi.fn();
describe('RoleAuthManager', () => {
beforeEach(() => {
RoleAuthManager.clearCache();
vi.clearAllMocks();
});
it('should authenticate and cache token', async () => {
const mockToken = 'mock-jwt-token-12345';
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { token: mockToken } })
});
const token = await RoleAuthManager.getRoleToken('admin');
expect(token).toBe(mockToken);
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/api/auth/login'),
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('admin')
})
);
});
it('should return cached token on second call', async () => {
const mockToken = 'cached-token';
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { token: mockToken } })
});
const token1 = await RoleAuthManager.getRoleToken('admin');
const token2 = await RoleAuthManager.getRoleToken('admin');
expect(token1).toBe(token2);
expect(global.fetch).toHaveBeenCalledTimes(1);
});
it('should throw error for unknown role', async () => {
await expect(RoleAuthManager.getRoleToken('unknown')).rejects.toThrow("Role 'unknown' not found");
});
it('should throw error on authentication failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false,
statusText: 'Unauthorized'
});
await expect(RoleAuthManager.getRoleToken('admin')).rejects.toThrow('Authentication failed');
});
it('should clear specific role token', async () => {
const mockToken = 'token-to-clear';
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { token: mockToken } })
});
await RoleAuthManager.getRoleToken('admin');
RoleAuthManager.clearRoleToken('admin');
// 再次获取应该重新认证
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { token: 'new-token' } })
});
const newToken = await RoleAuthManager.getRoleToken('admin');
expect(newToken).toBe('new-token');
expect(global.fetch).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,117 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TestDataManager, getTestDataManager } from '../test-data-manager';
global.fetch = vi.fn();
describe('TestDataManager', () => {
let manager: TestDataManager;
beforeEach(() => {
manager = TestDataManager.getInstance();
manager.clearTracking();
vi.clearAllMocks();
});
it('should be a singleton', () => {
const instance1 = getTestDataManager();
const instance2 = getTestDataManager();
expect(instance1).toBe(instance2);
});
it('should create user and track it', async () => {
const mockUserId = 'user-123';
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { id: mockUserId } })
});
const userData = {
username: 'testuser',
password: 'Test@123',
email: 'test@example.com',
};
const result = await manager.createUser(userData);
expect(result.id).toBe(mockUserId);
expect(result.type).toBe('user');
expect(result.data.username).toBe('testuser');
expect(manager.getCreatedData('user')).toHaveLength(1);
});
it('should create role and track it', async () => {
const mockRoleId = 'role-456';
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { id: mockRoleId } })
});
const roleData = {
roleName: '测试角色',
roleKey: 'test_role',
};
const result = await manager.createRole(roleData);
expect(result.id).toBe(mockRoleId);
expect(result.type).toBe('role');
expect(manager.getCreatedData('role')).toHaveLength(1);
});
it('should cleanup created data', async () => {
(global.fetch as any)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { id: 'user-1' } })
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { id: 'user-2' } })
})
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ ok: true });
await manager.createUser({ username: 'user1', password: 'Test@123', email: 'user1@test.com' });
await manager.createUser({ username: 'user2', password: 'Test@123', email: 'user2@test.com' });
expect(manager.getCreatedData('user')).toHaveLength(2);
await manager.cleanup('user');
expect(manager.getCreatedData('user')).toHaveLength(0);
expect(global.fetch).toHaveBeenCalledTimes(4); // 2 creates + 2 deletes
});
it('should cleanup all data types when no type specified', async () => {
(global.fetch as any)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { id: 'user-1' } })
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { id: 'role-1' } })
})
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ ok: true });
await manager.createUser({ username: 'user1', password: 'Test@123', email: 'user1@test.com' });
await manager.createRole({ roleName: '角色1', roleKey: 'role1' });
await manager.cleanup();
expect(manager.getCreatedData('user')).toHaveLength(0);
expect(manager.getCreatedData('role')).toHaveLength(0);
});
it('should throw error on creation failure', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: false,
statusText: 'Bad Request'
});
await expect(
manager.createUser({ username: 'test', password: 'Test@123', email: 'test@test.com' })
).rejects.toThrow('Failed to create user');
});
});
@@ -0,0 +1,76 @@
import { Page, BrowserContext } from '@playwright/test';
import { RoleFactory } from '../roles/role-factory';
import { RoleAuthManager } from './role-auth-manager';
import type { RoleDefinition } from '../roles/base.role';
export class AuthHelper {
constructor(
private page: Page,
private context: BrowserContext
) {}
async loginAsRole(roleName: string, useTokenInjection: boolean = true): Promise<void> {
const role = RoleFactory.getRole(roleName);
if (useTokenInjection) {
await this.injectToken(role);
} else {
await this.performLogin(role);
}
}
private async injectToken(role: RoleDefinition): Promise<void> {
const token = await RoleAuthManager.getRoleToken(role.name);
// 注入token到localStorage
await this.page.addInitScript((token) => {
localStorage.setItem('token', token);
localStorage.setItem('username', 'admin');
}, token);
// 设置cookie
await this.context.addCookies([
{
name: 'token',
value: token,
domain: 'localhost',
path: '/',
}
]);
}
private async performLogin(role: RoleDefinition): Promise<void> {
await this.page.goto('/login');
await this.page.fill('input[placeholder*="用户名"]', role.credentials.username);
await this.page.fill('input[placeholder*="密码"]', role.credentials.password);
await this.page.click('button[type="submit"]');
// 等待登录成功跳转
await this.page.waitForURL(/\/(dashboard|home)?/, { timeout: 10000 });
}
async logout(): Promise<void> {
await this.page.click('[data-testid="user-menu"]');
await this.page.click('[data-testid="logout-button"]');
await this.page.waitForURL('/login');
}
async clearAuth(): Promise<void> {
await this.context.clearCookies();
await this.page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
}
}
export async function createAuthenticatedPage(
page: Page,
context: BrowserContext,
roleName: string
): Promise<AuthHelper> {
const helper = new AuthHelper(page, context);
await helper.loginAsRole(roleName);
return helper;
}
@@ -0,0 +1,131 @@
import { Page, expect } from '@playwright/test';
import type { RoleDefinition } from '../roles/base.role';
export class PermissionHelper {
constructor(private page: Page) {}
async verifyCanAccess(path: string): Promise<void> {
await this.page.goto(path);
await expect(this.page).not.toHaveURL(/\/login/);
await expect(this.page).not.toHaveURL(/\/403/);
await expect(this.page).not.toHaveURL(/\/404/);
}
async verifyCannotAccess(path: string): Promise<void> {
await this.page.goto(path);
// 应该被重定向到登录页或显示403错误
const url = this.page.url();
const isForbidden = url.includes('/403') || url.includes('/login');
expect(isForbidden || await this.isAccessDenied()).toBeTruthy();
}
private async isAccessDenied(): Promise<boolean> {
const deniedMessage = this.page.locator('text=/无权限|权限不足|Access Denied|Forbidden/i');
return await deniedMessage.count() > 0;
}
async verifyCanCreate(_resource: string, createButtonSelector: string): Promise<void> {
const createButton = this.page.locator(createButtonSelector);
await expect(createButton).toBeVisible();
await expect(createButton).toBeEnabled();
}
async verifyCannotCreate(_resource: string, createButtonSelector: string): Promise<void> {
const createButton = this.page.locator(createButtonSelector);
const count = await createButton.count();
if (count > 0) {
await expect(createButton).not.toBeVisible();
}
}
async verifyCanEdit(_resourceId: string, editButtonSelector: string): Promise<void> {
const editButton = this.page.locator(editButtonSelector);
await expect(editButton).toBeVisible();
await expect(editButton).toBeEnabled();
}
async verifyCannotEdit(_resourceId: string, editButtonSelector: string): Promise<void> {
const editButton = this.page.locator(editButtonSelector);
const count = await editButton.count();
if (count > 0) {
await expect(editButton).not.toBeVisible();
}
}
async verifyCanDelete(_resourceId: string, deleteButtonSelector: string): Promise<void> {
const deleteButton = this.page.locator(deleteButtonSelector);
await expect(deleteButton).toBeVisible();
await expect(deleteButton).toBeEnabled();
}
async verifyCannotDelete(_resourceId: string, deleteButtonSelector: string): Promise<void> {
const deleteButton = this.page.locator(deleteButtonSelector);
const count = await deleteButton.count();
if (count > 0) {
await expect(deleteButton).not.toBeVisible();
}
}
async verifyRolePermissions(role: RoleDefinition): Promise<void> {
// 验证可访问的路径
for (const path of role.expectedBehaviors.canRead) {
if (path !== 'self') {
await this.verifyCanAccess(`/${path}`);
}
}
// 验证不可访问的路径
for (const path of role.cannotAccess) {
await this.verifyCannotAccess(path);
}
}
async verifyPermissionBoundary(
role: RoleDefinition,
testScenarios: {
resource: string;
path: string;
createButton?: string;
editButton?: string;
deleteButton?: string;
}
): Promise<void> {
await this.page.goto(testScenarios.path);
// 验证创建权限
if (testScenarios.createButton) {
if (role.expectedBehaviors.canCreate.includes(testScenarios.resource)) {
await this.verifyCanCreate(testScenarios.resource, testScenarios.createButton);
} else {
await this.verifyCannotCreate(testScenarios.resource, testScenarios.createButton);
}
}
// 验证编辑权限
if (testScenarios.editButton) {
if (role.expectedBehaviors.canUpdate.includes(testScenarios.resource)) {
await this.verifyCanEdit(testScenarios.resource, testScenarios.editButton);
} else {
await this.verifyCannotEdit(testScenarios.resource, testScenarios.editButton);
}
}
// 验证删除权限
if (testScenarios.deleteButton) {
if (role.expectedBehaviors.canDelete.includes(testScenarios.resource)) {
await this.verifyCanDelete(testScenarios.resource, testScenarios.deleteButton);
} else {
await this.verifyCannotDelete(testScenarios.resource, testScenarios.deleteButton);
}
}
}
}
export function createPermissionHelper(page: Page): PermissionHelper {
return new PermissionHelper(page);
}
@@ -0,0 +1,59 @@
import { RoleFactory } from '../roles/role-factory';
interface TokenCache {
token: string;
expiresAt: number;
}
export class RoleAuthManager {
private static tokenCache: Map<string, TokenCache> = new Map();
private static readonly API_BASE_URL = process.env.VITE_API_BASE_URL || 'http://localhost:8084';
private static readonly TOKEN_EXPIRY_BUFFER = 60000;
static async getRoleToken(roleName: string): Promise<string> {
const cached = this.tokenCache.get(roleName);
if (cached && cached.expiresAt > Date.now() + this.TOKEN_EXPIRY_BUFFER) {
return cached.token;
}
const role = RoleFactory.getRole(roleName);
const token = await this.authenticateWithBackend(role.credentials);
this.tokenCache.set(roleName, {
token,
expiresAt: Date.now() + 3600000
});
return token;
}
private static async authenticateWithBackend(credentials: { username: string; password: string }): Promise<string> {
const path = '/api/auth/login';
const body = JSON.stringify(credentials);
const response = await fetch(`${this.API_BASE_URL}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Authentication failed for user ${credentials.username}: ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data.data?.token || data.token;
}
static clearCache(): void {
this.tokenCache.clear();
}
static clearRoleToken(roleName: string): void {
this.tokenCache.delete(roleName);
}
}
@@ -0,0 +1,150 @@
import { Page } from '@playwright/test';
export interface TestData {
id: string;
type: string;
data: Record<string, any>;
createdAt: Date;
}
export class TestDataManager {
private static instance: TestDataManager;
private createdData: Map<string, TestData[]> = new Map();
private _page: Page | null = null;
private static readonly API_BASE_URL = process.env.VITE_API_BASE_URL || 'http://localhost:8084';
static getInstance(): TestDataManager {
if (!TestDataManager.instance) {
TestDataManager.instance = new TestDataManager();
}
return TestDataManager.instance;
}
setPage(page: Page): void {
this._page = page;
}
getPage(): Page | null {
return this._page;
}
async createUser(userData: {
username: string;
password: string;
email: string;
phone?: string;
nickname?: string;
}): Promise<TestData> {
const response = await fetch(`${TestDataManager.API_BASE_URL}/api/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...userData,
status: 1,
}),
});
if (!response.ok) {
throw new Error(`Failed to create user: ${response.statusText}`);
}
const result = await response.json();
const testData: TestData = {
id: result.data?.id || result.id,
type: 'user',
data: userData,
createdAt: new Date(),
};
this.trackData('user', testData);
return testData;
}
async createRole(roleData: {
roleName: string;
roleKey: string;
roleSort?: number;
}): Promise<TestData> {
const response = await fetch(`${TestDataManager.API_BASE_URL}/api/roles`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...roleData,
status: 1,
}),
});
if (!response.ok) {
throw new Error(`Failed to create role: ${response.statusText}`);
}
const result = await response.json();
const testData: TestData = {
id: result.data?.id || result.id,
type: 'role',
data: roleData,
createdAt: new Date(),
};
this.trackData('role', testData);
return testData;
}
async cleanup(type?: string): Promise<void> {
const typesToClean = type ? [type] : Array.from(this.createdData.keys());
for (const dataType of typesToClean) {
const items = this.createdData.get(dataType) || [];
for (const item of items.reverse()) {
try {
await this.deleteData(item);
} catch (error) {
console.error(`Failed to cleanup ${dataType} ${item.id}:`, error);
}
}
this.createdData.delete(dataType);
}
}
private async deleteData(data: TestData): Promise<void> {
const endpoint = this.getEndpoint(data.type);
await fetch(`${TestDataManager.API_BASE_URL}${endpoint}/${data.id}`, {
method: 'DELETE',
});
}
private getEndpoint(type: string): string {
const endpoints: Record<string, string> = {
user: '/api/users',
role: '/api/roles',
menu: '/api/menus',
config: '/api/configs',
};
return endpoints[type] || `/api/${type}s`;
}
private trackData(type: string, data: TestData): void {
if (!this.createdData.has(type)) {
this.createdData.set(type, []);
}
this.createdData.get(type)!.push(data);
}
getCreatedData(type: string): TestData[] {
return this.createdData.get(type) || [];
}
clearTracking(): void {
this.createdData.clear();
}
}
export function getTestDataManager(): TestDataManager {
return TestDataManager.getInstance();
}