Add k6 load test scripts for Darkwatch authentication endpoints (FRE-4928)
- darkwatch-auth.js: k6 script testing POST /auth/login, /auth/logout, /auth/refresh - P99 thresholds: login <200ms, logout <100ms, refresh <150ms - Config: 500 req/s sustained for 5 minutes - Mixed workload scenario + individual endpoint scenarios - .env.example and run.sh for execution Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
20
load-tests/darkwatch-auth/.env.example
Normal file
20
load-tests/darkwatch-auth/.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# Darkwatch Auth Load Test Configuration
|
||||
# Copy to .env and adjust values
|
||||
|
||||
# Base URL of the Darkwatch API
|
||||
DARKWATCH_BASE_URL=http://localhost:3000
|
||||
|
||||
# Test credentials for load testing
|
||||
TEST_EMAIL=loadtest@darkwatch.shieldai
|
||||
TEST_PASSWORD=LoadTest2026!
|
||||
|
||||
# Test duration (default: 300s = 5 minutes)
|
||||
DURATION=300s
|
||||
|
||||
# Target requests per second (default: 500)
|
||||
TARGET_RPS=500
|
||||
|
||||
# P99 latency thresholds in milliseconds
|
||||
LOGIN_P99_MS=200
|
||||
LOGOUT_P99_MS=100
|
||||
REFRESH_P99_MS=150
|
||||
293
load-tests/darkwatch-auth/darkwatch-auth.js
Normal file
293
load-tests/darkwatch-auth/darkwatch-auth.js
Normal file
@@ -0,0 +1,293 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
|
||||
// ── Configuration ────────────────────────────────────────────────────────────
|
||||
const BASE_URL = __ENV.DARKWATCH_BASE_URL || 'http://localhost:3000';
|
||||
const TEST_EMAIL = __ENV.TEST_EMAIL || 'loadtest@darkwatch.shieldai';
|
||||
const TEST_PASSWORD = __ENV.TEST_PASSWORD || 'LoadTest2026!';
|
||||
const DURATION = __ENV.DURATION || '300s'; // 5 minutes
|
||||
const TARGET_RPS = parseInt(__ENV.TARGET_RPS || '500', 10);
|
||||
|
||||
// P99 latency thresholds (ms)
|
||||
const THRESHOLDS = {
|
||||
login: parseInt(__ENV.LOGIN_P99_MS || '200', 10),
|
||||
logout: parseInt(__ENV.LOGOUT_P99_MS || '100', 10),
|
||||
refresh: parseInt(__ENV.REFRESH_P99_MS || '150', 10),
|
||||
};
|
||||
|
||||
// ── Custom Metrics ───────────────────────────────────────────────────────────
|
||||
const loginLatency = new Trend('login_p99');
|
||||
const logoutLatency = new Trend('logout_p99');
|
||||
const refreshLatency = new Trend('refresh_p99');
|
||||
|
||||
const loginSuccess = new Rate('login_success');
|
||||
const logoutSuccess = new Rate('logout_success');
|
||||
const refreshSuccess = new Rate('refresh_success');
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
const authHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// ── Scenario: Login (POST /auth/login) ──────────────────────────────────────
|
||||
function testLogin() {
|
||||
const uniqueEmail = `${TEST_EMAIL.replace('@', `_${uuidv4().slice(0, 8)}@`)}`;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
email: uniqueEmail,
|
||||
password: TEST_PASSWORD,
|
||||
});
|
||||
|
||||
const res = http.post(`${BASE_URL}/auth/login`, payload, { headers: authHeaders });
|
||||
const duration = res.timings.duration;
|
||||
loginLatency.add(duration);
|
||||
|
||||
const success = res.status === 200 || res.status === 201;
|
||||
loginSuccess.add(success);
|
||||
|
||||
check(res, {
|
||||
'login: status 200 or 201': (r) => r.status === 200 || r.status === 201,
|
||||
'login: has access_token': (r) => {
|
||||
try {
|
||||
const json = JSON.parse(r.body);
|
||||
return !!json.access_token || !!json.token || !!json.data?.access_token;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
`login: P99 < ${THRESHOLDS.login}ms`: (r) => duration < THRESHOLDS.login,
|
||||
});
|
||||
|
||||
try {
|
||||
const json = JSON.parse(res.body);
|
||||
return {
|
||||
accessToken: json.access_token || json.token || json.data?.access_token || uuidv4(),
|
||||
refreshToken: json.refresh_token || json.data?.refresh_token || uuidv4(),
|
||||
userId: json.user?.id || json.data?.user?.id || uuidv4(),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
accessToken: uuidv4(),
|
||||
refreshToken: uuidv4(),
|
||||
userId: uuidv4(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scenario: Refresh (POST /auth/refresh) ──────────────────────────────────
|
||||
function testRefresh(refreshToken) {
|
||||
const token = refreshToken || uuidv4();
|
||||
|
||||
const payload = JSON.stringify({
|
||||
refresh_token: token,
|
||||
});
|
||||
|
||||
const res = http.post(`${BASE_URL}/auth/refresh`, payload, { headers: authHeaders });
|
||||
const duration = res.timings.duration;
|
||||
refreshLatency.add(duration);
|
||||
|
||||
const success = res.status === 200;
|
||||
refreshSuccess.add(success);
|
||||
|
||||
check(res, {
|
||||
'refresh: status 200': (r) => r.status === 200,
|
||||
'refresh: has new access_token': (r) => {
|
||||
try {
|
||||
const json = JSON.parse(r.body);
|
||||
return !!json.access_token || !!json.token || !!json.data?.access_token;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
`refresh: P99 < ${THRESHOLDS.refresh}ms`: (r) => duration < THRESHOLDS.refresh,
|
||||
});
|
||||
|
||||
try {
|
||||
const json = JSON.parse(res.body);
|
||||
return {
|
||||
accessToken: json.access_token || json.token || json.data?.access_token || uuidv4(),
|
||||
refreshToken: json.refresh_token || json.data?.refresh_token || token,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
accessToken: uuidv4(),
|
||||
refreshToken: token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scenario: Logout (POST /auth/logout) ────────────────────────────────────
|
||||
function testLogout(accessToken) {
|
||||
const token = accessToken || uuidv4();
|
||||
|
||||
const payload = JSON.stringify({
|
||||
access_token: token,
|
||||
});
|
||||
|
||||
const res = http.post(`${BASE_URL}/auth/logout`, payload, {
|
||||
headers: {
|
||||
...authHeaders,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const duration = res.timings.duration;
|
||||
logoutLatency.add(duration);
|
||||
|
||||
const success = res.status === 200 || res.status === 204;
|
||||
logoutSuccess.add(success);
|
||||
|
||||
check(res, {
|
||||
'logout: status 200 or 204': (r) => r.status === 200 || r.status === 204,
|
||||
`logout: P99 < ${THRESHOLDS.logout}ms`: (r) => duration < THRESHOLDS.logout,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Default Scenario: Weighted mixed workload ────────────────────────────────
|
||||
export const options = {
|
||||
scenarios: {
|
||||
sustained_load: {
|
||||
executor: 'constant-arrival-rate',
|
||||
duration: DURATION,
|
||||
rate: TARGET_RPS,
|
||||
preAllocatedVUs: 20,
|
||||
maxVUs: 100,
|
||||
startTime: '0s',
|
||||
exec: 'mixedWorkload',
|
||||
tags: { scenario: 'sustained_load' },
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
`login_p99`: [`p(99)<${THRESHOLDS.login}`],
|
||||
`logout_p99`: [`p(99)<${THRESHOLDS.logout}`],
|
||||
`refresh_p99`: [`p(99)<${THRESHOLDS.refresh}`],
|
||||
`login_success`: ['rate>0.95'],
|
||||
`logout_success`: ['rate>0.95'],
|
||||
`refresh_success`: ['rate>0.95'],
|
||||
http_req_duration: [`p(95)<300`, `p(99)<400`],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
// Mixed workload: 40% login, 35% refresh, 25% logout
|
||||
export function mixedWorkload() {
|
||||
const rand = Math.random();
|
||||
|
||||
if (rand < 0.4) {
|
||||
const tokens = testLogin();
|
||||
sleep(0.05);
|
||||
testRefresh(tokens.refreshToken);
|
||||
sleep(0.02);
|
||||
testLogout(tokens.accessToken);
|
||||
} else if (rand < 0.75) {
|
||||
const tokens = testLogin();
|
||||
sleep(0.05);
|
||||
testRefresh(tokens.refreshToken);
|
||||
} else {
|
||||
const tokens = testLogin();
|
||||
sleep(0.05);
|
||||
testLogout(tokens.accessToken);
|
||||
}
|
||||
|
||||
sleep(0.05);
|
||||
}
|
||||
|
||||
// ── Individual endpoint scenarios for targeted testing ───────────────────────
|
||||
export const endpointScenarios = {
|
||||
login_only: {
|
||||
executor: 'constant-arrival-rate',
|
||||
duration: DURATION,
|
||||
rate: TARGET_RPS,
|
||||
preAllocatedVUs: 20,
|
||||
maxVUs: 100,
|
||||
exec: 'loginOnly',
|
||||
startTime: '0s',
|
||||
tags: { scenario: 'login_only' },
|
||||
},
|
||||
logout_only: {
|
||||
executor: 'constant-arrival-rate',
|
||||
duration: DURATION,
|
||||
rate: TARGET_RPS,
|
||||
preAllocatedVUs: 20,
|
||||
maxVUs: 100,
|
||||
exec: 'logoutOnly',
|
||||
startTime: '0s',
|
||||
tags: { scenario: 'logout_only' },
|
||||
},
|
||||
refresh_only: {
|
||||
executor: 'constant-arrival-rate',
|
||||
duration: DURATION,
|
||||
rate: TARGET_RPS,
|
||||
preAllocatedVUs: 20,
|
||||
maxVUs: 100,
|
||||
exec: 'refreshOnly',
|
||||
startTime: '0s',
|
||||
tags: { scenario: 'refresh_only' },
|
||||
},
|
||||
};
|
||||
|
||||
export function loginOnly() {
|
||||
testLogin();
|
||||
sleep(0.1);
|
||||
}
|
||||
|
||||
export function logoutOnly() {
|
||||
const tokens = testLogin();
|
||||
sleep(0.05);
|
||||
testLogout(tokens.accessToken);
|
||||
sleep(0.05);
|
||||
}
|
||||
|
||||
export function refreshOnly() {
|
||||
const tokens = testLogin();
|
||||
sleep(0.05);
|
||||
testRefresh(tokens.refreshToken);
|
||||
sleep(0.05);
|
||||
}
|
||||
|
||||
// ── Summary Hook ─────────────────────────────────────────────────────────────
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'stdout': `\n=== Darkwatch Auth Load Test Results ===\n`,
|
||||
'summary.json': JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
duration: DURATION,
|
||||
targetRPS: TARGET_RPS,
|
||||
thresholds: THRESHOLDS,
|
||||
metrics: {
|
||||
login: {
|
||||
p99: data.metrics.login_p99?.values['p(99)']?.toFixed(2) || 'N/A',
|
||||
p95: data.metrics.login_p99?.values['p(95)']?.toFixed(2) || 'N/A',
|
||||
avg: data.metrics.login_p99?.values.avg?.toFixed(2) || 'N/A',
|
||||
count: data.metrics.login_p99?.values.count || 0,
|
||||
successRate: (data.metrics.login_success?.values.rate || 0) * 100 + '%',
|
||||
},
|
||||
logout: {
|
||||
p99: data.metrics.logout_p99?.values['p(99)']?.toFixed(2) || 'N/A',
|
||||
p95: data.metrics.logout_p99?.values['p(95)']?.toFixed(2) || 'N/A',
|
||||
avg: data.metrics.logout_p99?.values.avg?.toFixed(2) || 'N/A',
|
||||
count: data.metrics.logout_p99?.values.count || 0,
|
||||
successRate: (data.metrics.logout_success?.values.rate || 0) * 100 + '%',
|
||||
},
|
||||
refresh: {
|
||||
p99: data.metrics.refresh_p99?.values['p(99)']?.toFixed(2) || 'N/A',
|
||||
p95: data.metrics.refresh_p99?.values['p(95)']?.toFixed(2) || 'N/A',
|
||||
avg: data.metrics.refresh_p99?.values.avg?.toFixed(2) || 'N/A',
|
||||
count: data.metrics.refresh_p99?.values.count || 0,
|
||||
successRate: (data.metrics.refresh_success?.values.rate || 0) * 100 + '%',
|
||||
},
|
||||
},
|
||||
passed: Object.entries(data.metrics).every(
|
||||
([_, metric]) => metric?.thresholds?.every?.((t) => t.pass)
|
||||
),
|
||||
}, null, 2),
|
||||
};
|
||||
}
|
||||
69
load-tests/darkwatch-auth/run.sh
Executable file
69
load-tests/darkwatch-auth/run.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run k6 load tests for Darkwatch authentication endpoints
|
||||
# Usage: ./run.sh [scenario]
|
||||
# scenario: mixed (default), login, logout, refresh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Load environment variables from .env if present
|
||||
if [[ -f .env ]]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
SCENARIO="${1:-mixed}"
|
||||
OUTPUT_DIR="${SCRIPT_DIR}/results"
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "=== Darkwatch Auth Load Test ==="
|
||||
echo "Scenario: $SCENARIO"
|
||||
echo "Target RPS: ${TARGET_RPS:-500}"
|
||||
echo "Duration: ${DURATION:-300s}"
|
||||
echo "Base URL: ${DARKWATCH_BASE_URL:-http://localhost:3000}"
|
||||
echo ""
|
||||
|
||||
case "$SCENARIO" in
|
||||
mixed)
|
||||
k6 run darkwatch-auth.js \
|
||||
--out json="$OUTPUT_DIR/results-${TIMESTAMP}.json" \
|
||||
<<EOF
|
||||
EOF
|
||||
;;
|
||||
login)
|
||||
k6 run --scenario login_only darkwatch-auth.js \
|
||||
--out json="$OUTPUT_DIR/results-${TIMESTAMP}.json"
|
||||
;;
|
||||
logout)
|
||||
k6 run --scenario logout_only darkwatch-auth.js \
|
||||
--out json="$OUTPUT_DIR/results-${TIMESTAMP}.json"
|
||||
;;
|
||||
refresh)
|
||||
k6 run --scenario refresh_only darkwatch-auth.js \
|
||||
--out json="$OUTPUT_DIR/results-${TIMESTAMP}.json"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown scenario: $SCENARIO"
|
||||
echo "Available: mixed, login, logout, refresh"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [[ $EXIT_CODE -eq 0 ]]; then
|
||||
echo ""
|
||||
echo "✅ All thresholds passed!"
|
||||
echo "Results saved to: $OUTPUT_DIR/results-${TIMESTAMP}.json"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ Thresholds failed. Check output above."
|
||||
echo "Results saved to: $OUTPUT_DIR/results-${TIMESTAMP}.json"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
Reference in New Issue
Block a user