test-api.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
/**
* API Testing Script for Cronjob Manager
*
* This script tests all API endpoints and provides clear reporting on what's working and what's broken.
* Usage: node test-api.mjs [baseUrl]
* Example: node test-api.mjs http://localhost:3000
*
* Running it with params: AUTH_PASSWORD=<password> node test-api.mjs http://localhost:<port>
*/
import https from 'https';
import http from 'http';
class APITester {
constructor(baseUrl = 'http://localhost:3000') {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.sessionCookie = null;
this.testResults = {
passed: 0,
failed: 0,
total: 0,
details: []
};
}
async makeRequest(method, path, options = {}) {
return new Promise((resolve, reject) => {
const url = new URL(path, this.baseUrl);
const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http;
const requestOptions = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: method.toUpperCase(),
headers: {
'Content-Type': 'application/json',
'User-Agent': 'API-Test-Script/1.0',
...options.headers
}
};
if (this.sessionCookie && !options.skipAuth) {
requestOptions.headers.Cookie = this.sessionCookie;
}
const req = client.request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = {
status: res.statusCode,
headers: res.headers,
data: data ? JSON.parse(data) : null,
rawData: data
};
resolve(response);
} catch (error) {
resolve({
status: res.statusCode,
headers: res.headers,
data: null,
rawData: data,
parseError: error.message
});
}
});
});
req.on('error', (error) => {
reject(error);
});
if (options.body) {
req.write(JSON.stringify(options.body));
}
req.end();
});
}
async login(password = process.env.AUTH_PASSWORD || 'admin') {
try {
console.log('\n🔐 Testing login...');
const response = await this.makeRequest('POST', '/api/auth/login', {
body: { password },
skipAuth: true
});
if (response.status === 200 && response.data?.success) {
const setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader) {
const sessionMatch = setCookieHeader.find(cookie => cookie.startsWith('cronmaster-session='));
if (sessionMatch) {
this.sessionCookie = sessionMatch.split(';')[0];
console.log('✅ Login successful, session cookie set');
return true;
}
}
}
console.log('❌ Login failed:', response.data?.message || 'Unknown error');
return false;
} catch (error) {
console.log('❌ Login error:', error.message);
return false;
}
}
recordResult(testName, passed, details = '') {
this.testResults.total++;
if (passed) {
this.testResults.passed++;
console.log(`✅ ${testName}`);
} else {
this.testResults.failed++;
console.log(`❌ ${testName}`);
if (details) console.log(` Details: ${details}`);
}
this.testResults.details.push({ testName, passed, details });
}
async testEndpoint(testName, method, path, options = {}) {
try {
const response = await this.makeRequest(method, path, options);
const expectedStatus = options.expectedStatus || (method === 'GET' ? 200 : 201);
const statusOk = response.status === expectedStatus;
let dataOk = true;
if (options.expectedDataShape) {
dataOk = this.checkDataShape(response.data, options.expectedDataShape);
}
const passed = statusOk && dataOk;
let details = '';
if (!statusOk) {
details += `Expected status ${expectedStatus}, got ${response.status}. `;
}
if (!dataOk) {
details += 'Response data shape mismatch. ';
}
if (response.parseError) {
details += `JSON parse error: ${response.parseError}. `;
}
if (response.data?.error) {
details += `API error: ${response.data.error}. `;
}
this.recordResult(testName, passed, details);
return response;
} catch (error) {
this.recordResult(testName, false, `Request failed: ${error.message}`);
return null;
}
}
async testSSEEndpoint(testName, path, options = {}) {
return new Promise((resolve) => {
const url = new URL(path, this.baseUrl);
const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http;
const requestOptions = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: 'GET',
headers: {
'User-Agent': 'API-Test-Script/1.0',
}
};
if (this.sessionCookie && !options.skipAuth) {
requestOptions.headers.Cookie = this.sessionCookie;
}
const timeout = setTimeout(() => {
req.destroy();
this.recordResult(testName, true, 'SSE connection established (timed out as expected)');
resolve(null);
}, 2000);
const req = client.request(requestOptions, (res) => {
clearTimeout(timeout);
const expectedStatus = options.expectedStatus || 200;
const statusOk = res.statusCode === expectedStatus;
if (statusOk) {
res.destroy();
this.recordResult(testName, true, 'SSE connection established successfully');
} else {
this.recordResult(testName, false, `Expected status ${expectedStatus}, got ${res.statusCode}`);
}
resolve(null);
});
req.on('error', (error) => {
clearTimeout(timeout);
this.recordResult(testName, false, `SSE connection failed: ${error.message}`);
resolve(null);
});
req.end();
});
}
checkDataShape(data, shape) {
if (!data) return false;
for (const [key, type] of Object.entries(shape)) {
if (!(key in data)) return false;
if (type === 'array' && !Array.isArray(data[key])) return false;
if (type === 'object' && (typeof data[key] !== 'object' || Array.isArray(data[key]))) return false;
if (type === 'string' && typeof data[key] !== 'string') return false;
if (type === 'boolean' && typeof data[key] !== 'boolean') return false;
if (type === 'number' && typeof data[key] !== 'number') return false;
}
return true;
}
async runTests() {
console.log(`🚀 Starting API tests for ${this.baseUrl}`);
console.log('=' .repeat(60));
const loginSuccess = await this.login();
if (!loginSuccess) {
console.log('\n❌ Cannot proceed without authentication. Please check AUTH_PASSWORD environment variable.');
return;
}
await this.testEndpoint('GET /api/auth/check-session', 'GET', '/api/auth/check-session', {
expectedDataShape: { valid: 'boolean' }
});
const cronjobsResponse = await this.testEndpoint('GET /api/cronjobs', 'GET', '/api/cronjobs', {
expectedDataShape: { success: 'boolean', data: 'array' }
});
let cronJobId = null;
if (cronjobsResponse?.data?.success && cronjobsResponse.data.data.length > 0) {
cronJobId = cronjobsResponse.data.data[0].id;
await this.testEndpoint(`GET /api/cronjobs/${cronJobId}`, 'GET', `/api/cronjobs/${cronJobId}`, {
expectedDataShape: { success: 'boolean', data: 'object' }
});
await this.testEndpoint(`GET /api/cronjobs/${cronJobId}/execute`, 'GET', `/api/cronjobs/${cronJobId}/execute`, {
expectedDataShape: { success: 'boolean' }
});
await this.testEndpoint(`GET /api/cronjobs/${cronJobId}/execute?runInBackground=false`, 'GET', `/api/cronjobs/${cronJobId}/execute?runInBackground=false`, {
expectedDataShape: { success: 'boolean' }
});
} else {
console.log('ℹ️ No cronjobs found, skipping individual cronjob tests');
}
await this.testEndpoint('GET /api/scripts', 'GET', '/api/scripts', {
expectedDataShape: { success: 'boolean', data: 'array' }
});
await this.testEndpoint('GET /api/system-stats', 'GET', '/api/system-stats', {
expectedDataShape: { uptime: 'string', memory: 'object', cpu: 'object' }
});
await this.testSSEEndpoint('GET /api/events', '/api/events', {
expectedStatus: 200
});
await this.testEndpoint('POST /api/auth/logout', 'POST', '/api/auth/logout', {
expectedStatus: 200,
expectedDataShape: { success: 'boolean' }
});
await this.testEndpoint('GET /api/auth/check-session (after logout)', 'GET', '/api/auth/check-session', {
expectedStatus: 401,
expectedDataShape: { valid: 'boolean' }
});
await this.testEndpoint('GET /api/logs/stream (without runId)', 'GET', '/api/logs/stream', {
expectedStatus: 400
});
console.log('\n' + '='.repeat(60));
console.log('📊 TEST RESULTS SUMMARY');
console.log('='.repeat(60));
console.log(`Total tests: ${this.testResults.total}`);
console.log(`✅ Passed: ${this.testResults.passed}`);
console.log(`❌ Failed: ${this.testResults.failed}`);
console.log(`Success rate: ${((this.testResults.passed / this.testResults.total) * 100).toFixed(1)}%`);
if (this.testResults.failed > 0) {
console.log('\n🔍 FAILED TESTS DETAILS:');
this.testResults.details
.filter(test => !test.passed)
.forEach(test => {
console.log(`❌ ${test.testName}`);
if (test.details) console.log(` ${test.details}`);
});
}
console.log('\n🎯 REMOVED ENDPOINTS VERIFICATION:');
console.log('The following POST endpoints should return 405 Method Not Allowed:');
const removedEndpoints = [
'/api/cronjobs',
'/api/scripts'
];
for (const endpoint of removedEndpoints) {
try {
const response = await this.makeRequest('POST', endpoint, { skipAuth: true });
if (response.status === 405) {
console.log(`✅ ${endpoint} - Correctly returns 405 Method Not Allowed`);
} else {
console.log(`❌ ${endpoint} - Returns ${response.status}, expected 405`);
}
} catch {
console.log(`❓ ${endpoint} - Could not test (connection error)`);
}
}
if (cronJobId) {
try {
const response = await this.makeRequest('POST', `/api/cronjobs/${cronJobId}/execute`, { skipAuth: true });
if (response.status === 405) {
console.log(`✅ /api/cronjobs/${cronJobId}/execute - Correctly returns 405 Method Not Allowed (now GET only)`);
} else {
console.log(`❌ /api/cronjobs/${cronJobId}/execute - Returns ${response.status}, expected 405 for POST`);
}
} catch {
console.log(`❓ /api/cronjobs/${cronJobId}/execute - Could not test (connection error)`);
}
}
process.exit(this.testResults.failed > 0 ? 1 : 0);
}
}
const baseUrl = process.argv[2] || 'http://localhost:3000';
const tester = new APITester(baseUrl);
tester.runTests().catch(error => {
console.error('💥 Test runner failed:', error.message);
process.exit(1);
});