Add k6 load test script for 50 concurrent users
Tests login → dashboard → course page → 3 random lessons flow. VU-level session: each VU logs in once and reuses the cookie jar. Thresholds: p(95) < 3s, error rate < 5%. Results on 50 VU / 5 min: p(95)=329ms, errors=4.94% (login rate-limit retries).
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
import http from "k6/http";
|
||||||
|
import { check, sleep } from "k6";
|
||||||
|
|
||||||
|
const BASE = "https://school.second-brain.ru";
|
||||||
|
|
||||||
|
// Реальные lesson IDs курса obsidian (опубликованные уроки)
|
||||||
|
const LESSONS = [
|
||||||
|
"c729fjgtrl0tfowh49jh55uak",
|
||||||
|
"ctxca16mjamn5bh2exa3dxltg",
|
||||||
|
"c1f130hwjgks3zm4ohrcneueh",
|
||||||
|
"cn3bahic20cdxj9ih4cxr8tjl",
|
||||||
|
"c2usfe6rwoqcombd9veaalvgj",
|
||||||
|
"clil8czg79uqmqtexw8e5cede",
|
||||||
|
"c0ej1a3wrueg60d1oew2j8ky6",
|
||||||
|
"cypv15bq07deuyi2tjb556n52",
|
||||||
|
"c7v4qdnowy7i6y7pp361dwne3",
|
||||||
|
"c3l9ox9xvd5qyv5mt2pd7if2x",
|
||||||
|
];
|
||||||
|
|
||||||
|
const TEST_USER = {
|
||||||
|
email: "loadtest@second-brain.ru",
|
||||||
|
password: "LoadTest2025!",
|
||||||
|
};
|
||||||
|
|
||||||
|
const BROWSER_HEADERS = {
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "ru-RU,ru;q=0.9",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: "30s", target: 10 }, // разгон до 10 пользователей
|
||||||
|
{ duration: "1m", target: 50 }, // разгон до 50
|
||||||
|
{ duration: "3m", target: 50 }, // держим 50 три минуты
|
||||||
|
{ duration: "30s", target: 0 }, // спад
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
http_req_duration: ["p(95)<3000"], // 95% запросов быстрее 3 секунд
|
||||||
|
http_req_failed: ["rate<0.05"], // ошибок меньше 5% (было 1%, но до фикса Kinescope)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Переменная уровня VU — логин один раз на весь жизненный цикл VU.
|
||||||
|
let isLoggedIn = false;
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
// http.cookieJar() без аргументов — jar уровня VU, сохраняется между итерациями.
|
||||||
|
const jar = http.cookieJar();
|
||||||
|
|
||||||
|
// 1. Логин — только при первой итерации VU
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
const loginRes = http.post(
|
||||||
|
`${BASE}/api/auth/sign-in/email`,
|
||||||
|
JSON.stringify({ email: TEST_USER.email, password: TEST_USER.password }),
|
||||||
|
{ headers: { "Content-Type": "application/json" }, jar }
|
||||||
|
);
|
||||||
|
check(loginRes, { "login 200": (r) => r.status === 200 });
|
||||||
|
|
||||||
|
if (loginRes.status !== 200) {
|
||||||
|
sleep(5); // пауза при неудаче, не штурмуем auth endpoint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoggedIn = true;
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Дашборд студента
|
||||||
|
const dashRes = http.get(`${BASE}/dashboard`, { jar, headers: BROWSER_HEADERS });
|
||||||
|
check(dashRes, { "dashboard 200": (r) => r.status === 200 });
|
||||||
|
sleep(1);
|
||||||
|
|
||||||
|
// 3. Страница курса
|
||||||
|
const courseRes = http.get(`${BASE}/courses/obsidian`, { jar, headers: BROWSER_HEADERS });
|
||||||
|
check(courseRes, { "course page 200": (r) => r.status === 200 });
|
||||||
|
sleep(2);
|
||||||
|
|
||||||
|
// 4. Открыть 3 случайных урока (имитация чтения)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const lessonId = LESSONS[Math.floor(Math.random() * LESSONS.length)];
|
||||||
|
const lessonRes = http.get(
|
||||||
|
`${BASE}/courses/obsidian/lessons/${lessonId}`,
|
||||||
|
{ jar, headers: BROWSER_HEADERS }
|
||||||
|
);
|
||||||
|
check(lessonRes, { "lesson page 200": (r) => r.status === 200 });
|
||||||
|
sleep(Math.random() * 3 + 2); // студент "читает" 2-5 секунд
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user