Files
git.stella-ops.org/tests/load/router/spike-test.js
StellaOps Bot 5146204f1b feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
2025-12-22 23:21:21 +02:00

228 lines
7.4 KiB
JavaScript

// -----------------------------------------------------------------------------
// spike-test.js
// Sprint: SPRINT_5100_0005_0001_router_chaos_suite
// Task: T1 - Load Test Harness
// Description: k6 load test for router spike testing and backpressure validation.
// -----------------------------------------------------------------------------
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// Custom metrics for throttle behavior
const throttledRate = new Rate('throttled_requests');
const retryAfterTrend = new Trend('retry_after_seconds');
const recoveryTime = new Trend('recovery_time_ms');
const throttle429Count = new Counter('throttle_429_count');
const throttle503Count = new Counter('throttle_503_count');
const successCount = new Counter('success_count');
export const options = {
scenarios: {
// Phase 1: Baseline load (normal operation)
baseline: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '1m',
preAllocatedVUs: 50,
maxVUs: 100,
},
// Phase 2: 10x spike
spike_10x: {
executor: 'constant-arrival-rate',
rate: 1000,
timeUnit: '1s',
duration: '30s',
startTime: '1m',
preAllocatedVUs: 500,
maxVUs: 1000,
},
// Phase 3: 50x spike
spike_50x: {
executor: 'constant-arrival-rate',
rate: 5000,
timeUnit: '1s',
duration: '30s',
startTime: '2m',
preAllocatedVUs: 2000,
maxVUs: 5000,
},
// Phase 4: Recovery observation
recovery: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '2m',
startTime: '3m',
preAllocatedVUs: 50,
maxVUs: 100,
},
},
thresholds: {
// At least 95% of requests should succeed OR return proper throttle response
'http_req_failed{expected_response:true}': ['rate<0.05'],
// Throttled requests should have Retry-After header
'throttled_requests': ['rate>0'], // We expect some throttling during spike
// Recovery should happen within reasonable time
'recovery_time_ms': ['p(95)<30000'], // 95% recover within 30s
// Response time should be bounded even under load
'http_req_duration{expected_response:true}': ['p(95)<5000'],
},
};
const ROUTER_URL = __ENV.ROUTER_URL || 'http://localhost:8080';
const API_ENDPOINT = __ENV.API_ENDPOINT || '/api/v1/scan';
export function setup() {
console.log(`Testing router at: ${ROUTER_URL}${API_ENDPOINT}`);
// Verify router is reachable
const healthCheck = http.get(`${ROUTER_URL}/health`);
if (healthCheck.status !== 200) {
console.warn(`Router health check returned ${healthCheck.status}`);
}
return {
startTime: new Date().toISOString(),
routerUrl: ROUTER_URL,
};
}
export default function () {
const payload = JSON.stringify({
image: 'alpine:latest',
requestId: `spike-test-${__VU}-${__ITER}`,
timestamp: new Date().toISOString(),
});
const params = {
headers: {
'Content-Type': 'application/json',
'X-Request-ID': `${__VU}-${__ITER}`,
},
tags: { expected_response: 'true' },
timeout: '10s',
};
const response = http.post(`${ROUTER_URL}${API_ENDPOINT}`, payload, params);
// Handle throttle responses (429 Too Many Requests)
if (response.status === 429) {
throttledRate.add(1);
throttle429Count.add(1);
// Verify Retry-After header
const retryAfter = response.headers['Retry-After'];
check(response, {
'429 has Retry-After header': (r) => r.headers['Retry-After'] !== undefined,
'Retry-After is valid number': (r) => {
const val = r.headers['Retry-After'];
return val && !isNaN(parseInt(val));
},
'Retry-After is reasonable (1-300s)': (r) => {
const val = parseInt(r.headers['Retry-After']);
return val >= 1 && val <= 300;
},
});
if (retryAfter) {
retryAfterTrend.add(parseInt(retryAfter));
}
}
// Handle overload responses (503 Service Unavailable)
else if (response.status === 503) {
throttledRate.add(1);
throttle503Count.add(1);
check(response, {
'503 has Retry-After header': (r) => r.headers['Retry-After'] !== undefined,
});
const retryAfter = response.headers['Retry-After'];
if (retryAfter) {
retryAfterTrend.add(parseInt(retryAfter));
}
}
// Handle success responses
else {
throttledRate.add(0);
successCount.add(1);
check(response, {
'status is 200 or 202': (r) => r.status === 200 || r.status === 202,
'response has body': (r) => r.body && r.body.length > 0,
'response time < 5s': (r) => r.timings.duration < 5000,
});
}
// Track any errors
if (response.status >= 500 && response.status !== 503) {
check(response, {
'no unexpected 5xx errors': () => false,
});
}
}
export function teardown(data) {
console.log(`Test completed. Started at: ${data.startTime}`);
console.log(`Router URL: ${data.routerUrl}`);
}
export function handleSummary(data) {
const summary = {
testRun: {
startTime: new Date().toISOString(),
routerUrl: ROUTER_URL,
},
metrics: {
totalRequests: data.metrics.http_reqs ? data.metrics.http_reqs.values.count : 0,
throttled429: data.metrics.throttle_429_count ? data.metrics.throttle_429_count.values.count : 0,
throttled503: data.metrics.throttle_503_count ? data.metrics.throttle_503_count.values.count : 0,
successful: data.metrics.success_count ? data.metrics.success_count.values.count : 0,
throttleRate: data.metrics.throttled_requests ? data.metrics.throttled_requests.values.rate : 0,
retryAfterAvg: data.metrics.retry_after_seconds ? data.metrics.retry_after_seconds.values.avg : null,
retryAfterP95: data.metrics.retry_after_seconds ? data.metrics.retry_after_seconds.values['p(95)'] : null,
},
thresholds: data.thresholds,
checks: data.metrics.checks ? {
passes: data.metrics.checks.values.passes,
fails: data.metrics.checks.values.fails,
rate: data.metrics.checks.values.rate,
} : null,
};
return {
'results/spike-test-summary.json': JSON.stringify(summary, null, 2),
stdout: textSummary(data, { indent: ' ', enableColors: true }),
};
}
function textSummary(data, options) {
let output = '\n=== Router Spike Test Summary ===\n\n';
const totalReqs = data.metrics.http_reqs ? data.metrics.http_reqs.values.count : 0;
const throttled429 = data.metrics.throttle_429_count ? data.metrics.throttle_429_count.values.count : 0;
const throttled503 = data.metrics.throttle_503_count ? data.metrics.throttle_503_count.values.count : 0;
const successful = data.metrics.success_count ? data.metrics.success_count.values.count : 0;
output += `Total Requests: ${totalReqs}\n`;
output += `Successful (2xx): ${successful}\n`;
output += `Throttled (429): ${throttled429}\n`;
output += `Overloaded (503): ${throttled503}\n`;
output += `Throttle Rate: ${((throttled429 + throttled503) / totalReqs * 100).toFixed(2)}%\n`;
if (data.metrics.retry_after_seconds) {
output += `\nRetry-After Header:\n`;
output += ` Avg: ${data.metrics.retry_after_seconds.values.avg.toFixed(2)}s\n`;
output += ` P95: ${data.metrics.retry_after_seconds.values['p(95)'].toFixed(2)}s\n`;
}
output += '\nThreshold Results:\n';
for (const [name, result] of Object.entries(data.thresholds || {})) {
output += ` ${result.ok ? 'PASS' : 'FAIL'}: ${name}\n`;
}
return output;
}