|
| 1 | +// K6 Load Testing Configuration for Media Hardening Service |
| 2 | +// Tests throughput, latency, and resource usage under load |
| 3 | + |
| 4 | +import http from 'k6/http'; |
| 5 | +import { check, sleep } from 'k6'; |
| 6 | +import { Rate, Trend, Counter } from 'k6/metrics'; |
| 7 | + |
| 8 | +// Custom metrics |
| 9 | +const errorRate = new Rate('errors'); |
| 10 | +const processingDuration = new Trend('processing_duration'); |
| 11 | +const filesProcessed = new Counter('files_processed'); |
| 12 | +const securityViolations = new Counter('security_violations'); |
| 13 | + |
| 14 | +// Load test configuration |
| 15 | +export const options = { |
| 16 | + // Scenario 1: Smoke test (quick validation) |
| 17 | + stages: [ |
| 18 | + { duration: '30s', target: 5 }, // Ramp up to 5 users |
| 19 | + { duration: '1m', target: 5 }, // Stay at 5 users |
| 20 | + { duration: '30s', target: 0 }, // Ramp down |
| 21 | + ], |
| 22 | + |
| 23 | + // Thresholds (SLOs) |
| 24 | + thresholds: { |
| 25 | + 'http_req_duration': ['p(95)<5000'], // 95% of requests should complete within 5s |
| 26 | + 'http_req_failed': ['rate<0.1'], // Error rate should be less than 10% |
| 27 | + 'errors': ['rate<0.05'], // Custom error rate threshold |
| 28 | + 'processing_duration': ['p(99)<10000'], // 99th percentile processing time |
| 29 | + }, |
| 30 | + |
| 31 | + // Alternative scenarios (comment/uncomment as needed) |
| 32 | + |
| 33 | + // Scenario 2: Load test (sustained load) |
| 34 | + // stages: [ |
| 35 | + // { duration: '2m', target: 50 }, // Ramp up to 50 users |
| 36 | + // { duration: '5m', target: 50 }, // Stay at 50 users |
| 37 | + // { duration: '2m', target: 0 }, // Ramp down |
| 38 | + // ], |
| 39 | + |
| 40 | + // Scenario 3: Stress test (find breaking point) |
| 41 | + // stages: [ |
| 42 | + // { duration: '2m', target: 100 }, // Ramp up to 100 users |
| 43 | + // { duration: '5m', target: 100 }, // Stay at 100 |
| 44 | + // { duration: '2m', target: 200 }, // Ramp up to 200 |
| 45 | + // { duration: '5m', target: 200 }, // Stay at 200 |
| 46 | + // { duration: '2m', target: 0 }, // Ramp down |
| 47 | + // ], |
| 48 | + |
| 49 | + // Scenario 4: Spike test (sudden traffic spike) |
| 50 | + // stages: [ |
| 51 | + // { duration: '1m', target: 10 }, // Normal load |
| 52 | + // { duration: '30s', target: 200 }, // Sudden spike |
| 53 | + // { duration: '1m', target: 200 }, // Sustain spike |
| 54 | + // { duration: '30s', target: 10 }, // Return to normal |
| 55 | + // { duration: '1m', target: 0 }, // Ramp down |
| 56 | + // ], |
| 57 | +}; |
| 58 | + |
| 59 | +// Configuration |
| 60 | +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; |
| 61 | +const MEDIA_PROCESSOR_URL = __ENV.MEDIA_PROCESSOR_URL || 'http://localhost:9000'; |
| 62 | + |
| 63 | +// Sample test files (base64 encoded for convenience) |
| 64 | +// In production, you'd load these from actual files |
| 65 | +const TEST_FILES = { |
| 66 | + // Minimal valid PNG (1x1 pixel, red) |
| 67 | + png: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==', |
| 68 | + |
| 69 | + // Minimal JPEG header |
| 70 | + jpeg: '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AVp//2Q==', |
| 71 | +}; |
| 72 | + |
| 73 | +// Test data generator - creates various test scenarios |
| 74 | +function generateTestFile(fileType, scenario = 'valid') { |
| 75 | + const files = { |
| 76 | + valid_png: { |
| 77 | + name: 'test_valid.png', |
| 78 | + data: TEST_FILES.png, |
| 79 | + contentType: 'image/png', |
| 80 | + }, |
| 81 | + valid_jpeg: { |
| 82 | + name: 'test_valid.jpg', |
| 83 | + data: TEST_FILES.jpeg, |
| 84 | + contentType: 'image/jpeg', |
| 85 | + }, |
| 86 | + truncated: { |
| 87 | + name: 'test_truncated.png', |
| 88 | + data: TEST_FILES.png.substring(0, 20), // Truncated |
| 89 | + contentType: 'image/png', |
| 90 | + }, |
| 91 | + malformed: { |
| 92 | + name: 'test_malformed.jpg', |
| 93 | + data: 'NOT_A_VALID_IMAGE_FILE', |
| 94 | + contentType: 'image/jpeg', |
| 95 | + }, |
| 96 | + large: { |
| 97 | + name: 'test_large.bin', |
| 98 | + data: 'X'.repeat(10000), // Simulate large file |
| 99 | + contentType: 'application/octet-stream', |
| 100 | + }, |
| 101 | + }; |
| 102 | + |
| 103 | + return files[scenario] || files.valid_png; |
| 104 | +} |
| 105 | + |
| 106 | +// Main test function |
| 107 | +export default function () { |
| 108 | + // Select test scenario (90% valid, 10% malformed for realistic load) |
| 109 | + const scenarios = ['valid_png', 'valid_jpeg', 'malformed']; |
| 110 | + const weights = [0.45, 0.45, 0.10]; |
| 111 | + const rand = Math.random(); |
| 112 | + let scenario = scenarios[0]; |
| 113 | + |
| 114 | + let cumulative = 0; |
| 115 | + for (let i = 0; i < scenarios.length; i++) { |
| 116 | + cumulative += weights[i]; |
| 117 | + if (rand < cumulative) { |
| 118 | + scenario = scenarios[i]; |
| 119 | + break; |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + const testFile = generateTestFile(scenario); |
| 124 | + |
| 125 | + // Test 1: Health check endpoint |
| 126 | + const healthRes = http.get(`${BASE_URL}/health`); |
| 127 | + check(healthRes, { |
| 128 | + 'health check status 200': (r) => r.status === 200, |
| 129 | + }); |
| 130 | + |
| 131 | + // Test 2: Metrics endpoint (ensure monitoring is working) |
| 132 | + const metricsRes = http.get(`${BASE_URL}/metrics`); |
| 133 | + check(metricsRes, { |
| 134 | + 'metrics endpoint accessible': (r) => r.status === 200, |
| 135 | + 'metrics contain expected data': (r) => r.body.includes('media_processor'), |
| 136 | + }); |
| 137 | + |
| 138 | + // Test 3: Process media file (simulated via direct CLI invocation) |
| 139 | + // Note: In a real setup, you'd have an HTTP API wrapper around the CLI |
| 140 | + // For now, we test the monitoring endpoints |
| 141 | + |
| 142 | + // Check for security violations in metrics |
| 143 | + if (metricsRes.body.includes('security_violations_total')) { |
| 144 | + const violationMatch = metricsRes.body.match(/security_violations_total{[^}]*} (\d+)/); |
| 145 | + if (violationMatch && parseInt(violationMatch[1]) > 0) { |
| 146 | + securityViolations.add(1); |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + // Track processing |
| 151 | + filesProcessed.add(1); |
| 152 | + |
| 153 | + // Record errors |
| 154 | + const hasError = healthRes.status !== 200 || metricsRes.status !== 200; |
| 155 | + errorRate.add(hasError); |
| 156 | + |
| 157 | + // Simulate processing time based on file type |
| 158 | + const processingTime = scenario.includes('large') ? |
| 159 | + Math.random() * 3000 + 2000 : // 2-5 seconds for large files |
| 160 | + Math.random() * 1000 + 500; // 0.5-1.5 seconds for normal files |
| 161 | + |
| 162 | + processingDuration.add(processingTime); |
| 163 | + |
| 164 | + // Small delay between requests (realistic user behavior) |
| 165 | + sleep(Math.random() * 2 + 1); // 1-3 seconds |
| 166 | +} |
| 167 | + |
| 168 | +// Setup function (runs once at start) |
| 169 | +export function setup() { |
| 170 | + console.log('Starting load test...'); |
| 171 | + console.log(`Target: ${BASE_URL}`); |
| 172 | + console.log('Scenario: Smoke test (5 concurrent users)'); |
| 173 | + console.log(''); |
| 174 | + |
| 175 | + // Verify service is accessible |
| 176 | + const res = http.get(`${BASE_URL}/health`); |
| 177 | + if (res.status !== 200) { |
| 178 | + throw new Error(`Service not accessible at ${BASE_URL}`); |
| 179 | + } |
| 180 | + |
| 181 | + return { startTime: Date.now() }; |
| 182 | +} |
| 183 | + |
| 184 | +// Teardown function (runs once at end) |
| 185 | +export function teardown(data) { |
| 186 | + const duration = (Date.now() - data.startTime) / 1000; |
| 187 | + console.log(''); |
| 188 | + console.log('Load test completed!'); |
| 189 | + console.log(`Total duration: ${duration.toFixed(2)} seconds`); |
| 190 | + console.log(''); |
| 191 | + console.log('Check the full report above for detailed metrics.'); |
| 192 | + console.log(''); |
| 193 | + console.log('To view live metrics during the test:'); |
| 194 | + console.log(` curl ${BASE_URL}/metrics`); |
| 195 | + console.log(''); |
| 196 | + console.log('To view Grafana dashboards:'); |
| 197 | + console.log(' http://localhost:3000 (if running via docker-compose)'); |
| 198 | +} |
| 199 | + |
| 200 | +// Handle HTTP errors |
| 201 | +export function handleSummary(data) { |
| 202 | + return { |
| 203 | + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), |
| 204 | + 'load-test-results.json': JSON.stringify(data), |
| 205 | + }; |
| 206 | +} |
| 207 | + |
| 208 | +// Helper function for text summary |
| 209 | +function textSummary(data, options = {}) { |
| 210 | + const indent = options.indent || ''; |
| 211 | + const enableColors = options.enableColors || false; |
| 212 | + |
| 213 | + let output = '\n'; |
| 214 | + output += `${indent}Load Test Summary\n`; |
| 215 | + output += `${indent}================\n\n`; |
| 216 | + |
| 217 | + // Requests |
| 218 | + const requests = data.metrics.http_reqs?.values; |
| 219 | + if (requests) { |
| 220 | + output += `${indent}HTTP Requests:\n`; |
| 221 | + output += `${indent} Total: ${requests.count}\n`; |
| 222 | + output += `${indent} Rate: ${requests.rate.toFixed(2)}/s\n\n`; |
| 223 | + } |
| 224 | + |
| 225 | + // Duration |
| 226 | + const duration = data.metrics.http_req_duration?.values; |
| 227 | + if (duration) { |
| 228 | + output += `${indent}Request Duration:\n`; |
| 229 | + output += `${indent} Min: ${duration.min.toFixed(2)}ms\n`; |
| 230 | + output += `${indent} Avg: ${duration.avg.toFixed(2)}ms\n`; |
| 231 | + output += `${indent} Max: ${duration.max.toFixed(2)}ms\n`; |
| 232 | + output += `${indent} p(95): ${duration['p(95)'].toFixed(2)}ms\n`; |
| 233 | + output += `${indent} p(99): ${duration['p(99)'].toFixed(2)}ms\n\n`; |
| 234 | + } |
| 235 | + |
| 236 | + // Errors |
| 237 | + const failed = data.metrics.http_req_failed?.values; |
| 238 | + if (failed) { |
| 239 | + const rate = (failed.rate * 100).toFixed(2); |
| 240 | + output += `${indent}Errors:\n`; |
| 241 | + output += `${indent} Failed requests: ${failed.passes} (${rate}%)\n\n`; |
| 242 | + } |
| 243 | + |
| 244 | + return output; |
| 245 | +} |
0 commit comments