import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
{{#each dynamic_imports}}
{{{this}}}
{{/each}}
{{#each dynamic_globals}}
{{{this}}}
{{/each}}
// Custom metrics per operation
{{#each operations}}
const {{this.name}}_latency = new Trend('{{this.metric_name}}_latency');
const {{this.name}}_errors = new Rate('{{this.metric_name}}_errors');
{{/each}}
// Test configuration
export const options = {
{{#if skip_tls_verify}}
insecureSkipTLSVerify: true,
{{/if}}
scenarios: {
{{scenario_name}}: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{{#each stages}}
{ duration: '{{this.duration}}', target: {{this.target}} },
{{/each}}
],
gracefulRampDown: '10s',
},
},
thresholds: {
'http_req_duration': ['{{threshold_percentile}}<{{threshold_ms}}'],
'http_req_failed': ['rate<{{max_error_rate}}'],
},
};
const BASE_URL = '{{base_url}}';
export default function () {
{{#each operations}}
// Operation {{@index}}: {{this.display_name}}
{
{{#if @root.security_testing_enabled}}
// Get a grouped security payload for each operation (array of related payloads)
const secPayloadGroup = typeof getNextSecurityPayload === 'function' ? getNextSecurityPayload() : [];
// Copy headers and build request URL, applying all payloads from the group
const requestHeaders = { ...{{{this.headers}}} };
{{#if this.path_is_dynamic}}
const __path = {{{this.path}}};
let requestUrl = `${BASE_URL}${__path}`;
{{else}}
let requestUrl = `${BASE_URL}{{{this.path}}}`;
{{/if}}
let secBodyPayload = null;
let hasSecCookie = false;
for (const secPayload of secPayloadGroup) {
if (secPayload.location === 'header' && secPayload.headerName) {
requestHeaders[secPayload.headerName] = secPayload.payload;
if (secPayload.headerName === 'Cookie') hasSecCookie = true;
}
if (secPayload.location === 'uri') {
if (secPayload.injectAsPath) {
// Replace URL path so WAF inspects via REQUEST_FILENAME (e.g., CRS 942101)
requestUrl = `${BASE_URL}/${encodeURI(secPayload.payload)}`;
} else {
requestUrl += (requestUrl.includes('?') ? '&' : '?') + 'test=' + encodeURIComponent(secPayload.payload);
}
}
if (secPayload.location === 'body') {
secBodyPayload = secPayload;
}
}
// Skip CookieJar when security payload sets Cookie header (jar overrides manual Cookie headers)
const secRequestOpts = hasSecCookie ? { headers: requestHeaders } : { headers: requestHeaders, jar: new http.CookieJar() };
{{else}}
const requestHeaders = {{{this.headers}}};
{{/if}}
{{#if this.has_body}}
{{#if this.body_is_dynamic}}
// Dynamic body with runtime placeholders
let payload = {{{this.body}}};
{{else}}
let payload = {{{this.body}}};
{{/if}}
{{#if @root.security_testing_enabled}}
// Apply security payload to body if available
let requestBody;
if (secBodyPayload && secBodyPayload.formBody) {
// Parse form body into object for k6's native form encoding (CRS 942432)
// k6 auto-encodes objects as application/x-www-form-urlencoded
const formData = {};
secBodyPayload.formBody.split('&').forEach(function(pair) {
const eq = pair.indexOf('=');
if (eq >= 0) {
formData[decodeURIComponent(pair.substring(0, eq).replace(/\+/g, ' '))] =
decodeURIComponent(pair.substring(eq + 1).replace(/\+/g, ' '));
}
});
requestBody = formData;
} else if (secBodyPayload && typeof applySecurityPayload === 'function') {
payload = applySecurityPayload(payload, [], secBodyPayload);
requestBody = JSON.stringify(payload);
} else {
requestBody = JSON.stringify(payload);
}
const res = http.{{this.method}}(requestUrl, requestBody, secRequestOpts);
{{else}}
{{#if this.path_is_dynamic}}
const url = {{{this.path}}};
const res = http.{{this.method}}(`${BASE_URL}${url}`, JSON.stringify(payload), { headers: requestHeaders, jar: new http.CookieJar() });
{{else}}
const res = http.{{this.method}}(`${BASE_URL}{{{this.path}}}`, JSON.stringify(payload), { headers: requestHeaders, jar: new http.CookieJar() });
{{/if}}
{{/if}}
{{else if this.is_get_or_head}}
// GET and HEAD only take 2 args: http.get(url, params)
{{#if @root.security_testing_enabled}}
const res = http.{{this.method}}(requestUrl, secRequestOpts);
{{else}}
{{#if this.path_is_dynamic}}
const url = {{{this.path}}};
const res = http.{{this.method}}(`${BASE_URL}${url}`, { headers: requestHeaders, jar: new http.CookieJar() });
{{else}}
const res = http.{{this.method}}(`${BASE_URL}{{{this.path}}}`, { headers: requestHeaders, jar: new http.CookieJar() });
{{/if}}
{{/if}}
{{else}}
// POST, PUT, PATCH, DELETE take 3 args: http.post(url, body, params)
{{#if @root.security_testing_enabled}}
// Send form-encoded body if available, otherwise null
let requestBody = null;
if (secBodyPayload && secBodyPayload.formBody) {
requestBody = secBodyPayload.formBody;
requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
}
const res = http.{{this.method}}(requestUrl, requestBody, secRequestOpts);
{{else}}
{{#if this.path_is_dynamic}}
const url = {{{this.path}}};
const res = http.{{this.method}}(`${BASE_URL}${url}`, null, { headers: requestHeaders, jar: new http.CookieJar() });
{{else}}
const res = http.{{this.method}}(`${BASE_URL}{{{this.path}}}`, null, { headers: requestHeaders, jar: new http.CookieJar() });
{{/if}}
{{/if}}
{{/if}}
const success = check(res, {
'{{this.display_name}}: status is OK': (r) => r.status >= 200 && r.status < 300,
'{{this.display_name}}: has response': (r) => r.body !== null && r.body.length > 0,
});
{{this.name}}_latency.add(res.timings.duration);
{{this.name}}_errors.add(!success);
sleep(1);
}
{{/each}}
}
export function handleSummary(data) {
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
'summary.json': JSON.stringify(data),
};
}
function textSummary(data, options) {
const indent = options.indent || '';
const enableColors = options.enableColors || false;
const metrics = data.metrics;
let output = '\n';
output += indent + '='.repeat(60) + '\n';
output += indent + 'Load Test Summary\n';
output += indent + '='.repeat(60) + '\n\n';
// Request metrics
if (metrics.http_reqs && metrics.http_reqs.values) {
const count = metrics.http_reqs.values.count || 0;
const rate = metrics.http_reqs.values.rate;
output += indent + `Total Requests: ${count}\n`;
output += indent + `Request Rate: ${rate != null ? rate.toFixed(2) : '0.00'} req/s\n\n`;
} else {
output += indent + 'Total Requests: 0\n';
output += indent + 'Request Rate: 0.00 req/s\n\n';
}
// Duration metrics
if (metrics.http_req_duration && metrics.http_req_duration.values) {
const v = metrics.http_req_duration.values;
output += indent + 'Response Times:\n';
output += indent + ` Min: ${v.min != null ? v.min.toFixed(2) : 'N/A'}ms\n`;
output += indent + ` Avg: ${v.avg != null ? v.avg.toFixed(2) : 'N/A'}ms\n`;
output += indent + ` Med: ${v.med != null ? v.med.toFixed(2) : 'N/A'}ms\n`;
output += indent + ` p90: ${v['p(90)'] != null ? v['p(90)'].toFixed(2) : 'N/A'}ms\n`;
output += indent + ` p95: ${v['p(95)'] != null ? v['p(95)'].toFixed(2) : 'N/A'}ms\n`;
output += indent + ` p99: ${v['p(99)'] != null ? v['p(99)'].toFixed(2) : 'N/A'}ms\n`;
output += indent + ` Max: ${v.max != null ? v.max.toFixed(2) : 'N/A'}ms\n\n`;
} else {
output += indent + 'Response Times: No successful requests\n\n';
}
// Error rate
if (metrics.http_req_failed && metrics.http_req_failed.values) {
const rate = metrics.http_req_failed.values.rate;
const errorRate = rate != null ? (rate * 100).toFixed(2) : '100.00';
output += indent + `Error Rate: ${errorRate}%\n\n`;
} else {
output += indent + 'Error Rate: N/A\n\n';
}
output += indent + '='.repeat(60) + '\n';
return output;
}