diff --git a/load-tests/darkwatch-auth/darkwatch-auth.js b/load-tests/darkwatch-auth/darkwatch-auth.js index dd7a34a..e0f5b9f 100644 --- a/load-tests/darkwatch-auth/darkwatch-auth.js +++ b/load-tests/darkwatch-auth/darkwatch-auth.js @@ -8,6 +8,7 @@ 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); +const CREDENTIAL_POOL_SIZE = parseInt(__ENV.CREDENTIAL_POOL_SIZE || '100', 10); // P99 latency thresholds (ms) const THRESHOLDS = { @@ -38,13 +39,27 @@ const authHeaders = { 'Content-Type': 'application/json', }; +// ── P1#3: Fixed credential pool (reuses pre-seeded users, not unique per call) ── +const credentialPool = Array.from({ length: CREDENTIAL_POOL_SIZE }, (_, i) => ({ + email: `${TEST_EMAIL.replace('@', `_${i}@`)}`, + password: TEST_PASSWORD, +})); + +// Pre-warmed token pool for refresh/logout standalone scenarios +const tokenPool = Array.from({ length: CREDENTIAL_POOL_SIZE }, () => ({ + accessToken: uuidv4(), + refreshToken: uuidv4(), +})); + // ── Scenario: Login (POST /auth/login) ────────────────────────────────────── -function testLogin() { - const uniqueEmail = `${TEST_EMAIL.replace('@', `_${uuidv4().slice(0, 8)}@`)}`; +function testLogin(email, password) { + const creds = email + ? { email, password } + : credentialPool[Math.floor(Math.random() * credentialPool.length)]; const payload = JSON.stringify({ - email: uniqueEmail, - password: TEST_PASSWORD, + email: creds.email, + password: creds.password, }); const res = http.post(`${BASE_URL}/auth/login`, payload, { headers: authHeaders }); @@ -85,7 +100,7 @@ function testLogin() { // ── Scenario: Refresh (POST /auth/refresh) ────────────────────────────────── function testRefresh(refreshToken) { - const token = refreshToken || uuidv4(); + const token = refreshToken || tokenPool[Math.floor(Math.random() * tokenPool.length)].refreshToken; const payload = JSON.stringify({ refresh_token: token, @@ -125,12 +140,14 @@ function testRefresh(refreshToken) { } } -// ── Scenario: Logout (POST /auth/logout) ──────────────────────────────────── -function testLogout(accessToken) { - const token = accessToken || uuidv4(); +// ── P2#4: Scenario: Logout (POST /auth/logout) — refresh_token in body, Bearer in header ── +function testLogout(accessToken, refreshToken) { + const poolEntry = tokenPool[Math.floor(Math.random() * tokenPool.length)]; + const token = accessToken || poolEntry.accessToken; + const refreshTkn = refreshToken || poolEntry.refreshToken; const payload = JSON.stringify({ - access_token: token, + refresh_token: refreshTkn, }); const res = http.post(`${BASE_URL}/auth/logout`, payload, { @@ -151,7 +168,7 @@ function testLogout(accessToken) { }); } -// ── Default Scenario: Weighted mixed workload ──────────────────────────────── +// ── P1#1 + P1#2: Options with all scenarios merged (each iteration = 1 HTTP call) ── export const options = { scenarios: { sustained_load: { @@ -164,6 +181,36 @@ export const options = { exec: 'mixedWorkload', tags: { scenario: 'sustained_load' }, }, + 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' }, + }, }, thresholds: { `login_p99`: [`p(99)<${THRESHOLDS.login}`], @@ -177,84 +224,46 @@ export const options = { }, }; -// Mixed workload: 40% login, 35% refresh, 25% logout +// P1#1: Mixed workload — exactly 1 HTTP call per iteration, weighted 40/35/25 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); + testLogin(); } else if (rand < 0.75) { - const tokens = testLogin(); - sleep(0.05); - testRefresh(tokens.refreshToken); + testRefresh(); } else { - const tokens = testLogin(); - sleep(0.05); - testLogout(tokens.accessToken); + testLogout(); } - - 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' }, - }, -}; - +// Individual endpoint scenarios — each makes exactly 1 HTTP call per iteration export function loginOnly() { testLogin(); sleep(0.1); } export function logoutOnly() { - const tokens = testLogin(); - sleep(0.05); - testLogout(tokens.accessToken); - sleep(0.05); + testLogout(); + sleep(0.1); } export function refreshOnly() { - const tokens = testLogin(); - sleep(0.05); - testRefresh(tokens.refreshToken); - sleep(0.05); + testRefresh(); + sleep(0.1); } // ── Summary Hook ───────────────────────────────────────────────────────────── export function handleSummary(data) { + // P2#5: Only evaluate metrics that have thresholds defined + const thresholdedMetrics = Object.entries(data.metrics).filter( + ([_, metric]) => metric && metric.thresholds && metric.thresholds.length > 0 + ); + + const passed = thresholdedMetrics.every(([_, metric]) => + metric.thresholds.every((t) => t.pass) + ); + return { 'stdout': `\n=== Darkwatch Auth Load Test Results ===\n`, 'summary.json': JSON.stringify({ @@ -285,9 +294,7 @@ export function handleSummary(data) { successRate: (data.metrics.refresh_success?.values.rate || 0) * 100 + '%', }, }, - passed: Object.entries(data.metrics).every( - ([_, metric]) => metric?.thresholds?.every?.((t) => t.pass) - ), + passed: passed, }, null, 2), }; }