- 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>
547 lines
No EOL
19 KiB
JavaScript
Executable file
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; |