2026-03-02 18:22:38 +00:00
|
|
|
/**
|
|
|
|
|
* Stress test: 1000 WebSocket concurrent connections (v0.951)
|
|
|
|
|
* Usage: k6 run loadtests/chat/stress_1000ws.js
|
|
|
|
|
* Requires: Backend API + Chat server running
|
|
|
|
|
* Prérequis: Mémoire serveur < 2GB pendant le test
|
|
|
|
|
*/
|
|
|
|
|
import ws from 'k6/ws';
|
|
|
|
|
import { check, sleep } from 'k6';
|
|
|
|
|
import { Rate, Trend, Counter } from 'k6/metrics';
|
|
|
|
|
import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
|
|
|
|
|
import http from 'k6/http';
|
|
|
|
|
|
|
|
|
|
const API_ORIGIN = __ENV.API_ORIGIN || __ENV.BASE_URL || 'http://localhost:8080';
|
2026-03-03 20:18:53 +00:00
|
|
|
const CHAT_ORIGIN = __ENV.CHAT_ORIGIN || 'ws://localhost:8080';
|
|
|
|
|
// Chat migrated to Go backend (v0.502) — WebSocket at /api/v1/ws
|
2026-03-02 18:22:38 +00:00
|
|
|
const WS_BASE = CHAT_ORIGIN.startsWith('ws') ? CHAT_ORIGIN : `ws://${CHAT_ORIGIN.replace(/^https?:\/\//, '')}`;
|
2026-03-03 20:18:53 +00:00
|
|
|
const WS_URL = `${WS_BASE.replace(/\/?$/, '')}/api/v1/ws`;
|
2026-03-02 18:22:38 +00:00
|
|
|
const TEST_EMAIL_PREFIX = __ENV.TEST_EMAIL_PREFIX || 'user+st1k';
|
|
|
|
|
const TEST_EMAIL_DOMAIN = __ENV.TEST_EMAIL_DOMAIN || 'example.com';
|
|
|
|
|
const TEST_PASSWORD_PREFIX = __ENV.TEST_PASSWORD_PREFIX || 'V3za!st1k-';
|
|
|
|
|
|
|
|
|
|
const wsConnectionTime = new Trend('ws_connection_time');
|
|
|
|
|
const wsConnectionFailures = new Rate('ws_connection_failures');
|
|
|
|
|
const wsMessageFailures = new Rate('ws_message_failures');
|
|
|
|
|
const messagesReceived = new Counter('messages_received');
|
|
|
|
|
const messagesSent = new Counter('messages_sent');
|
|
|
|
|
|
|
|
|
|
export const options = {
|
|
|
|
|
scenarios: {
|
|
|
|
|
websocket_stress: {
|
|
|
|
|
executor: 'ramping-vus',
|
|
|
|
|
stages: [
|
|
|
|
|
{ duration: '1m', target: 200 },
|
|
|
|
|
{ duration: '1m', target: 500 },
|
|
|
|
|
{ duration: '1m', target: 1000 },
|
|
|
|
|
{ duration: '5m', target: 1000 },
|
|
|
|
|
{ duration: '1m', target: 0 },
|
|
|
|
|
],
|
|
|
|
|
gracefulRampDown: '60s',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
thresholds: {
|
|
|
|
|
ws_connection_time: ['p(95)<1000', 'p(99)<2000'],
|
|
|
|
|
ws_connection_failures: ['rate<0.01'],
|
|
|
|
|
ws_message_failures: ['rate<0.01'],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function createAuthenticatedUser() {
|
|
|
|
|
const rand = randomString(8);
|
|
|
|
|
const pwd = `${TEST_PASSWORD_PREFIX}${rand}`;
|
|
|
|
|
const user = {
|
|
|
|
|
email: `${TEST_EMAIL_PREFIX}${rand}@${TEST_EMAIL_DOMAIN}`,
|
|
|
|
|
password: pwd,
|
|
|
|
|
password_confirmation: pwd,
|
|
|
|
|
username: `st1k${rand}`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const registerRes = http.post(`${API_ORIGIN}/api/v1/auth/register`, JSON.stringify(user), {
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
});
|
|
|
|
|
if (registerRes.status !== 201) return null;
|
|
|
|
|
|
|
|
|
|
const loginRes = http.post(`${API_ORIGIN}/api/v1/auth/login`, JSON.stringify({ email: user.email, password: user.password }), {
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
});
|
|
|
|
|
if (loginRes.status !== 200) return null;
|
|
|
|
|
|
|
|
|
|
let accessToken = '';
|
|
|
|
|
try {
|
|
|
|
|
const body = JSON.parse(loginRes.body);
|
|
|
|
|
accessToken = body.data?.token?.access_token || '';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (!accessToken) return null;
|
|
|
|
|
|
|
|
|
|
const chatTokenRes = http.get(`${API_ORIGIN}/api/v1/chat/token`, {
|
|
|
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
|
|
|
});
|
|
|
|
|
if (chatTokenRes.status !== 200) return null;
|
|
|
|
|
|
|
|
|
|
let chatToken = '';
|
|
|
|
|
try {
|
|
|
|
|
const ctBody = JSON.parse(chatTokenRes.body);
|
|
|
|
|
chatToken = ctBody.data?.token || ctBody.token || '';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (!chatToken) return null;
|
|
|
|
|
|
|
|
|
|
return { ...user, chatToken };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function setup() {
|
|
|
|
|
const users = [];
|
|
|
|
|
for (let i = 0; i < 1100; i++) {
|
|
|
|
|
const user = createAuthenticatedUser();
|
|
|
|
|
if (user) users.push(user);
|
|
|
|
|
if (i % 50 === 0) sleep(0.05);
|
|
|
|
|
}
|
|
|
|
|
return { users };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function (data) {
|
|
|
|
|
const { users } = data;
|
|
|
|
|
if (users.length === 0) return;
|
|
|
|
|
|
|
|
|
|
const user = users[Math.floor(Math.random() * users.length)];
|
|
|
|
|
const url = `${WS_URL}?token=${user.chatToken}`;
|
|
|
|
|
const connectionStart = Date.now();
|
|
|
|
|
|
|
|
|
|
const res = ws.connect(url, {}, function (socket) {
|
|
|
|
|
socket.on('open', () => {
|
|
|
|
|
wsConnectionTime.add(Date.now() - connectionStart);
|
|
|
|
|
|
|
|
|
|
socket.send(JSON.stringify({ type: 'join', room: 'general' }));
|
|
|
|
|
|
|
|
|
|
socket.setInterval(() => {
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: 'message',
|
|
|
|
|
room: 'general',
|
|
|
|
|
content: `Stress ${Date.now()}`,
|
|
|
|
|
}));
|
|
|
|
|
messagesSent.add(1);
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
|
|
|
|
socket.setTimeout(() => {
|
|
|
|
|
socket.close();
|
|
|
|
|
}, 300000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('message', (data) => {
|
|
|
|
|
messagesReceived.add(1);
|
|
|
|
|
try {
|
|
|
|
|
const msg = JSON.parse(data);
|
|
|
|
|
if (msg.error) wsMessageFailures.add(1);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
wsMessageFailures.add(1);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('error', () => {
|
|
|
|
|
wsConnectionFailures.add(1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
check(res, { 'WebSocket connection successful': (r) => r && r.status === 101 });
|
|
|
|
|
if (!res || res.status !== 101) {
|
|
|
|
|
wsConnectionFailures.add(1);
|
|
|
|
|
}
|
|
|
|
|
}
|