Fix k6 load test: 1-call/iteration, credential pool, merged scenarios, logout API contract, summary thresholds (FRE-4928)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -8,6 +8,7 @@ const TEST_EMAIL = __ENV.TEST_EMAIL || 'loadtest@darkwatch.shieldai';
|
|||||||
const TEST_PASSWORD = __ENV.TEST_PASSWORD || 'LoadTest2026!';
|
const TEST_PASSWORD = __ENV.TEST_PASSWORD || 'LoadTest2026!';
|
||||||
const DURATION = __ENV.DURATION || '300s'; // 5 minutes
|
const DURATION = __ENV.DURATION || '300s'; // 5 minutes
|
||||||
const TARGET_RPS = parseInt(__ENV.TARGET_RPS || '500', 10);
|
const TARGET_RPS = parseInt(__ENV.TARGET_RPS || '500', 10);
|
||||||
|
const CREDENTIAL_POOL_SIZE = parseInt(__ENV.CREDENTIAL_POOL_SIZE || '100', 10);
|
||||||
|
|
||||||
// P99 latency thresholds (ms)
|
// P99 latency thresholds (ms)
|
||||||
const THRESHOLDS = {
|
const THRESHOLDS = {
|
||||||
@@ -38,13 +39,27 @@ const authHeaders = {
|
|||||||
'Content-Type': 'application/json',
|
'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) ──────────────────────────────────────
|
// ── Scenario: Login (POST /auth/login) ──────────────────────────────────────
|
||||||
function testLogin() {
|
function testLogin(email, password) {
|
||||||
const uniqueEmail = `${TEST_EMAIL.replace('@', `_${uuidv4().slice(0, 8)}@`)}`;
|
const creds = email
|
||||||
|
? { email, password }
|
||||||
|
: credentialPool[Math.floor(Math.random() * credentialPool.length)];
|
||||||
|
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
email: uniqueEmail,
|
email: creds.email,
|
||||||
password: TEST_PASSWORD,
|
password: creds.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = http.post(`${BASE_URL}/auth/login`, payload, { headers: authHeaders });
|
const res = http.post(`${BASE_URL}/auth/login`, payload, { headers: authHeaders });
|
||||||
@@ -85,7 +100,7 @@ function testLogin() {
|
|||||||
|
|
||||||
// ── Scenario: Refresh (POST /auth/refresh) ──────────────────────────────────
|
// ── Scenario: Refresh (POST /auth/refresh) ──────────────────────────────────
|
||||||
function testRefresh(refreshToken) {
|
function testRefresh(refreshToken) {
|
||||||
const token = refreshToken || uuidv4();
|
const token = refreshToken || tokenPool[Math.floor(Math.random() * tokenPool.length)].refreshToken;
|
||||||
|
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
refresh_token: token,
|
refresh_token: token,
|
||||||
@@ -125,12 +140,14 @@ function testRefresh(refreshToken) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scenario: Logout (POST /auth/logout) ────────────────────────────────────
|
// ── P2#4: Scenario: Logout (POST /auth/logout) — refresh_token in body, Bearer in header ──
|
||||||
function testLogout(accessToken) {
|
function testLogout(accessToken, refreshToken) {
|
||||||
const token = accessToken || uuidv4();
|
const poolEntry = tokenPool[Math.floor(Math.random() * tokenPool.length)];
|
||||||
|
const token = accessToken || poolEntry.accessToken;
|
||||||
|
const refreshTkn = refreshToken || poolEntry.refreshToken;
|
||||||
|
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
access_token: token,
|
refresh_token: refreshTkn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = http.post(`${BASE_URL}/auth/logout`, payload, {
|
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 = {
|
export const options = {
|
||||||
scenarios: {
|
scenarios: {
|
||||||
sustained_load: {
|
sustained_load: {
|
||||||
@@ -164,6 +181,36 @@ export const options = {
|
|||||||
exec: 'mixedWorkload',
|
exec: 'mixedWorkload',
|
||||||
tags: { scenario: 'sustained_load' },
|
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: {
|
thresholds: {
|
||||||
`login_p99`: [`p(99)<${THRESHOLDS.login}`],
|
`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() {
|
export function mixedWorkload() {
|
||||||
const rand = Math.random();
|
const rand = Math.random();
|
||||||
|
|
||||||
if (rand < 0.4) {
|
if (rand < 0.4) {
|
||||||
const tokens = testLogin();
|
testLogin();
|
||||||
sleep(0.05);
|
|
||||||
testRefresh(tokens.refreshToken);
|
|
||||||
sleep(0.02);
|
|
||||||
testLogout(tokens.accessToken);
|
|
||||||
} else if (rand < 0.75) {
|
} else if (rand < 0.75) {
|
||||||
const tokens = testLogin();
|
testRefresh();
|
||||||
sleep(0.05);
|
|
||||||
testRefresh(tokens.refreshToken);
|
|
||||||
} else {
|
} else {
|
||||||
const tokens = testLogin();
|
testLogout();
|
||||||
sleep(0.05);
|
|
||||||
testLogout(tokens.accessToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Individual endpoint scenarios for targeted testing ───────────────────────
|
// Individual endpoint scenarios — each makes exactly 1 HTTP call per iteration
|
||||||
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() {
|
export function loginOnly() {
|
||||||
testLogin();
|
testLogin();
|
||||||
sleep(0.1);
|
sleep(0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logoutOnly() {
|
export function logoutOnly() {
|
||||||
const tokens = testLogin();
|
testLogout();
|
||||||
sleep(0.05);
|
sleep(0.1);
|
||||||
testLogout(tokens.accessToken);
|
|
||||||
sleep(0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshOnly() {
|
export function refreshOnly() {
|
||||||
const tokens = testLogin();
|
testRefresh();
|
||||||
sleep(0.05);
|
sleep(0.1);
|
||||||
testRefresh(tokens.refreshToken);
|
|
||||||
sleep(0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Summary Hook ─────────────────────────────────────────────────────────────
|
// ── Summary Hook ─────────────────────────────────────────────────────────────
|
||||||
export function handleSummary(data) {
|
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 {
|
return {
|
||||||
'stdout': `\n=== Darkwatch Auth Load Test Results ===\n`,
|
'stdout': `\n=== Darkwatch Auth Load Test Results ===\n`,
|
||||||
'summary.json': JSON.stringify({
|
'summary.json': JSON.stringify({
|
||||||
@@ -285,9 +294,7 @@ export function handleSummary(data) {
|
|||||||
successRate: (data.metrics.refresh_success?.values.rate || 0) * 100 + '%',
|
successRate: (data.metrics.refresh_success?.values.rate || 0) * 100 + '%',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
passed: Object.entries(data.metrics).every(
|
passed: passed,
|
||||||
([_, metric]) => metric?.thresholds?.every?.((t) => t.pass)
|
|
||||||
),
|
|
||||||
}, null, 2),
|
}, null, 2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user