import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
{{#each dynamic_imports}}
{{{this}}}
{{/each}}
{{#each dynamic_globals}}
{{{this}}}
{{/each}}
// Flow-level metrics
const flowSuccessRate = new Rate('flow_success_rate');
const flowDuration = new Trend('flow_duration');
const flowErrors = new Counter('flow_errors');
{{#if error_injection_enabled}}
// Error injection metrics
const errorInjectionRate = new Rate('error_injection_rate');
const errorInjectionCount = new Counter('error_injection_count');
{{/if}}
// Step-level metrics
{{#each flows}}
{{#each this.steps}}
const {{../name}}_step{{@index}}_latency = new Trend('{{../name}}_step{{@index}}_latency');
const {{../name}}_step{{@index}}_errors = new Rate('{{../name}}_step{{@index}}_errors');
{{/each}}
{{/each}}
// Test configuration
export const options = {
{{#if skip_tls_verify}}
insecureSkipTLSVerify: true,
{{/if}}
scenarios: {
crud_flow: {
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}}'],
'flow_success_rate': ['rate>0.95'],
},
};
const BASE_URL = '{{base_url}}';
const EXTRACTED_VALUES_OUTPUT_PATH = '{{extracted_values_output_path}}';
// Common headers
const headers = {{{headers}}};
const globalExtractedValues = {};
{{#if error_injection_enabled}}
// Error injection configuration
const ERROR_RATE = {{error_rate}};
const ERROR_TYPES = [{{#each error_types}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}];
// Determine if this request should be invalidated
function shouldInvalidate() {
const inject = Math.random() < ERROR_RATE;
errorInjectionRate.add(inject);
return inject;
}
// Select a random error type
function selectErrorType() {
if (ERROR_TYPES.length === 0) {
return 'MissingField'; // Default
}
return ERROR_TYPES[Math.floor(Math.random() * ERROR_TYPES.length)];
}
// Apply error injection to a payload
function injectError(payload) {
if (!payload || typeof payload !== 'object') {
return payload;
}
const errorType = selectErrorType();
const result = { ...payload };
const keys = Object.keys(result);
if (keys.length === 0) {
return result;
}
const randomKey = keys[Math.floor(Math.random() * keys.length)];
const originalValue = result[randomKey];
switch (errorType) {
case 'MissingField':
// Remove a required field
delete result[randomKey];
console.log(`[ERROR INJECTION] Removed field: ${randomKey}`);
break;
case 'WrongType':
// Change the type of a field
if (typeof originalValue === 'string') {
result[randomKey] = 12345;
} else if (typeof originalValue === 'number') {
result[randomKey] = 'invalid_string';
} else if (typeof originalValue === 'boolean') {
result[randomKey] = 'not_a_boolean';
} else if (Array.isArray(originalValue)) {
result[randomKey] = 'not_an_array';
} else {
result[randomKey] = null;
}
console.log(`[ERROR INJECTION] Changed type of ${randomKey}`);
break;
case 'Null':
// Set field to null
result[randomKey] = null;
console.log(`[ERROR INJECTION] Set ${randomKey} to null`);
break;
case 'OutOfRange':
// Set numeric fields to extreme values
if (typeof originalValue === 'number') {
result[randomKey] = originalValue > 0 ? -999999999 : 999999999;
} else if (typeof originalValue === 'string') {
result[randomKey] = 'x'.repeat(10000); // Very long string
}
console.log(`[ERROR INJECTION] Set ${randomKey} to out-of-range value`);
break;
case 'Empty':
// Set field to empty value
if (typeof originalValue === 'string') {
result[randomKey] = '';
} else if (Array.isArray(originalValue)) {
result[randomKey] = [];
} else if (typeof originalValue === 'object') {
result[randomKey] = {};
}
console.log(`[ERROR INJECTION] Set ${randomKey} to empty`);
break;
case 'InvalidFormat':
// Set field to invalid format
if (typeof originalValue === 'string') {
// Inject special characters
result[randomKey] = '!@#$%^&*()_+{}|:"<>?';
}
console.log(`[ERROR INJECTION] Set ${randomKey} to invalid format`);
break;
default:
// Default: remove the field
delete result[randomKey];
console.log(`[ERROR INJECTION] Default: removed field ${randomKey}`);
}
errorInjectionCount.add(1);
return result;
}
{{/if}}
// Helper function to extract value from JSON response
// Supports: simple fields (uuid), nested paths (data.uuid),
// array indices (results[0].uuid), and filters (results[?name="global"].uuid)
function extractValue(response, fieldPath, matchMode) {
try {
const json = response.json();
return extractFromObject(json, fieldPath, matchMode || 'first');
} catch (e) {
console.error(`Failed to extract ${fieldPath}: ${e.message}`);
return null;
}
}
// Extract value from object using path expression
function extractFromObject(obj, fieldPath, matchMode) {
if (!obj || !fieldPath) return null;
// Parse the field path into segments
const segments = parseFieldPath(fieldPath);
let value = obj;
for (const segment of segments) {
if (value === null || value === undefined) return null;
if (segment.type === 'field') {
// Simple field access
value = value[segment.name];
} else if (segment.type === 'index') {
// Array index access: [0], [1], etc.
if (!Array.isArray(value)) return null;
const idx = segment.index < 0 ? value.length + segment.index : segment.index;
value = value[idx];
} else if (segment.type === 'filter') {
// Filter access: [?field="value"] or [?field=value]
if (!Array.isArray(value)) return null;
const matches = value.filter(item => {
if (item && typeof item === 'object') {
const fieldVal = item[segment.filterField];
return String(fieldVal) === String(segment.filterValue);
}
return false;
});
if (matches.length === 0) return null;
// Return first or last based on matchMode
value = matchMode === 'last' ? matches[matches.length - 1] : matches[0];
}
}
return value;
}
// Parse field path into segments
function parseFieldPath(fieldPath) {
const segments = [];
let remaining = fieldPath;
while (remaining.length > 0) {
// Check for array index or filter: [0] or [?name="value"]
if (remaining.startsWith('[')) {
const closeIdx = remaining.indexOf(']');
if (closeIdx === -1) break;
const bracketContent = remaining.substring(1, closeIdx);
if (bracketContent.startsWith('?')) {
// Filter expression: [?name="value"] or [?name=value]
const filterExpr = bracketContent.substring(1);
const eqIdx = filterExpr.indexOf('=');
if (eqIdx !== -1) {
const filterField = filterExpr.substring(0, eqIdx).trim();
let filterValue = filterExpr.substring(eqIdx + 1).trim();
// Remove quotes if present
if ((filterValue.startsWith('"') && filterValue.endsWith('"')) ||
(filterValue.startsWith("'") && filterValue.endsWith("'"))) {
filterValue = filterValue.substring(1, filterValue.length - 1);
}
segments.push({ type: 'filter', filterField, filterValue });
}
} else {
// Numeric index: [0], [-1], etc.
const index = parseInt(bracketContent, 10);
if (!isNaN(index)) {
segments.push({ type: 'index', index });
}
}
remaining = remaining.substring(closeIdx + 1);
// Skip leading dot after bracket
if (remaining.startsWith('.')) {
remaining = remaining.substring(1);
}
} else {
// Field name
let dotIdx = remaining.indexOf('.');
let bracketIdx = remaining.indexOf('[');
let endIdx;
if (dotIdx === -1 && bracketIdx === -1) {
endIdx = remaining.length;
} else if (dotIdx === -1) {
endIdx = bracketIdx;
} else if (bracketIdx === -1) {
endIdx = dotIdx;
} else {
endIdx = Math.min(dotIdx, bracketIdx);
}
const fieldName = remaining.substring(0, endIdx);
if (fieldName.length > 0) {
segments.push({ type: 'field', name: fieldName });
}
remaining = remaining.substring(endIdx);
// Skip leading dot
if (remaining.startsWith('.')) {
remaining = remaining.substring(1);
}
}
}
return segments;
}
// Helper function to extract full response body with optional key filtering
function extractBody(response, excludeKeys) {
try {
const json = response.json();
if (excludeKeys && excludeKeys.length > 0) {
const filtered = { ...json };
for (const key of excludeKeys) {
delete filtered[key];
}
return filtered;
}
return json;
} catch (e) {
console.error(`Failed to extract body: ${e.message}`);
return null;
}
}
// Helper function to deep merge objects (target is modified with overrides)
function mergeObjects(target, overrides) {
if (!target || typeof target !== 'object') return overrides;
if (!overrides || typeof overrides !== 'object') return target;
const result = { ...target };
for (const [key, value] of Object.entries(overrides)) {
if (value && typeof value === 'object' && !Array.isArray(value) &&
result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
// Deep merge for nested objects
result[key] = mergeObjects(result[key], value);
} else {
// Direct override for primitives and arrays
result[key] = value;
}
}
return result;
}
// Helper function to replace path parameters
function replacePath(pathTemplate, values) {
let path = pathTemplate;
for (const [key, value] of Object.entries(values)) {
path = path.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
return path;
}
export default function () {
{{#each flows}}
// Flow: {{this.display_name}}
group('{{this.display_name}}', function() {
const flowStart = Date.now();
let flowSuccess = true;
const extractedValues = {};
{{#each this.steps}}
// Step {{@index}}: {{this.description}}
{
{{#if this.use_values}}
// Apply extracted values to path
const pathValues = {};
{{#each this.use_values}}
if (extractedValues['{{this}}']) {
pathValues['{{@key}}'] = extractedValues['{{this}}'];
}
{{/each}}
let path = replacePath('{{{this.path}}}', pathValues);
{{else}}
let path = '{{{this.path}}}';
{{/if}}
{{#if @root.security_testing_enabled}}
// Security testing: get next payload group and apply to request
const secPayloadGroup = typeof getNextSecurityPayload === 'function' ? getNextSecurityPayload() : [];
let requestHeaders = { ...headers };
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)
path = `/${encodeURI(secPayload.payload)}`;
} else {
const separator = path.includes('?') ? '&' : '?';
path = `${path}${separator}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() };
{{/if}}
{{#if this.use_body}}
// Use previously extracted body
let payload = extractedValues['{{this.use_body}}'];
if (!payload) {
console.error('Extracted body {{this.use_body}} not found');
payload = {};
}
{{#if this.merge_body}}
// Merge with override values
payload = mergeObjects(payload, {{{json this.merge_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)
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);
}
{{/if}}
{{#if @root.error_injection_enabled}}
// Apply error injection if triggered
if (shouldInvalidate()) {
payload = injectError(payload);
}
{{/if}}
{{#if @root.security_testing_enabled}}
const res = http.{{this.method}}(`${BASE_URL}${path}`, requestBody, secRequestOpts);
{{else}}
const res = http.{{this.method}}(`${BASE_URL}${path}`, JSON.stringify(payload), { headers, jar: new http.CookieJar() });
{{/if}}
{{else if this.has_body}}
{{#if this.body_is_dynamic}}
// Dynamic body with runtime placeholders and extracted values
let payload = {{{this.body}}};
{{#each this.use_values}}
// Replace {{@key}} with extracted value if present
if (extractedValues['{{this}}']) {
payload = JSON.parse(JSON.stringify(payload).replace(/\$\{extracted\.{{@key}}\}/g, extractedValues['{{this}}']));
}
{{/each}}
{{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)
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);
}
{{/if}}
{{#if @root.error_injection_enabled}}
// Apply error injection if triggered
if (shouldInvalidate()) {
payload = injectError(payload);
}
{{/if}}
{{#if @root.security_testing_enabled}}
const res = http.{{this.method}}(`${BASE_URL}${path}`, requestBody, secRequestOpts);
{{else}}
const res = http.{{this.method}}(`${BASE_URL}${path}`, JSON.stringify(payload), { headers, jar: new http.CookieJar() });
{{/if}}
{{else if this.is_get_or_head}}
{{#if @root.security_testing_enabled}}
const res = http.{{this.method}}(`${BASE_URL}${path}`, secRequestOpts);
{{else}}
const res = http.{{this.method}}(`${BASE_URL}${path}`, { headers, jar: new http.CookieJar() });
{{/if}}
{{else}}
{{#if @root.security_testing_enabled}}
// Send form-encoded body if available, otherwise null
let requestBody = null;
if (secBodyPayload && secBodyPayload.formBody) {
// Parse form body into object for k6's native form encoding (CRS 942432)
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;
}
const res = http.{{this.method}}(`${BASE_URL}${path}`, requestBody, secRequestOpts);
{{else}}
const res = http.{{this.method}}(`${BASE_URL}${path}`, null, { headers, jar: new http.CookieJar() });
{{/if}}
{{/if}}
const success = check(res, {
'{{this.display_name}}: status is OK': (r) => r.status >= 200 && r.status < 300,
});
{{../name}}_step{{@index}}_latency.add(res.timings.duration);
{{../name}}_step{{@index}}_errors.add(!success);
if (!success) {
flowSuccess = false;
console.error(`Step {{@index}} ({{this.display_name}}) failed: ${res.status} - ${res.body}`);
}
{{#if this.extract}}
// Extract values for subsequent steps
if (success) {
{{#each this.extract}}
{{#if this.body}}
// Extract full response body with filtering
const extracted_{{this.store_as}} = extractBody(res, {{{json this.exclude}}});
{{else}}
// Extract field: {{{this.field}}}{{#if this.match_mode}} (match: {{this.match_mode}}){{/if}}
const extracted_{{this.store_as}} = extractValue(res, '{{{this.field}}}'{{#if this.match_mode}}, '{{this.match_mode}}'{{/if}});
{{/if}}
if (extracted_{{this.store_as}} !== null && extracted_{{this.store_as}} !== undefined) {
extractedValues['{{this.store_as}}'] = extracted_{{this.store_as}};
globalExtractedValues['{{this.store_as}}'] = extracted_{{this.store_as}};
}
{{/each}}
}
{{/if}}
sleep(0.5); // Brief pause between steps
}
{{/each}}
const flowEnd = Date.now();
flowDuration.add(flowEnd - flowStart);
flowSuccessRate.add(flowSuccess);
if (!flowSuccess) {
flowErrors.add(1);
}
});
{{/each}}
sleep(1); // Pause between flow iterations
}
export function handleSummary(data) {
return {
[EXTRACTED_VALUES_OUTPUT_PATH]: JSON.stringify(globalExtractedValues, null, 2),
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
};
}
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 + 'CRUD Flow Test Summary\n';
output += indent + '='.repeat(60) + '\n\n';
// Flow metrics
if (metrics.flow_success_rate && metrics.flow_success_rate.values) {
const rate = metrics.flow_success_rate.values.rate;
const successRate = rate != null ? (rate * 100).toFixed(2) : 'N/A';
output += indent + `Flow Success Rate: ${successRate}%\n`;
}
if (metrics.flow_duration && metrics.flow_duration.values) {
const v = metrics.flow_duration.values;
output += indent + `Flow Duration (avg): ${v.avg != null ? v.avg.toFixed(2) : 'N/A'}ms\n`;
output += indent + `Flow Duration (p95): ${v['p(95)'] != null ? v['p(95)'].toFixed(2) : 'N/A'}ms\n`;
}
if (metrics.flow_errors && metrics.flow_errors.values) {
const count = metrics.flow_errors.values.count || 0;
output += indent + `Flow Errors: ${count}\n`;
}
// Error injection metrics (if enabled)
if (metrics.error_injection_count && metrics.error_injection_count.values) {
const injected = metrics.error_injection_count.values.count || 0;
output += indent + `Error Injections: ${injected}\n`;
}
if (metrics.error_injection_rate && metrics.error_injection_rate.values) {
const rate = metrics.error_injection_rate.values.rate;
const injectionRate = rate != null ? (rate * 100).toFixed(2) : '0.00';
output += indent + `Error Injection Rate: ${injectionRate}%\n`;
}
output += '\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`;
}
// 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`;
}
// 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`;
}
output += indent + '='.repeat(60) + '\n';
return output;
}