diff --git a/test/performance/auth/login.stress.js b/test/performance/auth/login.stress.js
index a21f56a0..8b46d603 100644
--- a/test/performance/auth/login.stress.js
+++ b/test/performance/auth/login.stress.js
@@ -5,8 +5,8 @@ export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 50 },
- { duration: '30s', target: 100 },
- { duration: '1m', target: 100 },
+ { duration: '30s', target: 600 },
+ { duration: '1m', target: 600 },
{ duration: '30s', target: 0 },
],
thresholds: {
diff --git a/test/performance/auth/report_login.html b/test/performance/auth/report_login.html
new file mode 100644
index 00000000..39396a90
--- /dev/null
+++ b/test/performance/auth/report_login.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/auth/report_reset-password.html b/test/performance/auth/report_reset-password.html
new file mode 100644
index 00000000..3ef8a3bb
--- /dev/null
+++ b/test/performance/auth/report_reset-password.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/auth/reset-password.stress.js b/test/performance/auth/reset-password.stress.js
index 0e927904..0242a07f 100644
--- a/test/performance/auth/reset-password.stress.js
+++ b/test/performance/auth/reset-password.stress.js
@@ -5,8 +5,8 @@ export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 50 },
- { duration: '30s', target: 100 },
- { duration: '1m', target: 100 },
+ { duration: '30s', target: 200 },
+ { duration: '1m', target: 400 },
{ duration: '30s', target: 0 },
],
thresholds: {
@@ -18,16 +18,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3001';
-
-export default function () {
- const params = {
- headers: {
- 'Content-Type': 'application/json',
- 'X-Client-Type': 'web'
- },
- };
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
+export function setup() {
const resCreate = http.post(
`${STRESS_TEST_URL}/test/users`,
);
@@ -39,6 +32,17 @@ export default function () {
const userData = resCreate.json('data');
const userEmail = userData.email;
+ return { userEmail };
+}
+
+export default function (data) {
+ const { userEmail } = data;
+ const params = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web'
+ },
+ };
const payloadForgot = JSON.stringify({
identifier: userEmail,
diff --git a/test/performance/auth/signup.stress.js b/test/performance/auth/signup.stress.js
index 6615cc14..d57315d9 100644
--- a/test/performance/auth/signup.stress.js
+++ b/test/performance/auth/signup.stress.js
@@ -5,8 +5,8 @@ export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 50 },
- { duration: '30s', target: 100 },
- { duration: '1m', target: 100 },
+ { duration: '30s', target: 200 },
+ { duration: '1m', target: 400 },
{ duration: '30s', target: 0 },
],
thresholds: {
@@ -22,7 +22,7 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
export default function () {
const uniqueId = `${__VU}-${__ITER}-${Date.now()}`;
diff --git a/test/performance/media/media-upload.stress.js b/test/performance/media/media-upload.stress.js
new file mode 100644
index 00000000..13b5da15
--- /dev/null
+++ b/test/performance/media/media-upload.stress.js
@@ -0,0 +1,192 @@
+import http from 'k6/http';
+import { check, sleep, group } from 'k6';
+import encoding from 'k6/encoding';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 50 }, // Ramp up to 50 users
+ { duration: '1m', target: 100 }, // Ramp up to 100 users
+ { duration: '30s', target: 200 }, // Ramp up to 200 users
+ { duration: '2m', target: 200 }, // Sustain at 200 users
+ { duration: '30s', target: 0 }, // Ramp down
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.05'], // Allow up to 5% failure rate for media uploads (I/O heavy)
+ 'http_req_duration{name:01_Login_Action}': ['p(95)<3000'],
+ 'http_req_duration{name:02_Upload_Image}': ['p(95)<10000'], // Image uploads can take longer
+ 'http_req_duration{name:03_Upload_Video}': ['p(95)<15000'], // Video uploads can take longer
+ 'http_req_duration{name:04_Upload_Gif}': ['p(95)<5000'], // GIF (Tenor lookup) should be faster
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+
+// Minimal valid PNG file (1x1 red pixel) - properly encoded
+const MINIMAL_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12P4z8DwHwAFAAH/q842AAAAAElFTkSuQmCC';
+
+// Minimal valid MP4 bytes - ftyp box only (smallest valid MP4 structure)
+// We'll create this as raw bytes array instead of base64 to avoid encoding issues
+function createMinimalMP4Bytes() {
+ // Minimal ftyp box: box size (4) + box type (4) + brand (4) + version (4)
+ const bytes = new Uint8Array([
+ 0x00, 0x00, 0x00, 0x14, // box size: 20 bytes
+ 0x66, 0x74, 0x79, 0x70, // box type: 'ftyp'
+ 0x69, 0x73, 0x6F, 0x6D, // brand: 'isom'
+ 0x00, 0x00, 0x00, 0x01, // version: 1
+ 0x69, 0x73, 0x6F, 0x6D, // compatible brand: 'isom'
+ ]);
+ return bytes.buffer; // Return ArrayBuffer, not Uint8Array
+}
+
+/**
+ * Helper function to create and login a user
+ * Returns { accessToken } or null on failure
+ */
+function createAndLoginUser(tagName) {
+ const resCreateUser = http.post(`${STRESS_TEST_URL}/test/users`);
+
+ if (!check(resCreateUser, { 'User Created 201': (r) => r.status === 201 })) {
+ console.error(`Setup Failed (Create User): ${resCreateUser.body}`);
+ return null;
+ }
+
+ const userData = resCreateUser.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword,
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: tagName },
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, { 'Login 200': (r) => r.status === 200 })) {
+ console.error(`Setup Failed (Login): ${resLogin.body}`);
+ return null;
+ }
+
+ return {
+ accessToken: resLogin.json('data.accessToken'),
+ };
+}
+
+export default function () {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // SETUP: Create and login user
+ // ─────────────────────────────────────────────────────────────────────────────
+ const user = createAndLoginUser('01_Login_Action');
+ if (!user) return;
+
+ const authHeaders = {
+ 'X-Client-Type': 'web',
+ Authorization: `Bearer ${user.accessToken}`,
+ };
+
+ sleep(0.2);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 1: Upload Image (POST /media/upload/image)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Image Upload', function () {
+ const imageBytes = encoding.b64decode(MINIMAL_PNG_BASE64);
+
+ const formData = {
+ file: http.file(imageBytes, `test-image-${__VU}-${__ITER}.png`, 'image/png'),
+ folder: 'tweets',
+ };
+
+ const resUploadImage = http.post(
+ `${STRESS_TEST_URL}/media/upload/image`,
+ formData,
+ {
+ headers: authHeaders,
+ tags: { name: '02_Upload_Image' },
+ }
+ );
+
+ const imageUploadOk = check(resUploadImage, {
+ 'Upload Image 2xx': (r) => r.status >= 200 && r.status < 300,
+ });
+
+ if (!imageUploadOk) {
+ console.error(`Upload Image Failed [VU:${__VU}]: ${resUploadImage.status} - ${resUploadImage.body}`);
+ }
+ });
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 2: Upload Video (POST /media/upload/video)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Video Upload', function () {
+ const videoBytes = createMinimalMP4Bytes();
+
+ const formData = {
+ file: http.file(videoBytes, `test-video-${__VU}-${__ITER}.mp4`, 'video/mp4'),
+ folder: 'tweets',
+ };
+
+ const resUploadVideo = http.post(
+ `${STRESS_TEST_URL}/media/upload/video`,
+ formData,
+ {
+ headers: authHeaders,
+ tags: { name: '03_Upload_Video' },
+ }
+ );
+
+ const videoUploadOk = check(resUploadVideo, {
+ 'Upload Video 2xx': (r) => r.status >= 200 && r.status < 300,
+ });
+
+ if (!videoUploadOk) {
+ console.error(`Upload Video Failed [VU:${__VU}]: ${resUploadVideo.status} - ${resUploadVideo.body}`);
+ }
+ });
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 3: Upload GIF via Tenor ID (POST /media/upload/gif)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('GIF Upload', function () {
+ // Note: This requires a valid Tenor GIF ID
+ // Using a commonly available GIF ID - may need to be updated if Tenor API changes
+ const tenorId = '16989471141791455574';
+
+ const gifPayload = JSON.stringify({
+ tenorId: tenorId,
+ });
+
+ const resUploadGif = http.post(
+ `${STRESS_TEST_URL}/media/upload/gif`,
+ gifPayload,
+ {
+ headers: {
+ ...authHeaders,
+ 'Content-Type': 'application/json',
+ },
+ tags: { name: '04_Upload_Gif' },
+ }
+ );
+
+ const gifUploadOk = check(resUploadGif, {
+ 'Upload GIF 2xx': (r) => r.status >= 200 && r.status < 300,
+ });
+
+ if (!gifUploadOk) {
+ console.error(`Upload GIF Failed [VU:${__VU}]: ${resUploadGif.status} - ${resUploadGif.body}`);
+ }
+ });
+
+ sleep(0.2);
+}
diff --git a/test/performance/media/report.html b/test/performance/media/report.html
new file mode 100644
index 00000000..faecda6d
--- /dev/null
+++ b/test/performance/media/report.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/notifications/notifications.stress.js b/test/performance/notifications/notifications.stress.js
new file mode 100644
index 00000000..0da20006
--- /dev/null
+++ b/test/performance/notifications/notifications.stress.js
@@ -0,0 +1,270 @@
+import http from 'k6/http';
+import { check, sleep, group } from 'k6';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 50 }, // Ramp up to 50 users
+ { duration: '1m', target: 100 }, // Ramp up to 100 users
+ { duration: '30s', target: 300 }, // Ramp up to 300 users
+ { duration: '2m', target: 300 }, // Sustain at 300 users
+ { duration: '30s', target: 0 }, // Ramp down
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.01'],
+ 'http_req_duration{name:01_Login_UserA}': ['p(95)<2000'],
+ 'http_req_duration{name:02_Login_UserB}': ['p(95)<2000'],
+ 'http_req_duration{name:03_UserA_Create_Tweet}': ['p(95)<2000'],
+ 'http_req_duration{name:04_UserB_Follow_UserA}': ['p(95)<1000'],
+ 'http_req_duration{name:05_UserB_Like_Tweet}': ['p(95)<500'],
+ 'http_req_duration{name:06_Get_Notifications}': ['p(95)<1000'],
+ 'http_req_duration{name:07_Get_Notifications_Page2}': ['p(95)<1000'],
+ 'http_req_duration{name:08_Get_Unseen_Count}': ['p(95)<500'],
+ 'http_req_duration{name:09_Mark_Single_Seen}': ['p(95)<500'],
+ 'http_req_duration{name:10_Mark_All_Seen}': ['p(95)<500'],
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+
+/**
+ * Helper function to create and login a user
+ * Returns { accessToken, userId, username } or null on failure
+ */
+function createAndLoginUser(userLabel, tagName) {
+ const resCreateUser = http.post(`${STRESS_TEST_URL}/test/users`);
+
+ if (!check(resCreateUser, { [`${userLabel} Created 201`]: (r) => r.status === 201 })) {
+ console.error(`Setup Failed (Create ${userLabel}): ${resCreateUser.body}`);
+ return null;
+ }
+
+ const userData = resCreateUser.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+ const username = userData.username; // Get username from /test/users response
+
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword,
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: tagName },
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, { [`${userLabel} Login 200`]: (r) => r.status === 200 })) {
+ console.error(`Setup Failed (Login ${userLabel}): ${resLogin.body}`);
+ return null;
+ }
+
+ return {
+ accessToken: resLogin.json('data.accessToken'),
+ username: username, // Use username from /test/users response
+ };
+}
+
+export default function () {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // SETUP: Create and login User A (will receive notifications)
+ // ─────────────────────────────────────────────────────────────────────────────
+ const userA = createAndLoginUser('UserA', '01_Login_UserA');
+ if (!userA) return;
+
+ const authParamsA = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ Authorization: `Bearer ${userA.accessToken}`,
+ },
+ };
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // SETUP: Create and login User B (will trigger notifications for User A)
+ // ─────────────────────────────────────────────────────────────────────────────
+ const userB = createAndLoginUser('UserB', '02_Login_UserB');
+ if (!userB) return;
+
+ const authParamsB = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ Authorization: `Bearer ${userB.accessToken}`,
+ },
+ };
+
+ sleep(0.2);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 1: User A creates a tweet (so User B can like it)
+ // ─────────────────────────────────────────────────────────────────────────────
+ const uniqueContent = `Notification Test Tweet ${__VU}-${__ITER} - ${Date.now()}`;
+
+ const createPayload = JSON.stringify({
+ content: uniqueContent,
+ });
+
+ const resCreateTweet = http.post(`${STRESS_TEST_URL}/tweets`, createPayload, {
+ ...authParamsA,
+ tags: { name: '03_UserA_Create_Tweet' },
+ });
+
+ if (!check(resCreateTweet, { 'UserA Create Tweet 201': (r) => r.status === 201 })) {
+ console.error(`UserA Create Tweet Failed: ${resCreateTweet.body}`);
+ return;
+ }
+
+ const tweetId = resCreateTweet.json('data.id');
+
+ sleep(0.2);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 2: User B follows User A (triggers FOLLOW notification)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Trigger Notifications', function () {
+ const resFollow = http.post(
+ `${STRESS_TEST_URL}/users/${userA.username}/following`,
+ null,
+ { ...authParamsB, tags: { name: '04_UserB_Follow_UserA' } }
+ );
+
+ check(resFollow, {
+ 'UserB Follow UserA 2xx': (r) => r.status >= 200 && r.status < 300,
+ });
+
+ sleep(0.1);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 3: User B likes User A's tweet (triggers LIKE notification)
+ // ───────────────────────────────────────────────────────────────────────────
+ const resLike = http.post(
+ `${STRESS_TEST_URL}/tweets/${tweetId}/like`,
+ null,
+ { ...authParamsB, tags: { name: '05_UserB_Like_Tweet' } }
+ );
+
+ check(resLike, {
+ 'UserB Like Tweet 2xx': (r) => r.status >= 200 && r.status < 300,
+ });
+ });
+
+ // Small delay to allow notifications to be processed
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // User A: Test Notification Endpoints
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Notification Endpoints', function () {
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 4: GET /notifications (first page)
+ // ───────────────────────────────────────────────────────────────────────────
+ const resNotifications = http.get(
+ `${STRESS_TEST_URL}/notifications?limit=20`,
+ { ...authParamsA, tags: { name: '06_Get_Notifications' } }
+ );
+
+ const notificationsOk = check(resNotifications, {
+ 'Get Notifications 200': (r) => r.status === 200,
+ });
+
+ if (!notificationsOk) {
+ console.error(`Get Notifications Failed: ${resNotifications.body}`);
+ return;
+ }
+
+ // Get a notification ID for single mark-as-seen test
+ let notificationId = null;
+ let cursor = null;
+ try {
+ const body = resNotifications.json();
+ if (body.items && body.items.length > 0) {
+ notificationId = body.items[0].id;
+ }
+ if (body.pagination && body.pagination.nextCursor) {
+ cursor = body.pagination.nextCursor;
+ }
+ } catch {
+ // No notifications available
+ }
+
+ sleep(0.1);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 5: GET /notifications (pagination - second page if cursor exists)
+ // ───────────────────────────────────────────────────────────────────────────
+ if (cursor) {
+ const resNotificationsPage2 = http.get(
+ `${STRESS_TEST_URL}/notifications?limit=20&cursor=${encodeURIComponent(cursor)}`,
+ { ...authParamsA, tags: { name: '07_Get_Notifications_Page2' } }
+ );
+
+ check(resNotificationsPage2, {
+ 'Get Notifications Page2 200': (r) => r.status === 200,
+ });
+ } else {
+ // Make a request with different limit to ensure consistent load
+ const resNotificationsPage2 = http.get(
+ `${STRESS_TEST_URL}/notifications?limit=10`,
+ { ...authParamsA, tags: { name: '07_Get_Notifications_Page2' } }
+ );
+
+ check(resNotificationsPage2, {
+ 'Get Notifications Page2 200': (r) => r.status === 200,
+ });
+ }
+
+ sleep(0.1);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 6: GET /notifications/count
+ // ───────────────────────────────────────────────────────────────────────────
+ const resCount = http.get(
+ `${STRESS_TEST_URL}/notifications/count`,
+ { ...authParamsA, tags: { name: '08_Get_Unseen_Count' } }
+ );
+
+ check(resCount, {
+ 'Get Unseen Count 200': (r) => r.status === 200,
+ });
+
+ sleep(0.1);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 7: PATCH /notifications/:notificationId/seen (mark single as seen)
+ // ───────────────────────────────────────────────────────────────────────────
+ if (notificationId) {
+ const resMarkSingleSeen = http.patch(
+ `${STRESS_TEST_URL}/notifications/${notificationId}/seen`,
+ null,
+ { ...authParamsA, tags: { name: '09_Mark_Single_Seen' } }
+ );
+
+ check(resMarkSingleSeen, {
+ 'Mark Single Seen 200': (r) => r.status === 200,
+ });
+ }
+ // Skip if no notification ID available (no notifications triggered yet)
+
+ sleep(0.1);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 8: PATCH /notifications/seen (mark all as seen)
+ // ───────────────────────────────────────────────────────────────────────────
+ const resMarkAllSeen = http.patch(
+ `${STRESS_TEST_URL}/notifications/seen`,
+ null,
+ { ...authParamsA, tags: { name: '10_Mark_All_Seen' } }
+ );
+
+ check(resMarkAllSeen, {
+ 'Mark All Seen 200': (r) => r.status === 200,
+ });
+ });
+
+ sleep(0.1);
+}
diff --git a/test/performance/notifications/report.html b/test/performance/notifications/report.html
new file mode 100644
index 00000000..686f6c67
--- /dev/null
+++ b/test/performance/notifications/report.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/onboarding/onboarding.stress.js b/test/performance/onboarding/onboarding.stress.js
new file mode 100644
index 00000000..7c7c88b3
--- /dev/null
+++ b/test/performance/onboarding/onboarding.stress.js
@@ -0,0 +1,168 @@
+import http from 'k6/http';
+import { check, sleep, group } from 'k6';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 50 }, // Ramp up to 50 users
+ { duration: '1m', target: 100 }, // Ramp up to 100 users
+ { duration: '30s', target: 200 }, // Ramp up to 200 users
+ { duration: '2m', target: 200 }, // Sustain at 200 users
+ { duration: '30s', target: 0 }, // Ramp down
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.01'],
+ 'http_req_duration{name:01_Login_Action}': ['p(95)<3000'],
+ 'http_req_duration{name:02_Get_Follow_Suggestions}': ['p(95)<2000'],
+ 'http_req_duration{name:03_Get_Follow_Suggestions_With_Limit}': ['p(95)<2000'],
+ 'http_req_duration{name:04_Get_Username_Suggestions}': ['p(95)<1500'],
+ 'http_req_duration{name:05_Get_Username_Suggestions_Typed}': ['p(95)<1500'],
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+
+/**
+ * Helper function to create and login a user
+ * Returns { accessToken } or null on failure
+ */
+function createAndLoginUser(tagName) {
+ const resCreateUser = http.post(`${STRESS_TEST_URL}/test/users`);
+
+ if (!check(resCreateUser, { 'User Created 201': (r) => r.status === 201 })) {
+ console.error(`Setup Failed (Create User): ${resCreateUser.body}`);
+ return null;
+ }
+
+ const userData = resCreateUser.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword,
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: tagName },
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, { 'Login 200': (r) => r.status === 200 })) {
+ console.error(`Setup Failed (Login): ${resLogin.body}`);
+ return null;
+ }
+
+ return {
+ accessToken: resLogin.json('data.accessToken'),
+ };
+}
+
+export default function () {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // SETUP: Create and login user
+ // ─────────────────────────────────────────────────────────────────────────────
+ const user = createAndLoginUser('01_Login_Action');
+ if (!user) return;
+
+ const authHeaders = {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ Authorization: `Bearer ${user.accessToken}`,
+ };
+
+ sleep(0.2);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 1: Get Follow Suggestions (default limit)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Follow Suggestions', function () {
+ const resFollowSuggestions = http.get(
+ `${STRESS_TEST_URL}/onboarding/follow-suggestions`,
+ {
+ headers: authHeaders,
+ tags: { name: '02_Get_Follow_Suggestions' },
+ }
+ );
+
+ const followSuggestionsOk = check(resFollowSuggestions, {
+ 'Get Follow Suggestions 200': (r) => r.status === 200,
+ });
+
+ if (!followSuggestionsOk) {
+ console.error(`Get Follow Suggestions Failed [VU:${__VU}]: ${resFollowSuggestions.status} - ${resFollowSuggestions.body}`);
+ }
+
+ sleep(0.2);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 2: Get Follow Suggestions with custom limit
+ // ───────────────────────────────────────────────────────────────────────────
+ const resFollowSuggestionsLimit = http.get(
+ `${STRESS_TEST_URL}/onboarding/follow-suggestions?limit=10`,
+ {
+ headers: authHeaders,
+ tags: { name: '03_Get_Follow_Suggestions_With_Limit' },
+ }
+ );
+
+ const followSuggestionsLimitOk = check(resFollowSuggestionsLimit, {
+ 'Get Follow Suggestions With Limit 200': (r) => r.status === 200,
+ });
+
+ if (!followSuggestionsLimitOk) {
+ console.error(`Get Follow Suggestions With Limit Failed [VU:${__VU}]: ${resFollowSuggestionsLimit.status} - ${resFollowSuggestionsLimit.body}`);
+ }
+ });
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 3: Get Username Suggestions (based on display name)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Username Suggestions', function () {
+ const resUsernameSuggestions = http.get(
+ `${STRESS_TEST_URL}/onboarding/username-suggestions`,
+ {
+ headers: authHeaders,
+ tags: { name: '04_Get_Username_Suggestions' },
+ }
+ );
+
+ const usernameSuggestionsOk = check(resUsernameSuggestions, {
+ 'Get Username Suggestions 200': (r) => r.status === 200,
+ });
+
+ if (!usernameSuggestionsOk) {
+ console.error(`Get Username Suggestions Failed [VU:${__VU}]: ${resUsernameSuggestions.status} - ${resUsernameSuggestions.body}`);
+ }
+
+ sleep(0.2);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 4: Get Username Suggestions with typed parameter
+ // ───────────────────────────────────────────────────────────────────────────
+ const typedInput = `user${__VU}${__ITER}`;
+ const resUsernameSuggestionsTyped = http.get(
+ `${STRESS_TEST_URL}/onboarding/username-suggestions?typed=${encodeURIComponent(typedInput)}`,
+ {
+ headers: authHeaders,
+ tags: { name: '05_Get_Username_Suggestions_Typed' },
+ }
+ );
+
+ const usernameSuggestionsTypedOk = check(resUsernameSuggestionsTyped, {
+ 'Get Username Suggestions Typed 200': (r) => r.status === 200,
+ });
+
+ if (!usernameSuggestionsTypedOk) {
+ console.error(`Get Username Suggestions Typed Failed [VU:${__VU}]: ${resUsernameSuggestionsTyped.status} - ${resUsernameSuggestionsTyped.body}`);
+ }
+ });
+
+ sleep(0.2);
+}
diff --git a/test/performance/onboarding/report.html b/test/performance/onboarding/report.html
new file mode 100644
index 00000000..2b97fe17
--- /dev/null
+++ b/test/performance/onboarding/report.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/profile/actions.stress.js b/test/performance/profile/actions.stress.js
index 9f0b2603..2208c08d 100644
--- a/test/performance/profile/actions.stress.js
+++ b/test/performance/profile/actions.stress.js
@@ -19,9 +19,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3001';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreate = http.post(
`${STRESS_TEST_URL}/test/users`,
@@ -59,6 +59,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const headers = {
'Content-Type': 'application/json',
diff --git a/test/performance/profile/report_actions.html b/test/performance/profile/report_actions.html
new file mode 100644
index 00000000..c66936ed
--- /dev/null
+++ b/test/performance/profile/report_actions.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/profile/report_update.html b/test/performance/profile/report_update.html
new file mode 100644
index 00000000..cefa564a
--- /dev/null
+++ b/test/performance/profile/report_update.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/profile/update.stress.js b/test/performance/profile/update.stress.js
index c09ed9da..f8d80028 100644
--- a/test/performance/profile/update.stress.js
+++ b/test/performance/profile/update.stress.js
@@ -20,9 +20,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3001';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreate = http.post(
`${STRESS_TEST_URL}/test/users`,
@@ -60,6 +60,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const headers = {
'Content-Type': 'application/json',
diff --git a/test/performance/search/report.html b/test/performance/search/report.html
new file mode 100644
index 00000000..cb56fa8f
--- /dev/null
+++ b/test/performance/search/report.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/search/search.stress.js b/test/performance/search/search.stress.js
new file mode 100644
index 00000000..7bbafacc
--- /dev/null
+++ b/test/performance/search/search.stress.js
@@ -0,0 +1,318 @@
+import http from 'k6/http';
+import { check, sleep, group } from 'k6';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 50 }, // Ramp up to 50 users
+ { duration: '1m', target: 100 }, // Ramp up to 100 users
+ { duration: '30s', target: 200 }, // Ramp up to 200 users
+ { duration: '2m', target: 200 }, // Sustain at 200 users
+ { duration: '30s', target: 0 }, // Ramp down
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.01'],
+ 'http_req_duration{name:01_Login_Action}': ['p(95)<3000'],
+ 'http_req_duration{name:02_Search_Suggestions}': ['p(95)<1500'],
+ 'http_req_duration{name:03_Search_Suggestions_Hashtag}': ['p(95)<1500'],
+ 'http_req_duration{name:04_Search_Tweets}': ['p(95)<2000'],
+ 'http_req_duration{name:05_Search_Tweets_Page2}': ['p(95)<2000'],
+ 'http_req_duration{name:06_Search_Users}': ['p(95)<2000'],
+ 'http_req_duration{name:07_Search_Users_Page2}': ['p(95)<2000'],
+ 'http_req_duration{name:08_User_Suggestions}': ['p(95)<1500'],
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+
+// Sample search queries to use
+const SEARCH_QUERIES = [
+ 'hello',
+ 'test',
+ 'user',
+ 'tweet',
+ 'world',
+ 'stress',
+ 'search',
+ 'random',
+];
+
+const HASHTAG_QUERIES = [
+ '#trending',
+ '#news',
+ '#tech',
+ '#hello',
+ '#test',
+];
+
+/**
+ * Get a random search query
+ */
+function getRandomQuery() {
+ return SEARCH_QUERIES[Math.floor(Math.random() * SEARCH_QUERIES.length)];
+}
+
+/**
+ * Get a random hashtag query
+ */
+function getRandomHashtagQuery() {
+ return HASHTAG_QUERIES[Math.floor(Math.random() * HASHTAG_QUERIES.length)];
+}
+
+/**
+ * Helper function to create and login a user
+ * Returns { accessToken } or null on failure
+ */
+function createAndLoginUser(tagName) {
+ const resCreateUser = http.post(`${STRESS_TEST_URL}/test/users`);
+
+ if (!check(resCreateUser, { 'User Created 201': (r) => r.status === 201 })) {
+ console.error(`Setup Failed (Create User): ${resCreateUser.body}`);
+ return null;
+ }
+
+ const userData = resCreateUser.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword,
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: tagName },
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, { 'Login 200': (r) => r.status === 200 })) {
+ console.error(`Setup Failed (Login): ${resLogin.body}`);
+ return null;
+ }
+
+ return {
+ accessToken: resLogin.json('data.accessToken'),
+ };
+}
+
+export default function () {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // SETUP: Create and login user
+ // ─────────────────────────────────────────────────────────────────────────────
+ const user = createAndLoginUser('01_Login_Action');
+ if (!user) return;
+
+ const authHeaders = {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ Authorization: `Bearer ${user.accessToken}`,
+ };
+
+ sleep(0.2);
+
+ const searchQuery = getRandomQuery();
+ const hashtagQuery = getRandomHashtagQuery();
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 1: GET /search/suggestions (regular query)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Search Suggestions', function () {
+ const resSuggestions = http.get(
+ `${STRESS_TEST_URL}/search/suggestions?query=${encodeURIComponent(searchQuery)}`,
+ {
+ headers: authHeaders,
+ tags: { name: '02_Search_Suggestions' },
+ }
+ );
+
+ const suggestionsOk = check(resSuggestions, {
+ 'Search Suggestions 200': (r) => r.status === 200,
+ });
+
+ if (!suggestionsOk) {
+ console.error(`Search Suggestions Failed [VU:${__VU}]: ${resSuggestions.status} - ${resSuggestions.body}`);
+ }
+
+ sleep(0.2);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 2: GET /search/suggestions (hashtag query)
+ // ───────────────────────────────────────────────────────────────────────────
+ const resSuggestionsHashtag = http.get(
+ `${STRESS_TEST_URL}/search/suggestions?query=${encodeURIComponent(hashtagQuery)}`,
+ {
+ headers: authHeaders,
+ tags: { name: '03_Search_Suggestions_Hashtag' },
+ }
+ );
+
+ const suggestionsHashtagOk = check(resSuggestionsHashtag, {
+ 'Search Suggestions Hashtag 200': (r) => r.status === 200,
+ });
+
+ if (!suggestionsHashtagOk) {
+ console.error(`Search Suggestions Hashtag Failed [VU:${__VU}]: ${resSuggestionsHashtag.status} - ${resSuggestionsHashtag.body}`);
+ }
+ });
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 3: GET /search/tweets (first page)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Search Tweets', function () {
+ const resTweets = http.get(
+ `${STRESS_TEST_URL}/search/tweets?query=${encodeURIComponent(searchQuery)}&limit=20`,
+ {
+ headers: authHeaders,
+ tags: { name: '04_Search_Tweets' },
+ }
+ );
+
+ const tweetsOk = check(resTweets, {
+ 'Search Tweets 200': (r) => r.status === 200,
+ });
+
+ if (!tweetsOk) {
+ console.error(`Search Tweets Failed [VU:${__VU}]: ${resTweets.status} - ${resTweets.body}`);
+ return;
+ }
+
+ sleep(0.2);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 4: GET /search/tweets (pagination - second page)
+ // ───────────────────────────────────────────────────────────────────────────
+ let cursor = null;
+ try {
+ const body = resTweets.json();
+ if (body.pagination && body.pagination.nextCursor) {
+ cursor = body.pagination.nextCursor;
+ }
+ } catch {
+ // No cursor available
+ }
+
+ if (cursor) {
+ const resTweetsPage2 = http.get(
+ `${STRESS_TEST_URL}/search/tweets?query=${encodeURIComponent(searchQuery)}&limit=20&cursor=${encodeURIComponent(cursor)}`,
+ {
+ headers: authHeaders,
+ tags: { name: '05_Search_Tweets_Page2' },
+ }
+ );
+
+ check(resTweetsPage2, {
+ 'Search Tweets Page2 200': (r) => r.status === 200,
+ });
+ } else {
+ // Make another request with different limit for consistent load
+ const resTweetsPage2 = http.get(
+ `${STRESS_TEST_URL}/search/tweets?query=${encodeURIComponent(searchQuery)}&limit=10`,
+ {
+ headers: authHeaders,
+ tags: { name: '05_Search_Tweets_Page2' },
+ }
+ );
+
+ check(resTweetsPage2, {
+ 'Search Tweets Page2 200': (r) => r.status === 200,
+ });
+ }
+ });
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 5: GET /search/users (first page)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Search Users', function () {
+ const resUsers = http.get(
+ `${STRESS_TEST_URL}/search/users?query=${encodeURIComponent(searchQuery)}&limit=20`,
+ {
+ headers: authHeaders,
+ tags: { name: '06_Search_Users' },
+ }
+ );
+
+ const usersOk = check(resUsers, {
+ 'Search Users 200': (r) => r.status === 200,
+ });
+
+ if (!usersOk) {
+ console.error(`Search Users Failed [VU:${__VU}]: ${resUsers.status} - ${resUsers.body}`);
+ return;
+ }
+
+ sleep(0.2);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 6: GET /search/users (pagination - second page)
+ // ───────────────────────────────────────────────────────────────────────────
+ let cursor = null;
+ try {
+ const body = resUsers.json();
+ if (body.pagination && body.pagination.nextCursor) {
+ cursor = body.pagination.nextCursor;
+ }
+ } catch {
+ // No cursor available
+ }
+
+ if (cursor) {
+ const resUsersPage2 = http.get(
+ `${STRESS_TEST_URL}/search/users?query=${encodeURIComponent(searchQuery)}&limit=20&cursor=${encodeURIComponent(cursor)}`,
+ {
+ headers: authHeaders,
+ tags: { name: '07_Search_Users_Page2' },
+ }
+ );
+
+ check(resUsersPage2, {
+ 'Search Users Page2 200': (r) => r.status === 200,
+ });
+ } else {
+ // Make another request with different limit for consistent load
+ const resUsersPage2 = http.get(
+ `${STRESS_TEST_URL}/search/users?query=${encodeURIComponent(searchQuery)}&limit=10`,
+ {
+ headers: authHeaders,
+ tags: { name: '07_Search_Users_Page2' },
+ }
+ );
+
+ check(resUsersPage2, {
+ 'Search Users Page2 200': (r) => r.status === 200,
+ });
+ }
+ });
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 7: GET /search/users/suggestions (for mentions)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('User Suggestions', function () {
+ const resUserSuggestions = http.get(
+ `${STRESS_TEST_URL}/search/users/suggestions?query=${encodeURIComponent(searchQuery)}`,
+ {
+ headers: authHeaders,
+ tags: { name: '08_User_Suggestions' },
+ }
+ );
+
+ const userSuggestionsOk = check(resUserSuggestions, {
+ 'User Suggestions 200': (r) => r.status === 200,
+ });
+
+ if (!userSuggestionsOk) {
+ console.error(`User Suggestions Failed [VU:${__VU}]: ${resUserSuggestions.status} - ${resUserSuggestions.body}`);
+ }
+ });
+
+ sleep(0.1);
+}
diff --git a/test/performance/settings/account/birthdate.stress.js b/test/performance/settings/account/birthdate.stress.js
index a310c90c..a0ee28be 100644
--- a/test/performance/settings/account/birthdate.stress.js
+++ b/test/performance/settings/account/birthdate.stress.js
@@ -17,9 +17,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3001';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreate = http.post(
`${STRESS_TEST_URL}/test/users`,
@@ -57,6 +57,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const headers = {
'Content-Type': 'application/json',
diff --git a/test/performance/settings/account/email.stress.js b/test/performance/settings/account/email.stress.js
index 8ce4a57c..64b48c9f 100644
--- a/test/performance/settings/account/email.stress.js
+++ b/test/performance/settings/account/email.stress.js
@@ -19,9 +19,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3001';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreate = http.post(
`${STRESS_TEST_URL}/test/users`,
@@ -59,6 +59,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const headers = {
'Content-Type': 'application/json',
diff --git a/test/performance/settings/account/password.stress.js b/test/performance/settings/account/password.stress.js
index ccf6cb98..29165112 100644
--- a/test/performance/settings/account/password.stress.js
+++ b/test/performance/settings/account/password.stress.js
@@ -17,10 +17,10 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3001';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreate = http.post(
`${STRESS_TEST_URL}/test/users`,
@@ -58,6 +58,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken, userPassword };
+}
+
+export default function (data) {
+ const { accessToken, userPassword } = data;
const headers = {
'Content-Type': 'application/json',
diff --git a/test/performance/settings/account/username.stress.js b/test/performance/settings/account/username.stress.js
index 41784978..8735abe6 100644
--- a/test/performance/settings/account/username.stress.js
+++ b/test/performance/settings/account/username.stress.js
@@ -18,9 +18,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3001';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreate = http.post(
`${STRESS_TEST_URL}/test/users`,
@@ -58,6 +58,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const headers = {
'Content-Type': 'application/json',
diff --git a/test/performance/settings/general.stress.js b/test/performance/settings/general.stress.js
new file mode 100644
index 00000000..b773924a
--- /dev/null
+++ b/test/performance/settings/general.stress.js
@@ -0,0 +1,180 @@
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 20 },
+ { duration: '1m', target: 50 },
+ { duration: '30s', target: 500 },
+ { duration: '1m', target: 700 },
+ { duration: '30s', target: 0 },
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.01'],
+ 'http_req_duration{name:01_Login_Action}': ['p(95)<1800'],
+ 'http_req_duration{name:02_Get_Me}': ['p(95)<500'],
+ 'http_req_duration{name:03_Get_Blocks}': ['p(95)<500'],
+ 'http_req_duration{name:04_Get_Mutes}': ['p(95)<500'],
+ 'http_req_duration{name:05_Get_Connected_Accounts}': ['p(95)<500'],
+ 'http_req_duration{name:06_Get_Countries}': ['p(95)<500'],
+ 'http_req_duration{name:07_Update_Country}': ['p(95)<500'],
+ 'http_req_duration{name:08_Update_Gender}': ['p(95)<500'],
+ 'http_req_duration{name:09_Get_Interests}': ['p(95)<500'],
+ 'http_req_duration{name:10_Update_Language}': ['p(95)<500'],
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
+
+export function setup() {
+
+ const resCreate = http.post(
+ `${STRESS_TEST_URL}/test/users`,
+ );
+
+ if (!check(resCreate, { 'User Created 201': (r) => r.status === 201 })) {
+ console.error(`Failed to create user: ${resCreate.body}`);
+ return;
+ }
+
+ const userData = resCreate.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: '01_Login_Action' }
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, {
+ 'Login status is 200': (r) => r.status === 200
+ })) {
+ console.error(`Login Failed: ${resLogin.body}`);
+ return;
+ }
+
+ const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
+
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ 'Authorization': `Bearer ${accessToken}`,
+ };
+
+ // GET /me
+ const resMe = http.get(
+ `${STRESS_TEST_URL}/me`,
+ { headers: headers, tags: { name: '02_Get_Me' } }
+ );
+ check(resMe, {
+ 'Get Me status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // GET /me/settings/blocks
+ const resBlocks = http.get(
+ `${STRESS_TEST_URL}/me/settings/blocks`,
+ { headers: headers, tags: { name: '03_Get_Blocks' } }
+ );
+ check(resBlocks, {
+ 'Get Blocks status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // GET /me/settings/mutes
+ const resMutes = http.get(
+ `${STRESS_TEST_URL}/me/settings/mutes`,
+ { headers: headers, tags: { name: '04_Get_Mutes' } }
+ );
+ check(resMutes, {
+ 'Get Mutes status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // GET /me/settings/connected-accounts
+ const resConnected = http.get(
+ `${STRESS_TEST_URL}/me/settings/connected-accounts`,
+ { headers: headers, tags: { name: '05_Get_Connected_Accounts' } }
+ );
+ check(resConnected, {
+ 'Get Connected Accounts status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // GET /me/settings/country
+ const resCountries = http.get(
+ `${STRESS_TEST_URL}/me/settings/country`,
+ { headers: headers, tags: { name: '06_Get_Countries' } }
+ );
+ check(resCountries, {
+ 'Get Countries status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // PUT /me/settings/country
+ const payloadCountry = JSON.stringify({
+ countryName: "Egypt"
+ });
+ const resUpdateCountry = http.put(
+ `${STRESS_TEST_URL}/me/settings/country`,
+ payloadCountry,
+ { headers: headers, tags: { name: '07_Update_Country' } }
+ );
+ check(resUpdateCountry, {
+ 'Update Country status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // PUT /me/settings/gender
+ const payloadGender = JSON.stringify({
+ gender: "Male"
+ });
+ const resUpdateGender = http.put(
+ `${STRESS_TEST_URL}/me/settings/gender`,
+ payloadGender,
+ { headers: headers, tags: { name: '08_Update_Gender' } }
+ );
+ check(resUpdateGender, {
+ 'Update Gender status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // GET /me/settings/interests
+ const resInterests = http.get(
+ `${STRESS_TEST_URL}/me/settings/interests`,
+ { headers: headers, tags: { name: '09_Get_Interests' } }
+ );
+ check(resInterests, {
+ 'Get Interests status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+
+ // PUT /me/settings/language
+ const payloadLanguage = JSON.stringify({
+ language: "en"
+ });
+ const resUpdateLanguage = http.put(
+ `${STRESS_TEST_URL}/me/settings/language`,
+ payloadLanguage,
+ { headers: headers, tags: { name: '10_Update_Language' } }
+ );
+ check(resUpdateLanguage, {
+ 'Update Language status is 200': (r) => r.status === 200,
+ });
+ sleep(0.1);
+}
diff --git a/test/performance/settings/report.html b/test/performance/settings/report.html
new file mode 100644
index 00000000..ea2561f3
--- /dev/null
+++ b/test/performance/settings/report.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/timeline/report.html b/test/performance/timeline/report.html
new file mode 100644
index 00000000..3973846f
--- /dev/null
+++ b/test/performance/timeline/report.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ k6 report
+
+
+
+
+
+
+
+
+
+
diff --git a/test/performance/timeline/timeline.stress.js b/test/performance/timeline/timeline.stress.js
new file mode 100644
index 00000000..f2875f50
--- /dev/null
+++ b/test/performance/timeline/timeline.stress.js
@@ -0,0 +1,219 @@
+import http from 'k6/http';
+import { check, sleep, group } from 'k6';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 20 }, // Ramp up to 20 users
+ { duration: '1m', target: 50 }, // Ramp up to 50 users
+ { duration: '30s', target: 100 }, // Ramp up to 100 users
+ { duration: '1m', target: 100 }, // Stay at 100 users
+ { duration: '30s', target: 0 }, // Ramp down
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.01'],
+ 'http_req_duration{name:01_Login_Action}': ['p(95)<2000'],
+ 'http_req_duration{name:02_Create_Tweet_For_Timeline}': ['p(95)<2000'],
+ 'http_req_duration{name:03_Get_Following_Timeline}': ['p(95)<1000'],
+ 'http_req_duration{name:04_Get_Following_Pagination}': ['p(95)<1000'],
+ 'http_req_duration{name:05_Get_ForYou_Timeline}': ['p(95)<1000'],
+ 'http_req_duration{name:06_Get_ForYou_Pagination}': ['p(95)<1000'],
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+
+export default function () {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // SETUP: Create a test user
+ // ─────────────────────────────────────────────────────────────────────────────
+ const resCreateUser = http.post(`${STRESS_TEST_URL}/test/users`);
+
+ if (!check(resCreateUser, { 'User Created 201': (r) => r.status === 201 })) {
+ console.error(`Setup Failed (Create User): ${resCreateUser.body}`);
+ return;
+ }
+
+ const userData = resCreateUser.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 1: Login
+ // ─────────────────────────────────────────────────────────────────────────────
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword,
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: '01_Login_Action' },
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, { 'Login 200': (r) => r.status === 200 })) {
+ console.error(`Setup Failed (Login): ${resLogin.body}`);
+ return;
+ }
+
+ const accessToken = resLogin.json('data.accessToken');
+
+ const authParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ Authorization: `Bearer ${accessToken}`,
+ },
+ };
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 2: Create a tweet so the timeline has content
+ // ─────────────────────────────────────────────────────────────────────────────
+ const uniqueContent = `Timeline Stress Test Tweet ${__VU}-${__ITER} - ${Date.now()}`;
+
+ const createPayload = JSON.stringify({
+ content: uniqueContent,
+ });
+
+ const resCreateTweet = http.post(`${STRESS_TEST_URL}/tweets`, createPayload, {
+ ...authParams,
+ tags: { name: '02_Create_Tweet_For_Timeline' },
+ });
+
+ if (!check(resCreateTweet, { 'Create Tweet 201': (r) => r.status === 201 })) {
+ console.error(`Create Tweet Failed: ${resCreateTweet.body}`);
+ return;
+ }
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 3: Get Following Timeline (first page)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('Following Timeline', function () {
+ const resFollowingTimeline = http.get(
+ `${STRESS_TEST_URL}/timeline/following?limit=20`,
+ { ...authParams, tags: { name: '03_Get_Following_Timeline' } }
+ );
+
+ const followingTimelineOk = check(resFollowingTimeline, {
+ 'Get Following Timeline 200': (r) => r.status === 200
+ });
+
+ if (!followingTimelineOk) {
+ console.error(`Get Following Timeline Failed: ${resFollowingTimeline.body}`);
+ return;
+ }
+
+ // Get cursor for pagination test if available
+ let followingCursor = null;
+ try {
+ const followingBody = resFollowingTimeline.json();
+ if (followingBody.pagination && followingBody.pagination.nextCursor) {
+ followingCursor = followingBody.pagination.nextCursor;
+ }
+ } catch {
+ // No cursor available
+ }
+
+ sleep(0.2);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 4: Get Following Timeline (pagination - second page if cursor exists)
+ // ───────────────────────────────────────────────────────────────────────────
+ if (followingCursor) {
+ const resFollowingPagination = http.get(
+ `${STRESS_TEST_URL}/timeline/following?limit=20&cursor=${encodeURIComponent(followingCursor)}`,
+ { ...authParams, tags: { name: '04_Get_Following_Pagination' } }
+ );
+
+ check(resFollowingPagination, {
+ 'Get Following Pagination 200': (r) => r.status === 200,
+ });
+
+ if (resFollowingPagination.status !== 200) {
+ console.error(`Get Following Pagination Failed: ${resFollowingPagination.body}`);
+ }
+ } else {
+ // Make a request anyway to ensure thresholds are met with consistent load
+ const resFollowingPagination = http.get(
+ `${STRESS_TEST_URL}/timeline/following?limit=10`,
+ { ...authParams, tags: { name: '04_Get_Following_Pagination' } }
+ );
+
+ check(resFollowingPagination, {
+ 'Get Following Pagination 200': (r) => r.status === 200,
+ });
+ }
+ });
+
+ sleep(0.3);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // STEP 5: Get For You Timeline (first page)
+ // ─────────────────────────────────────────────────────────────────────────────
+ group('For You Timeline', function () {
+ const resForYouTimeline = http.get(
+ `${STRESS_TEST_URL}/timeline/for-you?limit=20`,
+ { ...authParams, tags: { name: '05_Get_ForYou_Timeline' } }
+ );
+
+ const forYouTimelineOk = check(resForYouTimeline, {
+ 'Get ForYou Timeline 200': (r) => r.status === 200,
+ });
+
+ if (!forYouTimelineOk) {
+ console.error(`Get ForYou Timeline Failed: ${resForYouTimeline.body}`);
+ return;
+ }
+
+ // Get cursor for pagination test if available
+ let forYouCursor = null;
+ try {
+ const forYouBody = resForYouTimeline.json();
+ if (forYouBody.pagination && forYouBody.pagination.nextCursor) {
+ forYouCursor = forYouBody.pagination.nextCursor;
+ }
+ } catch {
+ // No cursor available
+ }
+
+ sleep(0.2);
+
+ // ───────────────────────────────────────────────────────────────────────────
+ // STEP 6: Get For You Timeline (pagination - second page if cursor exists)
+ // ───────────────────────────────────────────────────────────────────────────
+ if (forYouCursor) {
+ const resForYouPagination = http.get(
+ `${STRESS_TEST_URL}/timeline/for-you?limit=20&cursor=${encodeURIComponent(forYouCursor)}`,
+ { ...authParams, tags: { name: '06_Get_ForYou_Pagination' } }
+ );
+
+ check(resForYouPagination, {
+ 'Get ForYou Pagination 200': (r) => r.status === 200,
+ });
+
+ if (resForYouPagination.status !== 200) {
+ console.error(`Get ForYou Pagination Failed: ${resForYouPagination.body}`);
+ }
+ } else {
+ // Make a request anyway to ensure thresholds are met with consistent load
+ const resForYouPagination = http.get(
+ `${STRESS_TEST_URL}/timeline/for-you?limit=10`,
+ { ...authParams, tags: { name: '06_Get_ForYou_Pagination' } }
+ );
+
+ check(resForYouPagination, {
+ 'Get ForYou Pagination 200': (r) => r.status === 200,
+ });
+ }
+ });
+
+ sleep(0.1);
+}
diff --git a/test/performance/tweets/tweet-crud.stress.js b/test/performance/tweets/tweet-crud.stress.js
index 865f5474..a9ee4fa5 100644
--- a/test/performance/tweets/tweet-crud.stress.js
+++ b/test/performance/tweets/tweet-crud.stress.js
@@ -5,8 +5,8 @@ export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 50 },
- { duration: '30s', target: 100 },
- { duration: '1m', target: 100 },
+ { duration: '30s', target: 200 },
+ { duration: '1m', target: 400 },
{ duration: '30s', target: 0 },
],
thresholds: {
@@ -18,9 +18,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreateUser = http.post(
`${STRESS_TEST_URL}/test/users`,
);
@@ -55,6 +55,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const authParams = {
headers: {
diff --git a/test/performance/tweets/tweet-engagement.stress.js b/test/performance/tweets/tweet-engagement.stress.js
index 7c01e663..f46bc953 100644
--- a/test/performance/tweets/tweet-engagement.stress.js
+++ b/test/performance/tweets/tweet-engagement.stress.js
@@ -5,8 +5,8 @@ export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 50 },
- { duration: '30s', target: 100 },
- { duration: '1m', target: 100 },
+ { duration: '30s', target: 200 },
+ { duration: '1m', target: 400 },
{ duration: '30s', target: 0 },
],
thresholds: {
@@ -22,9 +22,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreateUser = http.post(
`${STRESS_TEST_URL}/test/users`,
@@ -54,6 +54,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const authParams = {
headers: {
'Content-Type': 'application/json',
diff --git a/test/performance/tweets/tweet-threads.stress.js b/test/performance/tweets/tweet-threads.stress.js
index 91ab7933..0418f7d4 100644
--- a/test/performance/tweets/tweet-threads.stress.js
+++ b/test/performance/tweets/tweet-threads.stress.js
@@ -5,8 +5,8 @@ export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 50 },
- { duration: '30s', target: 100 },
- { duration: '1m', target: 100 },
+ { duration: '30s', target: 200 },
+ { duration: '1m', target: 400 },
{ duration: '30s', target: 0 },
],
thresholds: {
@@ -20,9 +20,9 @@ export const options = {
},
};
-const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'http://localhost:3000';
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
-export default function () {
+export function setup() {
const resCreateUser = http.post(
`${STRESS_TEST_URL}/test/users`,
);
@@ -51,6 +51,11 @@ export default function () {
}
const accessToken = resLogin.json('data.accessToken');
+ return { accessToken };
+}
+
+export default function (data) {
+ const { accessToken } = data;
const authParams = {
headers: {
'Content-Type': 'application/json',
diff --git a/test/performance/users/user-data.stress.js b/test/performance/users/user-data.stress.js
new file mode 100644
index 00000000..959bf74b
--- /dev/null
+++ b/test/performance/users/user-data.stress.js
@@ -0,0 +1,133 @@
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 20 },
+ { duration: '1m', target: 50 },
+ { duration: '30s', target: 200 },
+ { duration: '1m', target: 400 },
+ { duration: '30s', target: 0 },
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.01'],
+ 'http_req_duration{name:01_Login_Action}': ['p(95)<2000'],
+ 'http_req_duration{name:02_Get_User_By_Id}': ['p(95)<500'],
+ 'http_req_duration{name:03_Get_User_Profile}': ['p(95)<500'],
+ 'http_req_duration{name:04_Get_User_Tweets}': ['p(95)<1000'],
+ 'http_req_duration{name:05_Get_User_Replies}': ['p(95)<1000'],
+ 'http_req_duration{name:06_Get_User_Media}': ['p(95)<1000'],
+ 'http_req_duration{name:07_Get_User_Likes}': ['p(95)<1000'],
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
+
+export function setup() {
+ // 1. Create User
+ const resCreateUser = http.post(`${STRESS_TEST_URL}/test/users`);
+
+ if (!check(resCreateUser, { 'User Created 201': (r) => r.status === 201 })) {
+ console.error(`Setup Failed (Create User): ${resCreateUser.body}`);
+ return;
+ }
+
+ const userData = resCreateUser.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+ const userId = userData.id;
+
+ // 2. Login
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword,
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: '01_Login_Action' },
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, { 'Login 200': (r) => r.status === 200 })) {
+ console.error(`Setup Failed (Login): ${resLogin.body}`);
+ return;
+ }
+
+ const accessToken = resLogin.json('data.accessToken');
+ return { accessToken, userId };
+}
+
+export default function (data) {
+ const { accessToken, userId } = data;
+
+ const authParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ 'Authorization': `Bearer ${accessToken}`,
+ },
+ };
+
+ sleep(0.5);
+
+ // 3. Get User By ID
+ const resGetUserById = http.get(
+ `${STRESS_TEST_URL}/users/id/${userId}`,
+ { ...authParams, tags: { name: '02_Get_User_By_Id' } }
+ );
+
+ check(resGetUserById, { 'Get User By Id 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ const users = ['notnowomar', 'gelgel'];
+ const randomUser = () => users[Math.floor(Math.random() * users.length)];
+
+ // 4. Get User Profile
+ const resGetUserProfile = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser()}/profile`,
+ { ...authParams, tags: { name: '03_Get_User_Profile' } }
+ );
+
+ check(resGetUserProfile, { 'Get User Profile 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 5. Get User Tweets
+ const resGetUserTweets = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser()}/tweets`,
+ { ...authParams, tags: { name: '04_Get_User_Tweets' } }
+ );
+
+ check(resGetUserTweets, { 'Get User Tweets 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 6. Get User Replies
+ const resGetUserReplies = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser()}/replies`,
+ { ...authParams, tags: { name: '05_Get_User_Replies' } }
+ );
+
+ check(resGetUserReplies, { 'Get User Replies 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 7. Get User Media
+ const resGetUserMedia = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser()}/media`,
+ { ...authParams, tags: { name: '06_Get_User_Media' } }
+ );
+
+ check(resGetUserMedia, { 'Get User Media 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 8. Get User Likes
+ const resGetUserLikes = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser()}/likes`,
+ { ...authParams, tags: { name: '07_Get_User_Likes' } }
+ );
+
+ check(resGetUserLikes, { 'Get User Likes 200': (r) => r.status === 200 });
+}
diff --git a/test/performance/users/user-social.stress.js b/test/performance/users/user-social.stress.js
new file mode 100644
index 00000000..d586222b
--- /dev/null
+++ b/test/performance/users/user-social.stress.js
@@ -0,0 +1,132 @@
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+
+export const options = {
+ stages: [
+ { duration: '30s', target: 20 },
+ { duration: '1m', target: 50 },
+ { duration: '30s', target: 200 },
+ { duration: '1m', target: 400 },
+ { duration: '30s', target: 0 },
+ ],
+ thresholds: {
+ http_req_failed: ['rate<0.01'],
+ 'http_req_duration{name:01_Login_Action}': ['p(95)<2000'],
+ 'http_req_duration{name:02_Follow_User}': ['p(95)<1000'],
+ 'http_req_duration{name:03_Get_Following}': ['p(95)<1000'],
+ 'http_req_duration{name:04_Get_Followers}': ['p(95)<1000'],
+ 'http_req_duration{name:05_Get_Relationship}': ['p(95)<500'],
+ 'http_req_duration{name:06_Get_Mutual}': ['p(95)<1000'],
+ 'http_req_duration{name:07_Unfollow_User}': ['p(95)<1000'],
+ },
+};
+
+const STRESS_TEST_URL = __ENV.STRESS_TEST_URL || 'https://test.api.raven.cmp27.space';
+
+export function setup() {
+ // 1. Create User A (The Actor)
+ const resCreateUser = http.post(`${STRESS_TEST_URL}/test/users`);
+ if (!check(resCreateUser, { 'User A Created 201': (r) => r.status === 201 })) {
+ console.error(`Setup Failed (Create User A): ${resCreateUser.body}`);
+ return;
+ }
+ const userData = resCreateUser.json('data');
+ const userEmail = userData.email;
+ const userPassword = userData.password;
+ const userUsername = userData.username;
+
+ // 3. Login as User A
+ const loginPayload = JSON.stringify({
+ identifier: userEmail,
+ password: userPassword,
+ });
+
+ const loginParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ },
+ tags: { name: '01_Login_Action' },
+ };
+
+ const resLogin = http.post(`${STRESS_TEST_URL}/auth/login`, loginPayload, loginParams);
+
+ if (!check(resLogin, { 'Login 200': (r) => r.status === 200 })) {
+ console.error(`Setup Failed (Login): ${resLogin.body}`);
+ return;
+ }
+
+ const accessToken = resLogin.json('data.accessToken');
+ return { accessToken, userUsername };
+}
+
+export default function (data) {
+ const { accessToken, userUsername } = data;
+
+ const authParams = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Client-Type': 'web',
+ 'Authorization': `Bearer ${accessToken}`,
+ },
+ };
+
+ sleep(0.5);
+ const users = ['notnowomar', 'gelgel'];
+ const randomUser = users[Math.floor(Math.random() * users.length)];
+
+ // 4. Follow User B
+ const resFollow = http.post(
+ `${STRESS_TEST_URL}/users/${randomUser}/following`,
+ null, // No body needed for follow usually, or empty object
+ { ...authParams, tags: { name: '02_Follow_User' } }
+ );
+
+ check(resFollow, { 'Follow User 200': (r) => r.status === 200 || r.status === 201 });
+ sleep(0.5);
+
+ // 5. Get Following (of User A)
+ const resGetFollowing = http.get(
+ `${STRESS_TEST_URL}/users/${userUsername}/following`,
+ { ...authParams, tags: { name: '03_Get_Following' } }
+ );
+
+ check(resGetFollowing, { 'Get Following 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 6. Get Followers (of User B)
+ const resGetFollowers = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser}/followers`,
+ { ...authParams, tags: { name: '04_Get_Followers' } }
+ );
+
+ check(resGetFollowers, { 'Get Followers 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 7. Get Relationship (A with B)
+ const resGetRelationship = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser}/relationship`,
+ { ...authParams, tags: { name: '05_Get_Relationship' } }
+ );
+
+ check(resGetRelationship, { 'Get Relationship 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 8. Get Mutual (A with B)
+ const resGetMutual = http.get(
+ `${STRESS_TEST_URL}/users/${randomUser}/mutual`,
+ { ...authParams, tags: { name: '06_Get_Mutual' } }
+ );
+
+ check(resGetMutual, { 'Get Mutual 200': (r) => r.status === 200 });
+ sleep(0.5);
+
+ // 9. Unfollow User B
+ const resUnfollow = http.del(
+ `${STRESS_TEST_URL}/users/${randomUser}/following`,
+ null,
+ { ...authParams, tags: { name: '07_Unfollow_User' } }
+ );
+
+ check(resUnfollow, { 'Unfollow User 200': (r) => r.status === 200 });
+}