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'; // Load environment variables const API_ORIGIN = __ENV.API_ORIGIN || 'https://api.lab.veza'; const CHAT_ORIGIN = __ENV.CHAT_ORIGIN || 'wss://chat.lab.veza'; const TEST_EMAIL_PREFIX = __ENV.TEST_EMAIL_PREFIX || 'user+ws'; const TEST_EMAIL_DOMAIN = __ENV.TEST_EMAIL_DOMAIN || 'lab.veza'; const TEST_PASSWORD_PREFIX = __ENV.TEST_PASSWORD_PREFIX || 'V3za!ws-'; // Custom metrics const wsConnectionTime = new Trend('ws_connection_time'); const wsMessageSendTime = new Trend('ws_message_send_time'); const wsMessageReceiveTime = new Trend('ws_message_receive_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'); // Test configuration export const options = { scenarios: { websocket_chat: { executor: 'ramping-vus', stages: [ { duration: '30s', target: 10 }, // Ramp up to 10 connections { duration: '2m', target: 50 }, // Ramp up to 50 connections { duration: '2m', target: 50 }, // Stay at 50 connections { duration: '30s', target: 0 }, // Ramp down ], gracefulRampDown: '30s', }, }, thresholds: { ws_connection_time: ['p(95)<500', 'p(99)<1000'], ws_message_send_time: ['p(95)<100', 'p(99)<200'], ws_connection_failures: ['rate<0.05'], // Less than 5% connection failures ws_message_failures: ['rate<0.01'], // Less than 1% message failures }, }; // Helper to create and authenticate a user function createAuthenticatedUser() { const user = { email: `${TEST_EMAIL_PREFIX}${randomString(8)}@${TEST_EMAIL_DOMAIN}`, password: `${TEST_PASSWORD_PREFIX}${randomString(8)}`, }; // Register user const registerRes = http.post(`${API_ORIGIN}/auth/register`, JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }); if (registerRes.status !== 201) { console.error(`Failed to register user: ${registerRes.status}`); return null; } try { const body = JSON.parse(registerRes.body); return { ...user, accessToken: body.access_token, userId: body.user.id, }; } catch (e) { console.error('Failed to parse registration response'); return null; } } // Setup: Create test users export function setup() { const users = []; const userCount = 60; // Create more than we need console.log(`Creating ${userCount} test users for WebSocket test...`); for (let i = 0; i < userCount; i++) { const user = createAuthenticatedUser(); if (user) { users.push(user); } sleep(0.1); // Small delay to avoid overwhelming the API } console.log(`Created ${users.length} authenticated users`); return { users }; } export default function (data) { const { users } = data; // Pick a random user const user = users[Math.floor(Math.random() * users.length)]; if (!user) { console.error('No user available'); return; } const url = `${CHAT_ORIGIN}?token=${user.accessToken}`; const params = { tags: { user: user.email } }; let connectionStartTime = new Date(); let connected = false; let messageCount = 0; let lastMessageTime = new Date(); const res = ws.connect(url, params, function (socket) { socket.on('open', () => { const connectionEndTime = new Date(); wsConnectionTime.add(connectionEndTime - connectionStartTime); connected = true; console.log(`User ${user.email} connected to WebSocket`); // Join general room socket.send(JSON.stringify({ type: 'join', room: 'general', })); // Send periodic messages socket.setInterval(() => { if (connected) { const message = { type: 'message', room: 'general', content: `Test message from ${user.email} at ${new Date().toISOString()}`, }; const sendStart = new Date(); socket.send(JSON.stringify(message)); messagesSent.add(1); // Record send time (approximation) wsMessageSendTime.add(new Date() - sendStart); } }, 5000); // Send message every 5 seconds // Send ping every 30 seconds to keep connection alive socket.setInterval(() => { if (connected) { socket.send(JSON.stringify({ type: 'ping' })); } }, 30000); }); socket.on('message', (data) => { const receiveTime = new Date(); messagesReceived.add(1); messageCount++; try { const message = JSON.parse(data); // Calculate receive time since last message wsMessageReceiveTime.add(receiveTime - lastMessageTime); lastMessageTime = receiveTime; // Check message validity check(message, { 'message has type': (m) => m.type !== undefined, 'message is valid': (m) => m.error === undefined, }); // Handle different message types switch (message.type) { case 'joined': console.log(`User ${user.email} joined room`); break; case 'message': // Received a chat message break; case 'error': console.error(`WebSocket error for ${user.email}: ${message.message}`); wsMessageFailures.add(1); break; } } catch (e) { console.error(`Failed to parse message: ${data}`); wsMessageFailures.add(1); } }); socket.on('close', () => { connected = false; console.log(`User ${user.email} disconnected. Received ${messageCount} messages`); }); socket.on('error', (e) => { console.error(`WebSocket error for ${user.email}: ${e}`); wsConnectionFailures.add(1); }); // Simulate user activity for 30-60 seconds const activityDuration = 30 + Math.random() * 30; socket.setTimeout(() => { console.log(`User ${user.email} ending session after ${activityDuration}s`); socket.close(); }, activityDuration * 1000); }); // Check connection result check(res, { 'WebSocket connection successful': (r) => r && r.status === 101, }); if (!res || res.status !== 101) { wsConnectionFailures.add(1); } } // Teardown export function teardown(data) { console.log('WebSocket chat test completed'); console.log(`Total users created: ${data.users.length}`); }