mockforge-bench 0.3.119

Load and performance testing for MockForge
Documentation
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;
}