08ea5fbe98
添加用户管理视图、API和状态管理文件
309 lines
8.8 KiB
TypeScript
309 lines
8.8 KiB
TypeScript
import { APIRequestContext, APIResponse } from '@playwright/test';
|
|
import { testLogger } from '../shared/utils/test-logger';
|
|
|
|
export interface APIRequestOptions {
|
|
headers?: Record<string, string>;
|
|
params?: Record<string, string | number>;
|
|
data?: any;
|
|
timeout?: number;
|
|
}
|
|
|
|
export interface APIResponseData<T = any> {
|
|
success: boolean;
|
|
code: string;
|
|
message: string;
|
|
data: T;
|
|
timestamp: number;
|
|
}
|
|
|
|
export class APIHelper {
|
|
private apiContext: APIRequestContext;
|
|
private baseURL: string;
|
|
private token: string | null = null;
|
|
private tokenType: string = 'Bearer';
|
|
|
|
constructor(apiContext: APIRequestContext, baseURL: string) {
|
|
this.apiContext = apiContext;
|
|
this.baseURL = baseURL;
|
|
testLogger.info(`APIHelper initialized with baseURL: ${baseURL}`);
|
|
}
|
|
|
|
setToken(token: string, tokenType: string = 'Bearer'): void {
|
|
this.token = token;
|
|
this.tokenType = tokenType;
|
|
testLogger.info('Token set successfully');
|
|
}
|
|
|
|
clearToken(): void {
|
|
this.token = null;
|
|
testLogger.info('Token cleared');
|
|
}
|
|
|
|
private buildHeaders(options?: APIRequestOptions): Record<string, string> {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...options?.headers
|
|
};
|
|
|
|
if (this.token) {
|
|
headers['Authorization'] = `${this.tokenType} ${this.token}`;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
private buildURL(endpoint: string, params?: Record<string, string | number>): string {
|
|
let url = `${this.baseURL}${endpoint}`;
|
|
|
|
if (params && Object.keys(params).length > 0) {
|
|
const queryString = new URLSearchParams(
|
|
Object.entries(params).reduce((acc, [key, value]) => {
|
|
acc[key] = String(value);
|
|
return acc;
|
|
}, {} as Record<string, string>)
|
|
).toString();
|
|
url += `?${queryString}`;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
private async handleResponse<T>(response: APIResponse, endpoint: string): Promise<APIResponseData<T>> {
|
|
const statusCode = response.status();
|
|
const contentType = response.headers()['content-type'] || '';
|
|
|
|
testLogger.debug(`API Response: ${endpoint} - Status: ${statusCode}, ContentType: ${contentType}`);
|
|
|
|
if (!response.ok()) {
|
|
const errorText = await response.text();
|
|
testLogger.error(`API Error: ${endpoint} - Status: ${statusCode}`, new Error(errorText));
|
|
throw new Error(`API request failed: ${statusCode} - ${errorText}`);
|
|
}
|
|
|
|
let responseData: any;
|
|
|
|
if (contentType.includes('application/json')) {
|
|
responseData = await response.json();
|
|
} else {
|
|
responseData = await response.text();
|
|
}
|
|
|
|
return responseData;
|
|
}
|
|
|
|
async get<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers = this.buildHeaders(options);
|
|
|
|
testLogger.info(`GET ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.get(url, {
|
|
headers,
|
|
timeout: options?.timeout || 30000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`GET ${url} - ${duration}ms`);
|
|
|
|
return this.handleResponse<T>(response, endpoint);
|
|
}
|
|
|
|
async post<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers = this.buildHeaders(options);
|
|
|
|
testLogger.info(`POST ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.post(url, {
|
|
headers,
|
|
data: options?.data,
|
|
timeout: options?.timeout || 30000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`POST ${url} - ${duration}ms`);
|
|
|
|
return this.handleResponse<T>(response, endpoint);
|
|
}
|
|
|
|
async put<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers = this.buildHeaders(options);
|
|
|
|
testLogger.info(`PUT ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.put(url, {
|
|
headers,
|
|
data: options?.data,
|
|
timeout: options?.timeout || 30000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`PUT ${url} - ${duration}ms`);
|
|
|
|
return this.handleResponse<T>(response, endpoint);
|
|
}
|
|
|
|
async delete<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers = this.buildHeaders(options);
|
|
|
|
testLogger.info(`DELETE ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.delete(url, {
|
|
headers,
|
|
timeout: options?.timeout || 30000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`DELETE ${url} - ${duration}ms`);
|
|
|
|
return this.handleResponse<T>(response, endpoint);
|
|
}
|
|
|
|
async patch<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers = this.buildHeaders(options);
|
|
|
|
testLogger.info(`PATCH ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.fetch(url, {
|
|
method: 'PATCH',
|
|
headers,
|
|
data: options?.data,
|
|
timeout: options?.timeout || 30000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`PATCH ${url} - ${duration}ms`);
|
|
|
|
return this.handleResponse<T>(response, endpoint);
|
|
}
|
|
|
|
async upload<T = any>(endpoint: string, formData: any, options?: APIRequestOptions): Promise<APIResponseData<T>> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers: Record<string, string> = {
|
|
...options?.headers
|
|
};
|
|
|
|
if (this.token) {
|
|
headers['Authorization'] = `${this.tokenType} ${this.token}`;
|
|
}
|
|
|
|
testLogger.info(`UPLOAD ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.post(url, {
|
|
headers,
|
|
multipart: formData,
|
|
timeout: options?.timeout || 60000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`UPLOAD ${url} - ${duration}ms`);
|
|
|
|
return this.handleResponse<T>(response, endpoint);
|
|
}
|
|
|
|
async download(endpoint: string, options?: APIRequestOptions): Promise<Buffer> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers = this.buildHeaders(options);
|
|
|
|
testLogger.info(`DOWNLOAD ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.get(url, {
|
|
headers,
|
|
timeout: options?.timeout || 60000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`DOWNLOAD ${url} - ${duration}ms`);
|
|
|
|
if (!response.ok()) {
|
|
throw new Error(`Download failed: ${response.status()}`);
|
|
}
|
|
|
|
return response.body();
|
|
}
|
|
|
|
async request<T = any>(method: string, endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
|
|
const url = this.buildURL(endpoint, options?.params);
|
|
const headers = this.buildHeaders(options);
|
|
|
|
testLogger.info(`${method.toUpperCase()} ${url}`);
|
|
|
|
const startTime = Date.now();
|
|
const response = await this.apiContext.fetch(url, {
|
|
method,
|
|
headers,
|
|
data: options?.data,
|
|
timeout: options?.timeout || 30000
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
testLogger.info(`${method.toUpperCase()} ${url} - ${duration}ms`);
|
|
|
|
return this.handleResponse<T>(response, endpoint);
|
|
}
|
|
|
|
async login(username: string, password: string): Promise<APIResponseData<{ token: string }>> {
|
|
testLogger.info(`Login attempt for user: ${username}`);
|
|
|
|
const response = await this.post('/auth/login', {
|
|
data: { username, password }
|
|
});
|
|
|
|
if (response.success && response.data.token) {
|
|
this.setToken(response.data.token);
|
|
testLogger.info('Login successful');
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async logout(): Promise<APIResponseData<any>> {
|
|
testLogger.info('Logout');
|
|
|
|
const response = await this.post('/auth/logout');
|
|
this.clearToken();
|
|
|
|
testLogger.info('Logout successful');
|
|
return response;
|
|
}
|
|
|
|
async refresh(): Promise<APIResponseData<{ token: string }>> {
|
|
testLogger.info('Token refresh');
|
|
|
|
if (!this.token) {
|
|
throw new Error('No token to refresh');
|
|
}
|
|
|
|
const response = await this.post('/auth/refresh');
|
|
|
|
if (response.success && response.data.token) {
|
|
this.setToken(response.data.token);
|
|
testLogger.info('Token refreshed successfully');
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
verifyToken(): boolean {
|
|
return this.token !== null && this.token.length > 0;
|
|
}
|
|
|
|
getToken(): string | null {
|
|
return this.token;
|
|
}
|
|
|
|
getBaseURL(): string {
|
|
return this.baseURL;
|
|
}
|
|
}
|