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); }