Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions infra/k6/common/api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class ApiClient {
get(path, params = {}, options = {}) {
const query = this._buildQuery(params);
const res = http.get(`${this.baseUrl}${path}${query}`, this._buildOptions(options));
this._logError(res, 'GET', path);
this._logError(res, 'GET', path, options);
return res;
}

Expand All @@ -96,7 +96,7 @@ export class ApiClient {
payload,
this._buildOptions(options, useJsonDefault),
);
this._logError(res, 'POST', path);
this._logError(res, 'POST', path, options);
return res;
}

Expand All @@ -114,7 +114,7 @@ export class ApiClient {
payload,
this._buildOptions(options, useJsonDefault),
);
this._logError(res, 'PATCH', path);
this._logError(res, 'PATCH', path, options);
return res;
}

Expand All @@ -132,7 +132,7 @@ export class ApiClient {
payload,
this._buildOptions(options, useJsonDefault),
);
this._logError(res, 'PUT', path);
this._logError(res, 'PUT', path, options);
return res;
}

Expand All @@ -143,7 +143,7 @@ export class ApiClient {
*/
delete(path, options = {}) {
const res = http.del(`${this.baseUrl}${path}`, null, this._buildOptions(options, false));
this._logError(res, 'DELETE', path);
this._logError(res, 'DELETE', path, options);
return res;
}

Expand All @@ -153,13 +153,24 @@ export class ApiClient {
* @param {import('k6/http').RefinedResponse<any>} res - Объект ответа k6.
* @param {string} method - Название HTTP метода для лога.
* @param {string} path - Путь запроса для лога.
* @param {Object} [options] - Доп. параметры запроса.
*/
_logError(res, method, path) {
_logError(res, method, path, options = {}) {
const expectedStatuses = Array.isArray(options.expectedStatuses)
? options.expectedStatuses
: null;
const isOk = expectedStatuses
? (r) => expectedStatuses.includes(r.status)
: (r) => r.status >= 200 && r.status < 300;
const statusLabel = expectedStatuses
? `statuses [${expectedStatuses.join(',')}]`
: 'statuses 2xx';

check(res, {
[`${method} ${path} status is 2xx`]: (r) => r.status >= 200 && r.status < 300,
[`${method} ${path} ${statusLabel} is expected`]: isOk,
});

if (res.status >= 400) {
if (!isOk(res)) {
console.error(`Error on ${method} ${path}: [${res.status}] ${res.body}`);
}
}
Expand Down
4 changes: 4 additions & 0 deletions infra/k6/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"test:all": "k6 run scenarios/stress-full.js",
"test:auth": "k6 run scenarios/auth.js",
"test:teams": "k6 run scenarios/teams.js",
"test:teams-members": "k6 run scenarios/teams-members.js",
"test:teams-invitations": "k6 run scenarios/teams-invitations.js",
"test:teams-settings": "k6 run scenarios/teams-settings.js",
"test:teams-me": "k6 run scenarios/teams-me.js",
"test:projects": "k6 run scenarios/projects.js",
"test:users": "k6 run scenarios/users.js",
"test:board": "k6 run scenarios/board-full.js",
Expand Down
119 changes: 119 additions & 0 deletions infra/k6/scenarios/teams-invitations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { SharedArray } from 'k6/data';
import { sleep } from 'k6';
import { GET_OPTIONS } from '../common/config.js';
import getAuthUser from '../shared/get-auth-user.js';

const users = new SharedArray('test users', function () {
return JSON.parse(open('../data/users.json'));
});
const teams = new SharedArray('test teams', function () {
return JSON.parse(open('../data/teams.json'));
});

const baseOptions = GET_OPTIONS();
baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, {
'http_req_duration{name:auth-sign-in}': ['p(95)<333'],
'http_req_duration{name:teams-invitations-list}': ['p(95)<333'],
'http_req_duration{name:teams-invitations-get}': ['p(95)<333'],
'http_req_duration{name:teams-invitations-update}': ['p(95)<333'],
'http_req_duration{name:teams-invitations-create}': ['p(95)<333'],
'http_req_duration{name:teams-invitations-create-duplicate}': ['p(95)<333'],
'http_req_duration{name:teams-invitations-accept}': ['p(95)<333'],
});

export const options = baseOptions;

function buildUserIndex(list) {
const index = {};
for (const u of list) index[u.email] = u;
return index;
}

const userByEmail = buildUserIndex(users);

export default function () {
const idx = (__VU - 1) % teams.length;
const team = teams[idx];
const owner = users[idx % users.length];
const { client } = getAuthUser(owner);

sleep(1);

// --- GET /teams/:slug/invitations ---
const listRes = client.get(
`/teams/${team.slug}/invitations`,
{},
{
tags: { name: 'teams-invitations-list' },
},
);

sleep(1);

const listBody =
listRes && listRes.status >= 200 && listRes.status < 300 ? listRes.json() : null;
const items = listBody && Array.isArray(listBody.items) ? listBody.items : [];
const invite = items.length ? items[0] : null;

if (invite && invite.code) {
// --- GET /teams/:slug/invitations/:code ---
client.get(
`/teams/${team.slug}/invitations/${invite.code}`,
{},
{
tags: { name: 'teams-invitations-get' },
},
);

sleep(1);

// --- PATCH /teams/:slug/invitations/:code ---
client.patch(
`/teams/${team.slug}/invitations/${invite.code}`,
{ role: 'member' },
{ tags: { name: 'teams-invitations-update' } },
);

sleep(1);

// --- POST /teams/:slug/invitations/:code/accept ---
if (__ITER === 0 && invite.email && userByEmail[invite.email]) {
const invitedUser = userByEmail[invite.email];
const { client: invitedClient } = getAuthUser(invitedUser, {
tags: { name: 'auth-sign-in' },
});

invitedClient.post(
`/teams/${team.slug}/invitations/${invite.code}/accept`,
{},
{ tags: { name: 'teams-invitations-accept' } },
);
}
}

sleep(1);

// --- POST /teams/:slug/invitations ---
const randomEmail = `k6_invite_${__VU}_${__ITER}@tasktracker.local`;
client.post(
`/teams/${team.slug}/invitations`,
{ email: randomEmail, role: 'member' },
{
tags: { name: 'teams-invitations-create' },
},
);

sleep(1);

// --- POST /teams/:slug/invitations (duplicate) ---
client.post(
`/teams/${team.slug}/invitations`,
{ email: randomEmail, role: 'member' },
{
tags: { name: 'teams-invitations-create-duplicate' },
expectedStatuses: [400],
},
);

sleep(1);
}
34 changes: 34 additions & 0 deletions infra/k6/scenarios/teams-me.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { SharedArray } from 'k6/data';
import { sleep } from 'k6';
import { GET_OPTIONS } from '../common/config.js';
import getAuthUser from '../shared/get-auth-user.js';

const users = new SharedArray('test users', function () {
return JSON.parse(open('../data/users.json'));
});

const baseOptions = GET_OPTIONS();
baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, {
'http_req_duration{name:auth-sign-in}': ['p(95)<333'],
'http_req_duration{name:users-me-teams}': ['p(95)<333'],
'http_req_duration{name:users-me-invites}': ['p(95)<333'],
});

export const options = baseOptions;

export default function () {
const user = users[(__VU - 1) % users.length];
const { client } = getAuthUser(user);

sleep(1);

// --- GET /users/me/teams ---
client.get('/users/me/teams', {}, { tags: { name: 'users-me-teams' } });

sleep(1);

// --- GET /users/me/invites ---
client.get('/users/me/invites', {}, { tags: { name: 'users-me-invites' } });

sleep(1);
}
60 changes: 60 additions & 0 deletions infra/k6/scenarios/teams-members.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { SharedArray } from 'k6/data';
import { sleep } from 'k6';
import { GET_OPTIONS } from '../common/config.js';
import getAuthUser from '../shared/get-auth-user.js';

const users = new SharedArray('test users', function () {
return JSON.parse(open('../data/users.json'));
});
const teams = new SharedArray('test teams', function () {
return JSON.parse(open('../data/teams.json'));
});

const baseOptions = GET_OPTIONS();
baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, {
'http_req_duration{name:auth-sign-in}': ['p(95)<333'],
'http_req_duration{name:teams-members-list}': ['p(95)<333'],
'http_req_duration{name:teams-members-update}': ['p(95)<333'],
});

export const options = baseOptions;

function pickTargetMember(items = []) {
if (!items.length) return null;
const notOwner = items.find((m) => m.role && m.role !== 'owner');
return notOwner || (items.length > 1 ? items[1] : items[0]);
}

export default function () {
const idx = (__VU - 1) % teams.length;
const team = teams[idx];
const user = users[idx % users.length];
const { client } = getAuthUser(user);

sleep(1);

// --- GET /teams/:slug/members ---
const membersRes = client.get(
`/teams/${team.slug}/members`,
{},
{
tags: { name: 'teams-members-list' },
},
);

const members = membersRes.json().items || [];
const target = pickTargetMember(members);

sleep(1);

// --- PATCH /teams/:slug/members/:userId ---
if (target && target.id) {
client.patch(
`/teams/${team.slug}/members/${target.id}`,
{ role: 'member' },
{ tags: { name: 'teams-members-update' } },
);
}

sleep(1);
}
51 changes: 51 additions & 0 deletions infra/k6/scenarios/teams-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { SharedArray } from 'k6/data';
import { sleep } from 'k6';
import { GET_OPTIONS } from '../common/config.js';
import getAuthUser from '../shared/get-auth-user.js';

const users = new SharedArray('test users', function () {
return JSON.parse(open('../data/users.json'));
});
const teams = new SharedArray('test teams', function () {
return JSON.parse(open('../data/teams.json'));
});
const tags = new SharedArray('test tags', function () {
return JSON.parse(open('../data/tags.json'));
});

const baseOptions = GET_OPTIONS();
baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, {
'http_req_duration{name:auth-sign-in}': ['p(95)<333'],
'http_req_duration{name:teams-tags-sync}': ['p(95)<333'],
});

export const options = baseOptions;

function pickTags(count = 3) {
if (!tags.length) return ['k6_tag'];
const start = (__VU - 1) % tags.length;
const selected = [];
for (let i = 0; i < Math.min(count, tags.length); i++) {
const idx = (start + i) % tags.length;
selected.push(tags[idx].name);
}
return selected;
}

export default function () {
const idx = (__VU - 1) % teams.length;
const team = teams[idx];
const user = users[idx % users.length];
const { client } = getAuthUser(user);

sleep(1);

// --- PUT /teams/:slug/tags ---
client.put(
`/teams/${team.slug}/tags`,
{ tags: pickTags(3) },
{ tags: { name: 'teams-tags-sync' } },
);

sleep(1);
}
Loading
Loading