feat: 实现认证辅助工具
- 创建 Token 管理器 RoleAuthManager - 创建认证辅助类 AuthHelper - 支持 Token 注入和真实登录两种模式 - 实现 Token 缓存机制 - 添加完整的单元测试(5个测试用例全部通过)
This commit is contained in:
@@ -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,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,55 @@
|
|||||||
|
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; // 1分钟缓冲
|
||||||
|
|
||||||
|
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 // 假设token有效期1小时
|
||||||
|
});
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async authenticateWithBackend(credentials: { username: string; password: string }): Promise<string> {
|
||||||
|
const response = await fetch(`${this.API_BASE_URL}/api/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(credentials),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Authentication failed for user ${credentials.username}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user