92df794cc8
问题: - H2数据库与PostgreSQL存在兼容性差异 - Flyway脚本在H2和PostgreSQL上的执行结果不一致 - 密码哈希验证失败 修复: - 修改后端服务配置:test -> dev - 修改网关服务配置:test -> dev - 使用PostgreSQL数据库(localhost:55432) 优势: - 更接近生产环境 - 避免H2兼容性问题 - 数据库行为一致 - Flyway脚本执行可靠
609 lines
18 KiB
TypeScript
609 lines
18 KiB
TypeScript
import { FullConfig } from '@playwright/test';
|
|
import { spawn, ChildProcess } from 'child_process';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { existsSync } from 'fs';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
let backendProcess: ChildProcess | null = null;
|
|
let gatewayProcess: ChildProcess | null = null;
|
|
let frontendProcess: ChildProcess | null = null;
|
|
let healthCheckInterval: NodeJS.Timeout | null = null;
|
|
|
|
async function checkBackendHealth(): Promise<boolean> {
|
|
try {
|
|
const response = await fetch('http://localhost:8084/actuator/health', {
|
|
signal: AbortSignal.timeout(5000)
|
|
} as any);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.status === 'UP';
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function checkGatewayHealth(): Promise<boolean> {
|
|
try {
|
|
const response = await fetch('http://localhost:8080/actuator/health', {
|
|
signal: AbortSignal.timeout(5000)
|
|
} as any);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.status === 'UP';
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function checkFrontendHealth(): Promise<boolean> {
|
|
try {
|
|
const response = await fetch('http://localhost:3002', {
|
|
signal: AbortSignal.timeout(5000)
|
|
} as any);
|
|
return response.ok;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function startHealthMonitoring() {
|
|
if (healthCheckInterval) {
|
|
clearInterval(healthCheckInterval);
|
|
}
|
|
|
|
healthCheckInterval = setInterval(async () => {
|
|
const backendHealthy = await checkBackendHealth();
|
|
const gatewayHealthy = await checkGatewayHealth();
|
|
const frontendHealthy = await checkFrontendHealth();
|
|
|
|
if (!backendHealthy) {
|
|
console.error('⚠️ 后端服务健康检查失败!');
|
|
}
|
|
if (!gatewayHealthy) {
|
|
console.error('⚠️ 网关服务健康检查失败!');
|
|
}
|
|
if (!frontendHealthy) {
|
|
console.error('⚠️ 前端服务健康检查失败!');
|
|
}
|
|
}, 30000);
|
|
}
|
|
|
|
function stopHealthMonitoring() {
|
|
if (healthCheckInterval) {
|
|
clearInterval(healthCheckInterval);
|
|
healthCheckInterval = null;
|
|
}
|
|
}
|
|
|
|
async function globalSetup(config: FullConfig) {
|
|
console.log('🚀 开始全局测试环境设置...');
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.PLAYWRIGHT_HEADLESS = 'false';
|
|
|
|
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
|
|
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
|
|
|
|
let backendCommand: string;
|
|
let backendArgs: string[];
|
|
|
|
if (existsSync(jarFile)) {
|
|
console.log('📦 使用JAR文件启动后端服务...');
|
|
console.log(` JAR文件: ${jarFile}`);
|
|
backendCommand = 'java';
|
|
backendArgs = [
|
|
'-jar',
|
|
jarFile,
|
|
'--spring.profiles.active=dev',
|
|
'-Xms256m',
|
|
'-Xmx512m'
|
|
];
|
|
} else {
|
|
console.log('📦 使用Maven启动后端服务...');
|
|
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
|
|
backendCommand = 'mvn';
|
|
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
|
|
}
|
|
|
|
console.log(` 目录: ${backendDir}`);
|
|
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
|
|
|
|
backendProcess = spawn(backendCommand, backendArgs, {
|
|
cwd: backendDir,
|
|
stdio: 'pipe',
|
|
shell: true,
|
|
detached: false,
|
|
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
|
|
});
|
|
|
|
if (backendProcess.stdout) {
|
|
backendProcess.stdout.on('data', (data) => {
|
|
const output = data.toString();
|
|
if (output.includes('Started ManageApplication') || output.includes('Tomcat started on port')) {
|
|
console.log('✅ 后端服务启动成功');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (backendProcess.stderr) {
|
|
backendProcess.stderr.on('data', (data) => {
|
|
const output = data.toString();
|
|
if (output.includes('ERROR') || output.includes('Exception')) {
|
|
console.error('❌ 后端服务启动错误:', output);
|
|
}
|
|
});
|
|
}
|
|
|
|
backendProcess.on('error', (error) => {
|
|
console.error('❌ 后端服务启动失败:', error);
|
|
});
|
|
|
|
backendProcess.on('exit', (code, signal) => {
|
|
if (code !== 0 && code !== null) {
|
|
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
|
|
}
|
|
});
|
|
|
|
console.log('⏳ 等待后端服务就绪...');
|
|
await waitForBackendReady();
|
|
|
|
const gatewayDir = path.resolve(__dirname, '../../novalon-manage-api/manage-gateway');
|
|
const gatewayJarFile = path.join(gatewayDir, 'target/manage-gateway-1.0.0.jar');
|
|
|
|
let gatewayCommand: string;
|
|
let gatewayArgs: string[];
|
|
|
|
if (existsSync(gatewayJarFile)) {
|
|
console.log('🚪 使用JAR文件启动网关服务...');
|
|
console.log(` JAR文件: ${gatewayJarFile}`);
|
|
gatewayCommand = 'java';
|
|
gatewayArgs = [
|
|
'-jar',
|
|
gatewayJarFile,
|
|
'--spring.profiles.active=dev',
|
|
'-Xms128m',
|
|
'-Xmx256m'
|
|
];
|
|
} else {
|
|
console.log('🚪 使用Maven启动网关服务...');
|
|
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
|
|
gatewayCommand = 'mvn';
|
|
gatewayArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
|
|
}
|
|
|
|
console.log(` 目录: ${gatewayDir}`);
|
|
console.log(` 命令: ${gatewayCommand} ${gatewayArgs.join(' ')}`);
|
|
|
|
gatewayProcess = spawn(gatewayCommand, gatewayArgs, {
|
|
cwd: gatewayDir,
|
|
stdio: 'pipe',
|
|
shell: true,
|
|
detached: false,
|
|
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'dev' }
|
|
});
|
|
|
|
if (gatewayProcess.stdout) {
|
|
gatewayProcess.stdout.on('data', (data) => {
|
|
const output = data.toString();
|
|
if (output.includes('Started GatewayApplication') || output.includes('Netty started on port')) {
|
|
console.log('✅ 网关服务启动成功');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (gatewayProcess.stderr) {
|
|
gatewayProcess.stderr.on('data', (data) => {
|
|
const output = data.toString();
|
|
if (output.includes('ERROR') || output.includes('Exception')) {
|
|
console.error('❌ 网关服务启动错误:', output);
|
|
}
|
|
});
|
|
}
|
|
|
|
gatewayProcess.on('error', (error) => {
|
|
console.error('❌ 网关服务启动失败:', error);
|
|
});
|
|
|
|
gatewayProcess.on('exit', (code, signal) => {
|
|
if (code !== 0 && code !== null) {
|
|
console.error(`❌ 网关服务异常退出,退出码: ${code}, 信号: ${signal}`);
|
|
}
|
|
});
|
|
|
|
console.log('⏳ 等待网关服务就绪...');
|
|
await waitForGatewayReady();
|
|
|
|
const frontendDir = path.resolve(__dirname, '..');
|
|
console.log('🌐 启动前端服务...');
|
|
console.log(` 目录: ${frontendDir}`);
|
|
|
|
frontendProcess = spawn('pnpm', ['run', 'dev'], {
|
|
cwd: frontendDir,
|
|
stdio: 'pipe',
|
|
shell: true,
|
|
detached: false,
|
|
env: { ...process.env, NODE_ENV: 'test' }
|
|
});
|
|
|
|
if (frontendProcess.stdout) {
|
|
frontendProcess.stdout.on('data', (data) => {
|
|
const output = data.toString();
|
|
if (output.includes('Local:') || output.includes('localhost:3002')) {
|
|
console.log('✅ 前端服务启动成功');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (frontendProcess.stderr) {
|
|
frontendProcess.stderr.on('data', (data) => {
|
|
const output = data.toString();
|
|
if (output.includes('ERROR') || output.includes('error')) {
|
|
console.error('❌ 前端服务启动错误:', output);
|
|
}
|
|
});
|
|
}
|
|
|
|
frontendProcess.on('error', (error) => {
|
|
console.error('❌ 前端服务启动失败:', error);
|
|
});
|
|
|
|
frontendProcess.on('exit', (code, signal) => {
|
|
if (code !== 0 && code !== null) {
|
|
console.error(`❌ 前端服务异常退出,退出码: ${code}, 信号: ${signal}`);
|
|
}
|
|
});
|
|
|
|
console.log('⏳ 等待前端服务就绪...');
|
|
await waitForFrontendReady();
|
|
|
|
console.log('🔍 验证所有服务连通性...');
|
|
await verifyAllServices();
|
|
|
|
console.log('🧹 清理测试数据...');
|
|
await cleanupTestData();
|
|
|
|
startHealthMonitoring();
|
|
|
|
console.log('✅ 全局测试环境设置完成');
|
|
}
|
|
|
|
async function verifyAllServices(): Promise<void> {
|
|
console.log(' 验证后端服务...');
|
|
const backendOk = await checkBackendHealth();
|
|
if (!backendOk) {
|
|
throw new Error('❌ 后端服务验证失败');
|
|
}
|
|
console.log(' ✅ 后端服务正常');
|
|
|
|
console.log(' 验证网关服务...');
|
|
const gatewayOk = await checkGatewayHealth();
|
|
if (!gatewayOk) {
|
|
throw new Error('❌ 网关服务验证失败');
|
|
}
|
|
console.log(' ✅ 网关服务正常');
|
|
|
|
console.log(' 验证前端服务...');
|
|
const frontendOk = await checkFrontendHealth();
|
|
if (!frontendOk) {
|
|
throw new Error('❌ 前端服务验证失败');
|
|
}
|
|
console.log(' ✅ 前端服务正常');
|
|
|
|
console.log(' 验证网关到后端的连通性...');
|
|
try {
|
|
const response = await fetch('http://localhost:8080/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin', password: 'admin123' }),
|
|
signal: AbortSignal.timeout(10000) as any
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`网关到后端连通性验证失败,状态码: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (!data.token) {
|
|
throw new Error('网关到后端连通性验证失败,未返回token');
|
|
}
|
|
|
|
console.log(' ✅ 网关到后端连通性正常');
|
|
} catch (error) {
|
|
throw new Error(`❌ 网关到后端连通性验证失败: ${error}`);
|
|
}
|
|
|
|
console.log('✅ 所有服务验证通过');
|
|
}
|
|
|
|
async function waitForBackendReady(): Promise<void> {
|
|
const maxRetries = 90;
|
|
const retryInterval = 1000;
|
|
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
const response = await fetch('http://localhost:8084/actuator/health', {
|
|
signal: AbortSignal.timeout(5000) as any
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.status === 'UP') {
|
|
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
|
|
|
// 验证服务连通性:测试登录API
|
|
try {
|
|
const loginTest = await fetch('http://localhost:8084/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin', password: 'admin123' }),
|
|
signal: AbortSignal.timeout(10000) as any
|
|
});
|
|
|
|
if (loginTest.ok) {
|
|
console.log('✅ 后端服务连通性验证通过(登录API可用)');
|
|
return;
|
|
} else {
|
|
console.log(`⚠️ 后端服务连通性验证失败,状态码: ${loginTest.status}`);
|
|
}
|
|
} catch (error) {
|
|
console.log('⚠️ 后端服务连通性验证失败,继续等待...');
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// 服务还未就绪,继续等待
|
|
}
|
|
|
|
if (i < maxRetries - 1) {
|
|
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
|
}
|
|
}
|
|
|
|
throw new Error('❌ 后端服务启动超时');
|
|
}
|
|
|
|
async function waitForGatewayReady(): Promise<void> {
|
|
const maxRetries = 90;
|
|
const retryInterval = 1000;
|
|
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
const response = await fetch('http://localhost:8080/actuator/health', {
|
|
signal: AbortSignal.timeout(5000) as any
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.status === 'UP') {
|
|
console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
|
|
|
// 验证网关连通性:通过网关测试登录API
|
|
try {
|
|
const loginTest = await fetch('http://localhost:8080/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin', password: 'admin123' }),
|
|
signal: AbortSignal.timeout(10000) as any
|
|
});
|
|
|
|
if (loginTest.ok) {
|
|
console.log('✅ 网关服务连通性验证通过(登录API可用)');
|
|
return;
|
|
} else {
|
|
console.log(`⚠️ 网关服务连通性验证失败,状态码: ${loginTest.status}`);
|
|
}
|
|
} catch (error) {
|
|
console.log('⚠️ 网关服务连通性验证失败,继续等待...');
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// 服务还未就绪,继续等待
|
|
}
|
|
|
|
if (i < maxRetries - 1) {
|
|
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
|
}
|
|
}
|
|
|
|
throw new Error('❌ 网关服务启动超时');
|
|
}
|
|
|
|
async function waitForFrontendReady(): Promise<void> {
|
|
const maxRetries = 90;
|
|
const retryInterval = 1000;
|
|
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
const response = await fetch('http://localhost:3002', {
|
|
signal: AbortSignal.timeout(5000) as any
|
|
});
|
|
if (response.ok) {
|
|
console.log(`✅ 前端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
// 服务还未就绪,继续等待
|
|
}
|
|
|
|
if (i < maxRetries - 1) {
|
|
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
|
}
|
|
}
|
|
|
|
throw new Error('❌ 前端服务启动超时');
|
|
}
|
|
|
|
async function cleanupTestData(): Promise<void> {
|
|
try {
|
|
// 登录获取token(通过网关)
|
|
const loginResponse = await fetch('http://localhost:8080/api/auth/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
username: 'admin',
|
|
password: 'admin123'
|
|
})
|
|
});
|
|
|
|
if (!loginResponse.ok) {
|
|
console.log('⚠️ 无法登录,跳过数据清理');
|
|
return;
|
|
}
|
|
|
|
const loginData = await loginResponse.json();
|
|
const token = loginData.token;
|
|
|
|
// 获取所有用户
|
|
const usersResponse = await fetch('http://localhost:8080/api/users', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (usersResponse.ok) {
|
|
const users = await usersResponse.json();
|
|
|
|
// 删除测试创建的用户(保留ID 1-10的初始用户)
|
|
for (const user of users) {
|
|
if (user.id > 10) {
|
|
try {
|
|
await fetch(`http://localhost:8080/api/users/${user.id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
console.log(` 删除用户: ${user.username}`);
|
|
} catch (error) {
|
|
console.log(` ⚠️ 无法删除用户 ${user.username}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 获取所有角色
|
|
const rolesResponse = await fetch('http://localhost:8080/api/roles', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (rolesResponse.ok) {
|
|
const roles = await rolesResponse.json();
|
|
|
|
// 删除测试创建的角色(保留ID 1-4的初始角色)
|
|
for (const role of roles) {
|
|
if (role.id > 4) {
|
|
try {
|
|
await fetch(`http://localhost:8080/api/roles/${role.id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
console.log(` 删除角色: ${role.roleName}`);
|
|
} catch (error) {
|
|
console.log(` ⚠️ 无法删除角色 ${role.roleName}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('✅ 测试数据清理完成');
|
|
} catch (error) {
|
|
console.log('⚠️ 数据清理失败,继续执行测试');
|
|
console.error('清理错误:', error);
|
|
}
|
|
}
|
|
|
|
async function globalTeardown() {
|
|
console.log('🧹 开始全局测试环境清理...');
|
|
|
|
stopHealthMonitoring();
|
|
|
|
if (backendProcess) {
|
|
console.log('🛑 停止后端服务...');
|
|
backendProcess.kill('SIGTERM');
|
|
|
|
await new Promise<void>((resolve) => {
|
|
if (backendProcess) {
|
|
backendProcess.on('exit', () => {
|
|
console.log('✅ 后端服务已停止');
|
|
resolve();
|
|
});
|
|
|
|
setTimeout(() => {
|
|
if (backendProcess) {
|
|
backendProcess.kill('SIGKILL');
|
|
console.log('⚠️ 强制停止后端服务');
|
|
resolve();
|
|
}
|
|
}, 10000);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (gatewayProcess) {
|
|
console.log('🛑 停止网关服务...');
|
|
gatewayProcess.kill('SIGTERM');
|
|
|
|
await new Promise<void>((resolve) => {
|
|
if (gatewayProcess) {
|
|
gatewayProcess.on('exit', () => {
|
|
console.log('✅ 网关服务已停止');
|
|
resolve();
|
|
});
|
|
|
|
setTimeout(() => {
|
|
if (gatewayProcess) {
|
|
gatewayProcess.kill('SIGKILL');
|
|
console.log('⚠️ 强制停止网关服务');
|
|
resolve();
|
|
}
|
|
}, 10000);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (frontendProcess) {
|
|
console.log('🛑 停止前端服务...');
|
|
frontendProcess.kill('SIGTERM');
|
|
|
|
await new Promise<void>((resolve) => {
|
|
if (frontendProcess) {
|
|
frontendProcess.on('exit', () => {
|
|
console.log('✅ 前端服务已停止');
|
|
resolve();
|
|
});
|
|
|
|
setTimeout(() => {
|
|
if (frontendProcess) {
|
|
frontendProcess.kill('SIGKILL');
|
|
console.log('⚠️ 强制停止前端服务');
|
|
resolve();
|
|
}
|
|
}, 10000);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('✅ 全局测试环境清理完成');
|
|
}
|
|
|
|
export default globalSetup;
|
|
export { globalTeardown };
|