refactor: 迁移shared工具类到src目录
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { RoleFactory } from '../roles/role-factory';
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface TokenCache {
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026';
|
||||
|
||||
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 generateSignatureHeaders(method: string, path: string, body: string): Record<string, string> {
|
||||
const timestamp = Date.now();
|
||||
const nonce = `${timestamp.toString(36)}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
|
||||
const stringToSign = [
|
||||
method.toUpperCase(),
|
||||
path,
|
||||
'',
|
||||
'',
|
||||
timestamp.toString(),
|
||||
nonce
|
||||
].join('\n');
|
||||
|
||||
const signature = crypto
|
||||
.createHmac('sha256', SIGNATURE_SECRET)
|
||||
.update(stringToSign)
|
||||
.digest('base64');
|
||||
|
||||
return {
|
||||
'X-Signature': signature,
|
||||
'X-Timestamp': timestamp.toString(),
|
||||
'X-Nonce': nonce
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user