Files
Stend_mont/scripts/load/portal_k6.js
T

235 lines
5.9 KiB
JavaScript

import http from 'k6/http';
import { check, fail, sleep } from 'k6';
import exec from 'k6/execution';
import { Counter, Rate, Trend } from 'k6/metrics';
const BASE_URL = (__ENV.BASE_URL || 'https://stend.4mont.ru').replace(/\/$/, '');
const HOST_HEADER = (__ENV.HOST_HEADER || '').trim();
const SERVICE_SLUGS = (__ENV.SERVICE_SLUGS || 'vmmanager')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const HEARTBEATS = Number(__ENV.HEARTBEATS || 3);
const THINK_TIME = Number(__ENV.THINK_TIME || 1);
const CLOSE_SESSION = (__ENV.CLOSE_SESSION || '1') !== '0';
const PROFILE = (__ENV.PROFILE || 'smoke').toLowerCase();
const usersFromCsv = parseUsersCsv(__ENV.USERS_CSV || '');
const singleUser = {
username: __ENV.USERNAME || '',
password: __ENV.PASSWORD || '',
};
const openAttempts = new Counter('open_attempts');
const openSuccess = new Counter('open_success');
const openRejected = new Counter('open_rejected');
const limitRedirects = new Counter('limit_redirects');
const touchRejected = new Counter('touch_rejected');
const closeCalls = new Counter('close_calls');
const loginFailures = new Counter('login_failures');
const flowErrors = new Rate('flow_errors');
const openLatency = new Trend('open_latency_ms');
const PROFILE_OPTIONS = {
smoke: {
vus: 5,
duration: '1m',
},
load: {
vus: 30,
duration: '10m',
},
stress: {
stages: [
{ duration: '2m', target: 30 },
{ duration: '5m', target: 80 },
{ duration: '2m', target: 0 },
],
},
};
const selected = PROFILE_OPTIONS[PROFILE] || PROFILE_OPTIONS.smoke;
export const options = {
...selected,
noCookiesReset: true,
insecureSkipTLSVerify: (__ENV.INSECURE_TLS || '0') === '1',
thresholds: {
http_req_failed: ['rate<0.02'],
http_req_duration: ['p(95)<2000'],
flow_errors: ['rate<0.05'],
},
};
let loggedInKey = '';
function withHostHeaders(headers = {}) {
if (!HOST_HEADER) return headers;
return { ...headers, Host: HOST_HEADER };
}
function parseUsersCsv(raw) {
if (!raw.trim()) return [];
return raw
.split(';')
.map((pair) => pair.trim())
.filter(Boolean)
.map((pair) => {
const idx = pair.indexOf(':');
if (idx <= 0) return null;
return {
username: pair.slice(0, idx).trim(),
password: pair.slice(idx + 1).trim(),
};
})
.filter((u) => u && u.username && u.password);
}
function currentCredentials() {
if (usersFromCsv.length > 0) {
const vu = exec.vu.idInTest || 1;
return usersFromCsv[(vu - 1) % usersFromCsv.length];
}
return singleUser;
}
function ensureLoggedIn() {
const creds = currentCredentials();
if (!creds.username || !creds.password) {
fail('Set USERNAME/PASSWORD or USERS_CSV (username:password;username2:password2)');
}
const userKey = `${creds.username}:${creds.password}`;
if (loggedInKey === userKey) return creds;
const jar = http.cookieJar();
jar.clear(BASE_URL);
const landing = http.get(`${BASE_URL}/`, {
redirects: 0,
tags: { name: 'GET /' },
headers: withHostHeaders(),
});
const csrfFromCookie = (jar.cookiesForURL(BASE_URL).csrf_token || [])[0] || '';
const csrf = csrfFromCookie || ((landing.cookies.csrf_token || [])[0] || {}).value || '';
if (!csrf) {
loginFailures.add(1);
fail('CSRF cookie not found on GET /');
}
const payload = [
`username=${encodeURIComponent(creds.username)}`,
`password=${encodeURIComponent(creds.password)}`,
`csrf_token=${encodeURIComponent(csrf)}`,
].join('&');
const loginRes = http.post(`${BASE_URL}/login`, payload, {
redirects: 0,
headers: withHostHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
tags: { name: 'POST /login' },
});
const ok = check(loginRes, {
'login status is redirect': (r) => r.status === 303,
});
const hasAuth = (jar.cookiesForURL(BASE_URL).portal_auth || []).length > 0;
if (!ok || !hasAuth) {
loginFailures.add(1);
fail(`Login failed for ${creds.username}: status=${loginRes.status}`);
}
loggedInKey = userKey;
return creds;
}
function pickSlug() {
return SERVICE_SLUGS[Math.floor(Math.random() * SERVICE_SLUGS.length)];
}
function extractSessionId(location) {
const match = (location || '').match(/\/s\/([0-9a-fA-F-]{36})\//);
return match ? match[1] : '';
}
export default function () {
ensureLoggedIn();
const slug = pickSlug();
openAttempts.add(1);
const openRes = http.get(`${BASE_URL}/go/${slug}`, {
redirects: 0,
tags: { name: 'GET /go/:slug' },
headers: withHostHeaders(),
});
openLatency.add(openRes.timings.duration);
const location = openRes.headers.Location || '';
const sessionId = extractSessionId(location);
const opened = check(openRes, {
'open returns redirect': (r) => r.status === 303,
});
if (!opened) {
openRejected.add(1);
flowErrors.add(1);
sleep(THINK_TIME);
return;
}
if (location.includes('launch_error=max_services')) {
limitRedirects.add(1);
flowErrors.add(1);
sleep(THINK_TIME);
return;
}
if (!sessionId) {
openRejected.add(1);
flowErrors.add(1);
sleep(THINK_TIME);
return;
}
openSuccess.add(1);
for (let i = 0; i < HEARTBEATS; i += 1) {
const touchRes = http.post(`${BASE_URL}/api/sessions/${sessionId}/touch`, null, {
redirects: 0,
tags: { name: 'POST /api/sessions/:id/touch' },
headers: withHostHeaders(),
});
if (touchRes.status === 410) {
touchRejected.add(1);
break;
}
const touchOk = check(touchRes, {
'touch status 200': (r) => r.status === 200,
});
if (!touchOk) {
flowErrors.add(1);
break;
}
sleep(0.5);
}
if (CLOSE_SESSION) {
const closeRes = http.post(`${BASE_URL}/api/sessions/${sessionId}/close`, null, {
redirects: 0,
tags: { name: 'POST /api/sessions/:id/close' },
headers: withHostHeaders(),
});
closeCalls.add(1);
check(closeRes, {
'close status 200': (r) => r.status === 200,
});
}
flowErrors.add(0);
sleep(THINK_TIME);
}