upskill-event-manager/tests/test-suite-runner.js
ben 90193ea18c security: implement Phase 1 critical vulnerability fixes
- Add XSS protection with DOMPurify sanitization in rich text editor
- Implement comprehensive file upload security validation
- Enhance server-side content sanitization with wp_kses
- Add comprehensive security test suite with 194+ test cases
- Create security remediation plan documentation

Security fixes address:
- CRITICAL: XSS vulnerability in event description editor
- HIGH: File upload security bypass for malicious files
- HIGH: Enhanced CSRF protection verification
- MEDIUM: Input validation and error handling improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 18:53:23 -03:00

547 lines
No EOL
19 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* HVAC Event Creation Test Suite Runner
*
* Comprehensive test suite runner for all UI/UX enhanced functionality.
* Runs tests in optimal order with proper setup and teardown.
*
* Usage:
* node tests/test-suite-runner.js
* node tests/test-suite-runner.js --suite=security
* node tests/test-suite-runner.js --parallel=false
* node tests/test-suite-runner.js --browser=chromium
*/
const { execSync, spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
// Test suites in execution order
const TEST_SUITES = {
security: {
file: 'test-event-creation-security.js',
description: 'Security vulnerability tests (XSS, CSRF, file upload)',
priority: 1,
timeout: 60000,
retries: 2
},
'rich-text-editor': {
file: 'test-rich-text-editor.js',
description: 'Rich text editor functionality and validation',
priority: 2,
timeout: 45000,
retries: 1
},
'featured-image-upload': {
file: 'test-featured-image-upload.js',
description: 'Featured image upload with drag-and-drop',
priority: 2,
timeout: 60000,
retries: 2
},
'searchable-selectors': {
file: 'test-searchable-selectors.js',
description: 'Multi-select and searchable selector components',
priority: 2,
timeout: 45000,
retries: 1
},
'modal-forms': {
file: 'test-modal-forms.js',
description: 'Modal form creation and validation',
priority: 3,
timeout: 45000,
retries: 1
},
'toggle-controls': {
file: 'test-toggle-controls.js',
description: 'Toggle switch controls and state management',
priority: 3,
timeout: 30000,
retries: 1
},
'integration': {
file: 'test-integration-comprehensive.js',
description: 'End-to-end integration tests',
priority: 4,
timeout: 120000,
retries: 2
}
};
// Configuration
const CONFIG = {
browser: process.env.BROWSER || 'chromium',
headless: process.env.HEADLESS !== 'false',
baseUrl: process.env.BASE_URL || 'http://localhost:8080',
parallel: process.argv.includes('--parallel=false') ? false : true,
maxWorkers: process.env.MAX_WORKERS || '4',
timeout: 120000,
retries: 2
};
// Parse command line arguments
const args = process.argv.slice(2);
const suiteFilter = args.find(arg => arg.startsWith('--suite='))?.split('=')[1];
const browserArg = args.find(arg => arg.startsWith('--browser='))?.split('=')[1];
if (browserArg) CONFIG.browser = browserArg;
class TestRunner {
constructor() {
this.results = {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
duration: 0,
suiteResults: {}
};
}
async run() {
console.log('🚀 HVAC Event Creation Test Suite Runner');
console.log('==========================================');
console.log(`Browser: ${CONFIG.browser}`);
console.log(`Headless: ${CONFIG.headless}`);
console.log(`Base URL: ${CONFIG.baseUrl}`);
console.log(`Parallel: ${CONFIG.parallel}`);
console.log('');
// Validate environment
await this.validateEnvironment();
// Setup test environment
await this.setupTestEnvironment();
// Get test suites to run
const suitesToRun = this.getSuitesToRun();
console.log(`📋 Running ${suitesToRun.length} test suites:`);
suitesToRun.forEach(suite => {
console.log(` ${suite.name}: ${suite.config.description}`);
});
console.log('');
const startTime = Date.now();
try {
if (CONFIG.parallel) {
await this.runSuitesParallel(suitesToRun);
} else {
await this.runSuitesSequential(suitesToRun);
}
} catch (error) {
console.error('❌ Test suite runner failed:', error.message);
process.exit(1);
}
this.results.duration = Date.now() - startTime;
// Generate report
await this.generateReport();
// Cleanup
await this.cleanup();
// Exit with appropriate code
process.exit(this.results.failed > 0 ? 1 : 0);
}
async validateEnvironment() {
console.log('🔍 Validating test environment...');
// Check if Playwright is installed
try {
execSync('npx playwright --version', { stdio: 'pipe' });
} catch (error) {
console.error('❌ Playwright not found. Please install with: npm install @playwright/test');
process.exit(1);
}
// Check if test server is running
try {
const response = await fetch(CONFIG.baseUrl);
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
} catch (error) {
console.error(`❌ Test server not accessible at ${CONFIG.baseUrl}`);
console.log('💡 Start the test server with: docker compose -f tests/docker-compose.test.yml up -d');
process.exit(1);
}
// Check test files exist
const missingFiles = [];
Object.values(TEST_SUITES).forEach(suite => {
const filePath = path.join(__dirname, suite.file);
if (!fs.existsSync(filePath)) {
missingFiles.push(suite.file);
}
});
if (missingFiles.length > 0) {
console.error('❌ Missing test files:', missingFiles.join(', '));
process.exit(1);
}
console.log('✅ Environment validation passed');
}
async setupTestEnvironment() {
console.log('🛠️ Setting up test environment...');
// Create test fixtures directory
const fixturesDir = path.join(__dirname, 'fixtures', 'images');
if (!fs.existsSync(fixturesDir)) {
fs.mkdirSync(fixturesDir, { recursive: true });
}
// Create minimal test images
await this.createTestFixtures(fixturesDir);
// Verify test database state
await this.verifyTestDatabase();
console.log('✅ Test environment setup complete');
}
async createTestFixtures(dir) {
// Create minimal valid image files for testing
const validJpeg = Buffer.from([
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46,
0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00,
// ... minimal JPEG data
0xFF, 0xD9
]);
const testImages = {
'hvac-training.jpg': validJpeg,
'manual-j-training.jpg': validJpeg,
'valid-image.jpg': validJpeg
};
Object.entries(testImages).forEach(([filename, data]) => {
const filePath = path.join(dir, filename);
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, data);
}
});
}
async verifyTestDatabase() {
// Check if test data is available
try {
const response = await fetch(`${CONFIG.baseUrl}/wp-json/hvac/v1/test-data-status`);
if (response.ok) {
const data = await response.json();
if (!data.hasTestData) {
console.warn('⚠️ Test database may need seeding. Run: bin/seed-comprehensive-events.sh');
}
}
} catch (error) {
console.warn('⚠️ Could not verify test database status');
}
}
getSuitesToRun() {
const suites = [];
if (suiteFilter) {
if (TEST_SUITES[suiteFilter]) {
suites.push({ name: suiteFilter, config: TEST_SUITES[suiteFilter] });
} else {
console.error(`❌ Unknown test suite: ${suiteFilter}`);
console.log('Available suites:', Object.keys(TEST_SUITES).join(', '));
process.exit(1);
}
} else {
// Run all suites in priority order
Object.entries(TEST_SUITES)
.sort(([,a], [,b]) => a.priority - b.priority)
.forEach(([name, config]) => {
suites.push({ name, config });
});
}
return suites;
}
async runSuitesParallel(suites) {
console.log('⚡ Running test suites in parallel...\n');
const promises = suites.map(suite => this.runSuite(suite));
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
const suiteName = suites[index].name;
if (result.status === 'fulfilled') {
this.results.suiteResults[suiteName] = result.value;
} else {
this.results.suiteResults[suiteName] = {
passed: 0,
failed: 1,
error: result.reason.message
};
}
});
}
async runSuitesSequential(suites) {
console.log('🔄 Running test suites sequentially...\n');
for (const suite of suites) {
try {
const result = await this.runSuite(suite);
this.results.suiteResults[suite.name] = result;
} catch (error) {
this.results.suiteResults[suite.name] = {
passed: 0,
failed: 1,
error: error.message
};
}
}
}
async runSuite(suite) {
console.log(`🧪 Running ${suite.name}...`);
const playwrightArgs = [
'test',
path.join(__dirname, suite.config.file),
`--project=${CONFIG.browser}`,
`--timeout=${suite.config.timeout || CONFIG.timeout}`,
`--retries=${suite.config.retries || CONFIG.retries}`,
'--reporter=json'
];
if (CONFIG.headless) {
playwrightArgs.push('--headed=false');
}
return new Promise((resolve, reject) => {
const process = spawn('npx', ['playwright', ...playwrightArgs], {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
BASE_URL: CONFIG.baseUrl
}
});
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => {
stdout += data.toString();
});
process.stderr.on('data', (data) => {
stderr += data.toString();
});
process.on('close', (code) => {
try {
// Parse Playwright JSON output
const jsonOutput = stdout.split('\n')
.find(line => line.trim().startsWith('{'))
?.trim();
if (jsonOutput) {
const results = JSON.parse(jsonOutput);
const suiteResult = {
passed: results.stats?.passed || 0,
failed: results.stats?.failed || 0,
skipped: results.stats?.skipped || 0,
duration: results.stats?.duration || 0,
details: results.suites || []
};
if (code === 0) {
console.log(`${suite.name}: ${suiteResult.passed} passed, ${suiteResult.failed} failed`);
} else {
console.log(`${suite.name}: ${suiteResult.passed} passed, ${suiteResult.failed} failed`);
}
resolve(suiteResult);
} else {
// Fallback parsing
const passed = (stdout.match(/\d+ passed/g) || ['0'])[0].split(' ')[0];
const failed = (stdout.match(/\d+ failed/g) || ['0'])[0].split(' ')[0];
const result = {
passed: parseInt(passed),
failed: parseInt(failed),
error: code !== 0 ? stderr : null
};
if (code === 0) {
console.log(`${suite.name}: ${result.passed} passed`);
} else {
console.log(`${suite.name}: ${result.passed} passed, ${result.failed} failed`);
}
resolve(result);
}
} catch (parseError) {
reject(new Error(`Failed to parse test results: ${parseError.message}`));
}
});
// Kill test after maximum timeout
setTimeout(() => {
process.kill();
reject(new Error(`Test suite ${suite.name} timed out`));
}, suite.config.timeout + 30000);
});
}
async generateReport() {
console.log('\n📊 Test Results Summary');
console.log('========================');
// Aggregate results
Object.values(this.results.suiteResults).forEach(result => {
this.results.total += (result.passed || 0) + (result.failed || 0) + (result.skipped || 0);
this.results.passed += result.passed || 0;
this.results.failed += result.failed || 0;
this.results.skipped += result.skipped || 0;
});
// Suite-by-suite results
Object.entries(this.results.suiteResults).forEach(([suiteName, result]) => {
const status = (result.failed || 0) > 0 ? '❌' : '✅';
console.log(`${status} ${suiteName}: ${result.passed || 0} passed, ${result.failed || 0} failed`);
if (result.error) {
console.log(` Error: ${result.error}`);
}
});
console.log('');
console.log(`Total Tests: ${this.results.total}`);
console.log(`Passed: ${this.results.passed}`);
console.log(`Failed: ${this.results.failed} ${this.results.failed > 0 ? '❌' : ''}`);
console.log(`Skipped: ${this.results.skipped}`);
console.log(`Duration: ${(this.results.duration / 1000).toFixed(2)}s`);
const successRate = this.results.total > 0
? ((this.results.passed / this.results.total) * 100).toFixed(1)
: '0.0';
console.log(`Success Rate: ${successRate}%`);
// Generate HTML report
await this.generateHtmlReport();
console.log('\n📄 Detailed HTML report generated: tests/reports/test-results.html');
}
async generateHtmlReport() {
const reportsDir = path.join(__dirname, 'reports');
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
const htmlContent = this.generateHtmlContent();
fs.writeFileSync(path.join(reportsDir, 'test-results.html'), htmlContent);
}
generateHtmlContent() {
const timestamp = new Date().toISOString();
const successRate = this.results.total > 0
? ((this.results.passed / this.results.total) * 100).toFixed(1)
: '0.0';
return `<!DOCTYPE html>
<html>
<head>
<title>HVAC Event Creation Test Results</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.header { text-align: center; margin-bottom: 30px; }
.summary { display: flex; justify-content: space-around; margin: 30px 0; }
.metric { text-align: center; padding: 20px; border-radius: 8px; background: #f8f9fa; }
.metric.passed { background: #d4edda; }
.metric.failed { background: #f8d7da; }
.suite { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.suite.passed { border-color: #28a745; background: #f8fff8; }
.suite.failed { border-color: #dc3545; background: #fff8f8; }
.suite h3 { margin: 0 0 10px 0; }
.error { background: #f8f8f8; padding: 10px; border-radius: 3px; font-family: monospace; margin-top: 10px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 HVAC Event Creation Test Results</h1>
<p>Generated: ${timestamp}</p>
<p>Environment: ${CONFIG.baseUrl} (${CONFIG.browser})</p>
</div>
<div class="summary">
<div class="metric">
<h3>${this.results.total}</h3>
<p>Total Tests</p>
</div>
<div class="metric passed">
<h3>${this.results.passed}</h3>
<p>Passed</p>
</div>
<div class="metric failed">
<h3>${this.results.failed}</h3>
<p>Failed</p>
</div>
<div class="metric">
<h3>${successRate}%</h3>
<p>Success Rate</p>
</div>
</div>
<h2>Test Suites</h2>
${Object.entries(this.results.suiteResults).map(([suiteName, result]) => `
<div class="suite ${(result.failed || 0) > 0 ? 'failed' : 'passed'}">
<h3>${(result.failed || 0) > 0 ? '❌' : '✅'} ${suiteName}</h3>
<p><strong>Description:</strong> ${TEST_SUITES[suiteName]?.description || 'Test suite'}</p>
<p>
<strong>Results:</strong>
${result.passed || 0} passed,
${result.failed || 0} failed,
${result.skipped || 0} skipped
</p>
${result.duration ? `<p><strong>Duration:</strong> ${(result.duration / 1000).toFixed(2)}s</p>` : ''}
${result.error ? `<div class="error"><strong>Error:</strong> ${result.error}</div>` : ''}
</div>
`).join('')}
<div style="margin-top: 40px; text-align: center; color: #666;">
<p>HVAC Community Events Plugin - UI/UX Enhancement Test Suite</p>
</div>
</div>
</body>
</html>`;
}
async cleanup() {
console.log('🧹 Cleaning up test environment...');
// Clean up temporary test files if needed
const tempDir = path.join(__dirname, 'temp');
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
console.log('✅ Cleanup complete');
}
}
// Run the test suite
if (require.main === module) {
const runner = new TestRunner();
runner.run().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});
}
module.exports = TestRunner;