diff --git a/load-test.js b/load-test.js new file mode 100644 index 0000000..a314624 --- /dev/null +++ b/load-test.js @@ -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 секунд + } +}