221 lines
5.6 KiB
JavaScript
221 lines
5.6 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 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,
|
|
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 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 /' } });
|
|
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: { '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' },
|
|
});
|
|
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' },
|
|
});
|
|
|
|
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' },
|
|
});
|
|
closeCalls.add(1);
|
|
check(closeRes, {
|
|
'close status 200': (r) => r.status === 200,
|
|
});
|
|
}
|
|
|
|
flowErrors.add(0);
|
|
sleep(THINK_TIME);
|
|
}
|