Add three new K6 load test scripts to cover previously untested API surfaces: - search-advanced.js: Combined geo + text + filter queries, paginated deep search, and sort variations against /search and /search/geo (300 peak VUs) - admin.js: Moderation queue CRUD, bulk moderation, dashboard stats, audit logs, and user management endpoints (50 peak VUs) - mcp.js: MCP server discovery, SSE connection, property-search tool calls, valuation/batch-valuation, and feature extraction (120 peak VUs) Also updates README with new suite documentation, per-suite custom thresholds, and adds the new suites to the CI workflow_dispatch selector. Co-Authored-By: Paperclip <noreply@paperclip.ing>
272 lines
8.4 KiB
JavaScript
272 lines
8.4 KiB
JavaScript
import http from 'k6/http';
|
|
import { check, sleep } from 'k6';
|
|
import { Rate, Trend } from 'k6/metrics';
|
|
import { BASE_URL, SLA_THRESHOLDS, authHeaders } from '../lib/config.js';
|
|
|
|
/**
|
|
* Admin Moderation Queue Load Test — Goodgo Platform
|
|
*
|
|
* Tests admin endpoints: moderation queue listing, approve/reject,
|
|
* bulk moderation, user management, dashboard stats, and audit logs.
|
|
* Requires an admin user account.
|
|
*/
|
|
|
|
const moderationListDuration = new Trend('moderation_list_duration', true);
|
|
const moderationActionDuration = new Trend('moderation_action_duration', true);
|
|
const adminDashboardDuration = new Trend('admin_dashboard_duration', true);
|
|
const auditLogDuration = new Trend('audit_log_duration', true);
|
|
const userListDuration = new Trend('user_list_duration', true);
|
|
const adminFailRate = new Rate('admin_failures');
|
|
|
|
export const options = {
|
|
scenarios: {
|
|
admin_load: {
|
|
executor: 'ramping-vus',
|
|
startVUs: 0,
|
|
stages: [
|
|
{ duration: '20s', target: 10 }, // admin traffic is lower volume
|
|
{ duration: '1m', target: 30 }, // sustain moderate load
|
|
{ duration: '1m', target: 50 }, // stress peak
|
|
{ duration: '20s', target: 0 }, // ramp down
|
|
],
|
|
gracefulRampDown: '10s',
|
|
},
|
|
},
|
|
thresholds: {
|
|
...SLA_THRESHOLDS,
|
|
moderation_list_duration: ['p(95)<500'],
|
|
moderation_action_duration: ['p(95)<800'],
|
|
admin_dashboard_duration: ['p(95)<500'],
|
|
audit_log_duration: ['p(95)<500'],
|
|
user_list_duration: ['p(95)<500'],
|
|
admin_failures: ['rate<0.05'],
|
|
},
|
|
};
|
|
|
|
export function setup() {
|
|
// Register admin user via the standard auth flow.
|
|
// In a real environment the admin role would be pre-seeded;
|
|
// for load testing we attempt registration then login.
|
|
const adminPayload = JSON.stringify({
|
|
phone: '0900000001',
|
|
password: 'AdminLoad@1234!',
|
|
fullName: 'K6 Admin User',
|
|
email: 'k6-admin@goodgo.test',
|
|
});
|
|
|
|
let res = http.post(`${BASE_URL}/auth/register`, adminPayload, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
tags: { name: 'setup_admin_register' },
|
|
});
|
|
|
|
// If already exists, login
|
|
if (res.status !== 200 && res.status !== 201) {
|
|
res = http.post(
|
|
`${BASE_URL}/auth/login`,
|
|
JSON.stringify({ phone: '0900000001', password: 'AdminLoad@1234!' }),
|
|
{
|
|
headers: { 'Content-Type': 'application/json' },
|
|
tags: { name: 'setup_admin_login' },
|
|
},
|
|
);
|
|
}
|
|
|
|
let accessToken = null;
|
|
if (res.status === 200 || res.status === 201) {
|
|
try {
|
|
accessToken = JSON.parse(res.body).accessToken;
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
// Also create some test listings for moderation queue
|
|
const listingIds = [];
|
|
if (accessToken) {
|
|
for (let i = 0; i < 20; i++) {
|
|
const listing = {
|
|
title: `K6 Admin Test Listing ${i}`,
|
|
description: `Moderation load test listing #${i}`,
|
|
transactionType: i % 2 === 0 ? 'SALE' : 'RENT',
|
|
propertyType: ['APARTMENT', 'HOUSE', 'LAND'][i % 3],
|
|
address: `${200 + i} Đường Kiểm Duyệt`,
|
|
ward: 'Phường 1',
|
|
district: 'Quận 1',
|
|
city: 'TP. Hồ Chí Minh',
|
|
latitude: 10.7769 + (i * 0.002),
|
|
longitude: 106.7009 + (i * 0.002),
|
|
area: 50 + (i * 5),
|
|
bedrooms: 1 + (i % 4),
|
|
bathrooms: 1 + (i % 3),
|
|
floors: 1 + (i % 3),
|
|
priceVND: 1000000000 + (i * 200000000),
|
|
direction: 'EAST',
|
|
};
|
|
|
|
const createRes = http.post(
|
|
`${BASE_URL}/listings`,
|
|
JSON.stringify(listing),
|
|
{ headers: authHeaders(accessToken), tags: { name: 'setup_create_listing' } },
|
|
);
|
|
|
|
if (createRes.status === 201 || createRes.status === 200) {
|
|
try {
|
|
listingIds.push(JSON.parse(createRes.body).id);
|
|
} catch (_) { /* skip */ }
|
|
}
|
|
}
|
|
}
|
|
|
|
return { accessToken, listingIds };
|
|
}
|
|
|
|
export default function (data) {
|
|
if (!data.accessToken) {
|
|
// Without auth, still test unauthenticated error handling
|
|
const res = http.get(`${BASE_URL}/admin/moderation?page=1&limit=10`, {
|
|
tags: { name: 'GET /admin/moderation (unauth)' },
|
|
});
|
|
check(res, {
|
|
'unauth moderation: returns 401': (r) => r.status === 401,
|
|
});
|
|
sleep(1);
|
|
return;
|
|
}
|
|
|
|
const headers = authHeaders(data.accessToken);
|
|
const iter = __ITER;
|
|
const scenario = iter % 7;
|
|
|
|
if (scenario === 0) {
|
|
// --- Moderation queue listing ---
|
|
const page = 1 + (iter % 3);
|
|
const res = http.get(`${BASE_URL}/admin/moderation?page=${page}&limit=10`, {
|
|
headers,
|
|
tags: { name: 'GET /admin/moderation' },
|
|
});
|
|
|
|
moderationListDuration.add(res.timings.duration);
|
|
const ok = check(res, {
|
|
'moderation list: status 200|403': (r) => r.status === 200 || r.status === 403,
|
|
});
|
|
if (!ok) adminFailRate.add(1);
|
|
|
|
} else if (scenario === 1 && data.listingIds.length > 0) {
|
|
// --- Approve listing ---
|
|
const listingId = data.listingIds[iter % data.listingIds.length];
|
|
const payload = JSON.stringify({
|
|
listingId,
|
|
moderationNotes: `K6 load test approval — iteration ${iter}`,
|
|
});
|
|
|
|
const res = http.post(`${BASE_URL}/admin/moderation/approve`, payload, {
|
|
headers,
|
|
tags: { name: 'POST /admin/moderation/approve' },
|
|
});
|
|
|
|
moderationActionDuration.add(res.timings.duration);
|
|
const ok = check(res, {
|
|
'approve: status 200|201|403|404|409': (r) =>
|
|
[200, 201, 403, 404, 409].includes(r.status),
|
|
});
|
|
if (!ok) adminFailRate.add(1);
|
|
|
|
} else if (scenario === 2 && data.listingIds.length > 0) {
|
|
// --- Reject listing ---
|
|
const listingId = data.listingIds[(iter + 1) % data.listingIds.length];
|
|
const payload = JSON.stringify({
|
|
listingId,
|
|
reason: `K6 load test rejection — does not meet criteria (iter ${iter})`,
|
|
});
|
|
|
|
const res = http.post(`${BASE_URL}/admin/moderation/reject`, payload, {
|
|
headers,
|
|
tags: { name: 'POST /admin/moderation/reject' },
|
|
});
|
|
|
|
moderationActionDuration.add(res.timings.duration);
|
|
const ok = check(res, {
|
|
'reject: status 200|201|403|404|409': (r) =>
|
|
[200, 201, 403, 404, 409].includes(r.status),
|
|
});
|
|
if (!ok) adminFailRate.add(1);
|
|
|
|
} else if (scenario === 3 && data.listingIds.length >= 3) {
|
|
// --- Bulk moderation ---
|
|
const startIdx = iter % Math.max(1, data.listingIds.length - 3);
|
|
const batchIds = data.listingIds.slice(startIdx, startIdx + 3);
|
|
const payload = JSON.stringify({
|
|
listingIds: batchIds,
|
|
action: iter % 2 === 0 ? 'approve' : 'reject',
|
|
reason: `K6 bulk moderation test — iteration ${iter}`,
|
|
});
|
|
|
|
const res = http.post(`${BASE_URL}/admin/moderation/bulk`, payload, {
|
|
headers,
|
|
tags: { name: 'POST /admin/moderation/bulk' },
|
|
});
|
|
|
|
moderationActionDuration.add(res.timings.duration);
|
|
const ok = check(res, {
|
|
'bulk moderate: status 200|201|403|409': (r) =>
|
|
[200, 201, 403, 409].includes(r.status),
|
|
});
|
|
if (!ok) adminFailRate.add(1);
|
|
|
|
} else if (scenario === 4) {
|
|
// --- Admin dashboard ---
|
|
const res = http.get(`${BASE_URL}/admin/dashboard`, {
|
|
headers,
|
|
tags: { name: 'GET /admin/dashboard' },
|
|
});
|
|
|
|
adminDashboardDuration.add(res.timings.duration);
|
|
const ok = check(res, {
|
|
'dashboard: status 200|403': (r) => r.status === 200 || r.status === 403,
|
|
});
|
|
if (!ok) adminFailRate.add(1);
|
|
|
|
} else if (scenario === 5) {
|
|
// --- Audit logs with various filters ---
|
|
const filters = [
|
|
'',
|
|
'?action=LISTING_APPROVED',
|
|
'?action=USER_BANNED',
|
|
`?startDate=2026-01-01&endDate=2026-12-31`,
|
|
];
|
|
const filter = filters[iter % filters.length];
|
|
|
|
const res = http.get(`${BASE_URL}/admin/audit-logs${filter}`, {
|
|
headers,
|
|
tags: { name: 'GET /admin/audit-logs' },
|
|
});
|
|
|
|
auditLogDuration.add(res.timings.duration);
|
|
const ok = check(res, {
|
|
'audit logs: status 200|403': (r) => r.status === 200 || r.status === 403,
|
|
});
|
|
if (!ok) adminFailRate.add(1);
|
|
|
|
} else {
|
|
// --- User management listing ---
|
|
const queries = [
|
|
'?limit=20',
|
|
'?role=AGENT&limit=10',
|
|
'?isActive=true&limit=20',
|
|
'?search=test&limit=10',
|
|
];
|
|
const query = queries[iter % queries.length];
|
|
|
|
const res = http.get(`${BASE_URL}/admin/users${query}`, {
|
|
headers,
|
|
tags: { name: 'GET /admin/users' },
|
|
});
|
|
|
|
userListDuration.add(res.timings.duration);
|
|
const ok = check(res, {
|
|
'user list: status 200|403': (r) => r.status === 200 || r.status === 403,
|
|
});
|
|
if (!ok) adminFailRate.add(1);
|
|
}
|
|
|
|
sleep(Math.random() * 1.5 + 0.5);
|
|
}
|