test(session): load tests for session api (#9212)

# Which Problems Are Solved

We currently are not able to benchmark the performance of the session
api

# How the Problems Are Solved

Load tests were added to
- use sessions in oidc tokens analog
https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard

# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/7847
This commit is contained in:
Silvan 2025-01-29 13:08:20 +01:00 committed by GitHub
parent 679ab58fa1
commit b10428fb56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 409 additions and 112 deletions

View File

@ -481,7 +481,7 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit
return err return err
}, stmt, args...) }, stmt, args...)
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") return nil, zerrors.ThrowInternal(err, "QUERY-h9TeF", "Errors.Internal")
} }
apps.State, err = q.latestState(ctx, appsTable) apps.State, err = q.latestState(ctx, appsTable)
return apps, err return apps, err

View File

@ -207,7 +207,7 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc
return err return err
}, stmt, args...) }, stmt, args...)
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") return nil, zerrors.ThrowInternal(err, "QUERY-l4bxm", "Errors.Internal")
} }
configs.State, err = q.latestState(ctx, smsConfigsTable) configs.State, err = q.latestState(ctx, smsConfigsTable)
return configs, err return configs, err

View File

@ -33,9 +33,21 @@ introspect: ensure_modules bundle
cd ../../xk6-modules && xk6 build --with xk6-zitadel=. cd ../../xk6-modules && xk6 build --with xk6-zitadel=.
${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/introspection.js --vus ${VUS} --duration ${DURATION} --out csv=output/introspect_${DATE}.csv ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/introspection.js --vus ${VUS} --duration ${DURATION} --out csv=output/introspect_${DATE}.csv
.PHONY: oidc_session
oidc_session: ensure_key_pair ensure_modules bundle
${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/oidc_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/oidc_session_${DATE}.csv
.PHONY: otp_session
otp_session: ensure_key_pair ensure_modules bundle
${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/otp_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/otp_session_${DATE}.csv
.PHONY: password_session
password_session: ensure_key_pair ensure_modules bundle
${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/password_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/otp_session_${DATE}.csv
.PHONY: add_session .PHONY: add_session
add_session: bundle add_session: ensure_modules bundle
${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/session.js --vus ${VUS} --duration ${DURATION} --out csv=output/add_session_${DATE}.csv ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/add_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/add_session_${DATE}.csv
.PHONY: machine_jwt_profile_grant .PHONY: machine_jwt_profile_grant
machine_jwt_profile_grant: ensure_modules ensure_key_pair bundle machine_jwt_profile_grant: ensure_modules ensure_key_pair bundle

View File

@ -20,6 +20,8 @@ The use cases under tests are defined in `src/use_cases`. The implementation of
- `VUS`: Amount of parallel processes execute the test (default is 20) - `VUS`: Amount of parallel processes execute the test (default is 20)
- `DURATION`: Defines how long the tests are executed (default is `200s`) - `DURATION`: Defines how long the tests are executed (default is `200s`)
- `ZITADEL_HOST`: URL of ZITADEL (default is `http://localhost:8080`) - `ZITADEL_HOST`: URL of ZITADEL (default is `http://localhost:8080`)
- `ADMIN_LOGIN_NAME`: Loginanme of a human user with `IAM_OWNER`-role
- `ADMIN_PASSWORD`: password of the human user
To setup the tests we use the credentials of console and log in using an admin. The user must be able to create organizations and all resources inside organizations. To setup the tests we use the credentials of console and log in using an admin. The user must be able to create organizations and all resources inside organizations.
@ -50,6 +52,15 @@ Before you run the tests you need an initialized user. The tests don't implement
* `make add_session` * `make add_session`
setup: creates human users setup: creates human users
test: creates new sessions with user id check test: creates new sessions with user id check
* `make oidc_session`
setup: creates a machine user to create the auth request and session.
test: creates an auth request, a session and links the session to the auth request. Implementation of [this flow](https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard).
* `make otp_session`
setup: creates 1 human user for each VU and adds email OTP to it
test: creates a session based on the login name of the user, sets the email OTP challenge to the session and afterwards checks the OTP code
* `make password_session`
setup: creates 1 human user for each VU and adds email OTP to it
test: creates a session based on the login name of the user and checks for the password on a second step
* `make machine_jwt_profile_grant` * `make machine_jwt_profile_grant`
setup: generates private/public key, creates machine users, adds a key setup: generates private/public key, creates machine users, adds a key
test: creates a token and calls user info test: creates a token and calls user info

View File

@ -27,7 +27,7 @@
}, },
"scripts": { "scripts": {
"bundle": "webpack", "bundle": "webpack",
"lint": "prettier --check src", "lint": "prettier --check src/**",
"lint:fix": "prettier --write src" "lint:fix": "prettier --write src"
} }
} }

View File

@ -21,10 +21,27 @@ export function loginByUsernamePassword(user: User) {
} }
const initLoginTrend = new Trend('login_ui_init_login_duration', true); const initLoginTrend = new Trend('login_ui_init_login_duration', true);
function initLogin(): Response { export function initLogin(clientId?: string): Response {
const response = http.get(url('/oauth/v2/authorize', { searchParams: Client() })); let params = {};
let expectedStatus = 200;
if (clientId) {
params = {
headers: {
'x-zitadel-login-client': clientId,
},
redirects: 0,
};
expectedStatus = 302;
}
const response = http.get(
url('/oauth/v2/authorize', {
searchParams: Client(),
}),
params,
);
check(response, { check(response, {
'authorize status ok': (r) => r.status == 200 || fail(`init login failed: ${r}`), 'authorize status ok': (r) => r.status == expectedStatus || fail(`init login failed: ${JSON.stringify(r)}`),
}); });
initLoginTrend.add(response.timings.duration); initLoginTrend.add(response.timings.duration);
return response; return response;

View File

@ -0,0 +1,25 @@
import http from 'k6/http';
import { Trend } from 'k6/metrics';
import url from './url';
import { check, fail } from 'k6';
const addIAMMemberTrend = new Trend('membership_iam_member', true);
export async function addIAMMember(userId: string, roles: string[], accessToken: string): Promise<void> {
const res = await http.post(
url('/admin/v1/members'),
JSON.stringify({
userId: userId,
roles: roles,
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
},
);
check(res, {
'member added successful': (r) => r.status == 200 || fail(`unable add member: ${JSON.stringify(res)}`),
});
addIAMMemberTrend.add(res.timings.duration);
}

View File

@ -1,9 +1,9 @@
import { JSONObject, check, fail } from 'k6'; import { JSONObject, check, fail } from 'k6';
import encoding from 'k6/encoding'; import encoding from 'k6/encoding';
import http, { RequestBody } from 'k6/http'; import http, { RequestBody, Response } from 'k6/http';
import { Trend } from 'k6/metrics'; import { Trend } from 'k6/metrics';
import url from './url'; import url from './url';
import { Config } from './config'; import { Client, Config } from './config';
// @ts-ignore Import module // @ts-ignore Import module
import zitadel from 'k6/x/zitadel'; import zitadel from 'k6/x/zitadel';
@ -79,9 +79,11 @@ export function introspect(jwt: string, token: string) {
const clientCredentialsTrend = new Trend('oidc_client_credentials_duration', true); const clientCredentialsTrend = new Trend('oidc_client_credentials_duration', true);
export function clientCredentials(clientId: string, clientSecret: string): Promise<Tokens> { export function clientCredentials(clientId: string, clientSecret: string): Promise<Tokens> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const response = http.asyncRequest('POST', configuration().token_endpoint, const response = http.asyncRequest(
'POST',
configuration().token_endpoint,
{ {
grant_type: "client_credentials", grant_type: 'client_credentials',
scope: 'openid profile urn:zitadel:iam:org:project:id:zitadel:aud', scope: 'openid profile urn:zitadel:iam:org:project:id:zitadel:aud',
client_id: clientId, client_id: clientId,
client_secret: clientSecret, client_secret: clientSecret,
@ -91,26 +93,26 @@ export function clientCredentials(clientId: string, clientSecret: string): Promi
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
}, },
); );
response.then((res) => { response.then((res) => {
check(res, { check(res, {
'client credentials status ok': (r) => r.status === 200, 'client credentials status ok': (r) => r.status === 200,
}) || reject(`client credentials request failed (client id: ${clientId}) status: ${res.status} body: ${res.body}`); }) || reject(`client credentials request failed (client id: ${clientId}) status: ${res.status} body: ${res.body}`);
clientCredentialsTrend.add(res.timings.duration); clientCredentialsTrend.add(res.timings.duration);
const tokens = new Tokens(res.json() as JSONObject) const tokens = new Tokens(res.json() as JSONObject);
check(tokens, { check(tokens, {
'client credentials token ok': (t) => t.accessToken !== undefined, 'client credentials token ok': (t) => t.accessToken !== undefined,
}) || reject(`client credentials access token missing (client id: ${clientId}`); }) || reject(`client credentials access token missing (client id: ${clientId}`);
resolve(tokens) resolve(tokens);
}); });
}); });
} }
export interface TokenRequest { export interface TokenRequest {
payload(): RequestBody; payload(): RequestBody;
headers(): { [name: string]: string; }; headers(): { [name: string]: string };
} }
const privateKey = open('../.keys/key.pem'); const privateKey = open('../.keys/key.pem');
@ -126,46 +128,83 @@ export class JWTProfileRequest implements TokenRequest {
this.keyPayload = { this.keyPayload = {
userId: userId, userId: userId,
// 1 minute // 1 minute
expiration: 60*1_000_000_000, expiration: 60 * 1_000_000_000,
keyId: keyId, keyId: keyId,
}; };
} }
payload(): RequestBody{ payload(): RequestBody {
const assertion = zitadel.signJWTProfileAssertion( const assertion = zitadel.signJWTProfileAssertion(this.keyPayload.userId, this.keyPayload.keyId, {
this.keyPayload.userId, audience: [Config.host],
this.keyPayload.keyId, expiration: this.keyPayload.expiration,
{ key: privateKey,
audience: [Config.host], });
expiration: this.keyPayload.expiration,
key: privateKey
});
return { return {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
scope: 'openid', scope: 'openid urn:zitadel:iam:org:project:id:zitadel:aud',
assertion: `${assertion}` assertion: `${assertion}`,
}; };
}; }
public headers(): { [name: string]: string; } { public headers(): { [name: string]: string } {
return { return {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded',
}; };
}; }
} }
const tokenDurationTrend = new Trend('oidc_token_duration', true); const tokenDurationTrend = new Trend('oidc_token_duration', true);
export async function token(request: TokenRequest): Promise<Tokens> { export async function token(request: TokenRequest): Promise<Tokens> {
return http.asyncRequest('POST', configuration().token_endpoint, return http
request.payload(), .asyncRequest('POST', configuration().token_endpoint, request.payload(), {
{
headers: request.headers(), headers: request.headers(),
}, })
).then((res) => { .then((res) => {
tokenDurationTrend.add(res.timings.duration); tokenDurationTrend.add(res.timings.duration);
check(res, { check(res, {
'token status ok': (r) => r.status === 200, 'token status ok': (r) => r.status === 200,
'access token returned': (r) => r.json('access_token')! != undefined && r.json('access_token')! != '', 'access token returned': (r) => r.json('access_token')! != undefined && r.json('access_token')! != '',
});
return new Tokens(res.json() as JSONObject);
}); });
return new Tokens(res.json() as JSONObject); }
const authRequestBiIDTrend = new Trend('oidc_auth_request_by_id_duration', true);
export async function authRequestByID(id: string, tokens: any): Promise<Response> {
const response = http.get(url(`/v2/oidc/auth_requests/${id}`), {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
}); });
}; check(response, {
'authorize status ok': (r) => r.status == 200 || fail(`auth request by failed: ${JSON.stringify(r)}`),
});
authRequestBiIDTrend.add(response.timings.duration);
return response;
}
const finalizeAuthRequestTrend = new Trend('oidc_auth_requst_by_id_duration', true);
export async function finalizeAuthRequest(id: string, session: any, tokens: any): Promise<Response> {
const res = await http.post(
url(`/v2/oidc/auth_requests/${id}`),
JSON.stringify({
session: {
sessionId: session.sessionId,
sessionToken: session.sessionToken,
},
}),
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
'Content-Type': 'application/json',
// 'Accept': 'application/json',
'x-zitadel-login-client': tokens.info.client_id,
},
},
);
check(res, {
'finalize auth request status ok': (r) => r.status == 200 || fail(`finalize auth request failed: ${JSON.stringify(r)}`),
});
finalizeAuthRequestTrend.add(res.timings.duration);
return res;
}

View File

@ -3,33 +3,23 @@ import { Org } from './org';
import http from 'k6/http'; import http from 'k6/http';
import url from './url'; import url from './url';
import { check } from 'k6'; import { check } from 'k6';
import { User } from './user';
export type Session = { export type Session = {
challenges: any;
id: string; id: string;
token: string;
}; };
const addSessionTrend = new Trend('session_add_session_duration', true); const addSessionTrend = new Trend('session_add_session_duration', true);
export function createSession(user: User, org: Org, accessToken: string): Promise<Session> { export function createSession(org: Org, accessToken: string, checks?: any): Promise<Session> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let response = http.asyncRequest( let response = http.asyncRequest('POST', url('/v2/sessions'), checks ? JSON.stringify({ checks: checks }) : null, {
'POST', headers: {
url('/v2beta/sessions'), authorization: `Bearer ${accessToken}`,
JSON.stringify({ 'Content-Type': 'application/json',
checks: { 'x-zitadel-orgid': org.organizationId,
user: {
userId: user.userId,
}
}
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
}, },
); });
response.then((res) => { response.then((res) => {
check(res, { check(res, {
'add Session status ok': (r) => r.status === 201, 'add Session status ok': (r) => r.status === 201,
@ -40,3 +30,29 @@ export function createSession(user: User, org: Org, accessToken: string): Promis
}); });
}); });
} }
const setSessionTrend = new Trend('session_set_session_duration', true);
export function setSession(id: string, session: any, accessToken: string, challenges?: any, checks?: any): Promise<Session> {
const body = {
sessionToken: session.sessionToken,
checks: checks ? checks : null,
challenges: challenges ? challenges : null,
};
return new Promise((resolve, reject) => {
let response = http.asyncRequest('PATCH', url(`/v2/sessions/${id}`), JSON.stringify(body), {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
// 'x-zitadel-orgid': org.organizationId,
},
});
response.then((res) => {
check(res, {
'set Session status ok': (r) => r.status === 200,
}) || reject(`unable to set Session status: ${res.status} body: ${res.body}`);
setSessionTrend.add(res.timings.duration);
resolve(res.json() as Session);
});
});
}

View File

@ -1,7 +1,7 @@
import { loginByUsernamePassword } from '../login_ui'; import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org'; import { createOrg, removeOrg } from '../org';
import {createMachine, User, addMachineSecret} from '../user'; import { createMachine, User, addMachineSecret } from '../user';
import {clientCredentials, userinfo} from '../oidc'; import { clientCredentials, userinfo } from '../oidc';
import { Config, MaxVUs } from '../config'; import { Config, MaxVUs } from '../config';
export async function setup() { export async function setup() {
@ -37,10 +37,9 @@ export async function setup() {
} }
export default function (data: any) { export default function (data: any) {
clientCredentials(data.machines[__VU - 1].loginName, data.machines[__VU - 1].password) clientCredentials(data.machines[__VU - 1].loginName, data.machines[__VU - 1].password).then((token) => {
.then((token) => { userinfo(token.accessToken!);
userinfo(token.accessToken!) });
})
} }
export function teardown(data: any) { export function teardown(data: any) {

View File

@ -1,7 +1,7 @@
import { loginByUsernamePassword } from '../login_ui'; import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org'; import { createOrg, removeOrg } from '../org';
import {createMachine, User, addMachineKey} from '../user'; import { createMachine, User, addMachineKey } from '../user';
import {JWTProfileRequest, token, userinfo} from '../oidc'; import { JWTProfileRequest, token, userinfo } from '../oidc';
import { Config, MaxVUs } from '../config'; import { Config, MaxVUs } from '../config';
import encoding from 'k6/encoding'; import encoding from 'k6/encoding';
@ -10,10 +10,10 @@ const publicKey = encoding.b64encode(open('../.keys/key.pem.pub'));
export async function setup() { export async function setup() {
const tokens = loginByUsernamePassword(Config.admin as User); const tokens = loginByUsernamePassword(Config.admin as User);
console.info('setup: admin signed in'); console.info('setup: admin signed in');
const org = await createOrg(tokens.accessToken!); const org = await createOrg(tokens.accessToken!);
console.info(`setup: org (${org.organizationId}) created`); console.info(`setup: org (${org.organizationId}) created`);
let machines = ( let machines = (
await Promise.all( await Promise.all(
Array.from({ length: MaxVUs() }, (_, i) => { Array.from({ length: MaxVUs() }, (_, i) => {
@ -24,16 +24,11 @@ export async function setup() {
return { userId: machine.userId, loginName: machine.loginNames[0] }; return { userId: machine.userId, loginName: machine.loginNames[0] };
}); });
console.info(`setup: ${machines.length} machines created`); console.info(`setup: ${machines.length} machines created`);
let keys = ( let keys = (
await Promise.all( await Promise.all(
machines.map((machine) => { machines.map((machine) => {
return addMachineKey( return addMachineKey(machine.userId, org, tokens.accessToken!, publicKey);
machine.userId,
org,
tokens.accessToken!,
publicKey,
);
}), }),
) )
).map((key, i) => { ).map((key, i) => {
@ -45,7 +40,7 @@ export async function setup() {
} }
export default function (data: any) { export default function (data: any) {
token(new JWTProfileRequest(data.machines[__VU - 1].userId, data.machines[__VU - 1].keyId)) token(new JWTProfileRequest(data.machines[__VU - 1].userId, data.machines[__VU - 1].keyId));
} }
export function teardown(data: any) { export function teardown(data: any) {

View File

@ -1,7 +1,7 @@
import { loginByUsernamePassword } from '../login_ui'; import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org'; import { createOrg, removeOrg } from '../org';
import {createMachine, User, addMachineKey} from '../user'; import { createMachine, User, addMachineKey } from '../user';
import {JWTProfileRequest, token, userinfo} from '../oidc'; import { JWTProfileRequest, token, userinfo } from '../oidc';
import { Config } from '../config'; import { Config } from '../config';
import encoding from 'k6/encoding'; import encoding from 'k6/encoding';
@ -10,20 +10,20 @@ const publicKey = encoding.b64encode(open('../.keys/key.pem.pub'));
export async function setup() { export async function setup() {
const tokens = loginByUsernamePassword(Config.admin as User); const tokens = loginByUsernamePassword(Config.admin as User);
console.info('setup: admin signed in'); console.info('setup: admin signed in');
const org = await createOrg(tokens.accessToken!); const org = await createOrg(tokens.accessToken!);
console.info(`setup: org (${org.organizationId}) created`); console.info(`setup: org (${org.organizationId}) created`);
const machine = await createMachine(`zitachine`, org, tokens.accessToken!); const machine = await createMachine(`zitachine`, org, tokens.accessToken!);
console.info(`setup: machine ${machine.userId} created`); console.info(`setup: machine ${machine.userId} created`);
const key = await addMachineKey(machine.userId, org, tokens.accessToken!, publicKey); const key = await addMachineKey(machine.userId, org, tokens.accessToken!, publicKey);
console.info(`setup: key ${key.keyId} added`); console.info(`setup: key ${key.keyId} added`);
return { tokens, machine: {userId: machine.userId, keyId: key.keyId}, org }; return { tokens, machine: { userId: machine.userId, keyId: key.keyId }, org };
} }
export default function (data: any) { export default function (data: any) {
token(new JWTProfileRequest(data.machine.userId, data.machine.keyId)) token(new JWTProfileRequest(data.machine.userId, data.machine.keyId));
} }
export function teardown(data: any) { export function teardown(data: any) {

View File

@ -1,9 +1,9 @@
import { loginByUsernamePassword } from '../login_ui'; import { loginByUsernamePassword } from '../../login_ui';
import { createOrg, removeOrg } from '../org'; import { createOrg, removeOrg } from '../../org';
import { User, createHuman } from '../user'; import { User, createHuman } from '../../user';
import { Trend } from 'k6/metrics'; import { Trend } from 'k6/metrics';
import { Config, MaxVUs } from '../config'; import { Config, MaxVUs } from '../../config';
import { createSession } from '../session'; import { createSession } from '../../session';
import { check } from 'k6'; import { check } from 'k6';
export async function setup() { export async function setup() {
@ -27,12 +27,16 @@ export async function setup() {
const addSessionTrend = new Trend('add_session_duration', true); const addSessionTrend = new Trend('add_session_duration', true);
export default async function (data: any) { export default async function (data: any) {
const start = new Date(); const start = new Date();
const session = await createSession(data.users[__VU - 1], data.org, data.tokens.accessToken); const session = await createSession(data.org, data.tokens.accessToken, {
user: {
userId: data.users[__VU - 1].userId,
},
});
check(session, { check(session, {
'add session is status ok': (s) => s.id !== "", 'add session is status ok': (s) => s.id !== '',
}); });
addSessionTrend.add(new Date().getTime() - start.getTime()); addSessionTrend.add(new Date().getTime() - start.getTime());
} }

View File

@ -0,0 +1,53 @@
import { loginByUsernamePassword, initLogin } from '../../login_ui';
import { createOrg, removeOrg } from '../../org';
import { User, addMachineKey, createMachine } from '../../user';
import { Trend } from 'k6/metrics';
import { Config } from '../../config';
import { check } from 'k6';
import { finalizeAuthRequest, JWTProfileRequest, token } from '../../oidc';
import { createSession } from '../../session';
import encoding from 'k6/encoding';
import { addIAMMember } from '../../membership';
const publicKey = encoding.b64encode(open('../.keys/key.pem.pub'));
export async function setup() {
const adminTokens = loginByUsernamePassword(Config.admin as User);
console.log('setup: admin signed in');
const org = await createOrg(adminTokens.accessToken!);
console.log(`setup: org (${org.organizationId}) created`);
const loginUser = await createMachine('load-test', org, adminTokens.accessToken!);
const loginUserKey = await addMachineKey(loginUser.userId, org, adminTokens.accessToken!, publicKey);
await addIAMMember(loginUser.userId, ['IAM_LOGIN_CLIENT'], adminTokens.accessToken!);
const tokens = await token(new JWTProfileRequest(loginUser.userId, loginUserKey.keyId));
return { tokens, user: loginUser, key: loginUserKey, org, adminTokens };
}
// implements the flow described in
// https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard
const addSessionTrend = new Trend('oidc_session_duration', true);
export default async function (data: any) {
const start = new Date();
const authorizeResponse = initLogin(data.tokens.info.client_id);
const authRequestId = new URLSearchParams(authorizeResponse.headers['Location']).values().next().value;
check(authRequestId, {
'auth request id returned': (s) => s !== '',
});
const session = await createSession(data.org, data.tokens.accessToken, {
user: {
userId: data.user.userId,
},
});
await finalizeAuthRequest(authRequestId!, session, data.tokens!);
addSessionTrend.add(new Date().getTime() - start.getTime());
}
export function teardown(data: any) {
removeOrg(data.org, data.adminTokens.accessToken);
}

View File

@ -0,0 +1,57 @@
import { loginByUsernamePassword } from '../../login_ui';
import { createOrg, removeOrg } from '../../org';
import { createHuman, setEmailOTPOnHuman, User } from '../../user';
import { Trend } from 'k6/metrics';
import { Config, MaxVUs } from '../../config';
import { createSession, setSession } from '../../session';
export async function setup() {
const tokens = loginByUsernamePassword(Config.admin as User);
console.log('setup: admin signed in');
const org = await createOrg(tokens.accessToken!);
console.log(`setup: org (${org.organizationId}) created`);
const humanPromises = Array.from({ length: MaxVUs() }, (_, i) => {
return createHuman(`zitizen-${i}`, org, tokens.accessToken!);
});
const humans = (await Promise.all(humanPromises)).map((user) => {
setEmailOTPOnHuman(user, org, tokens.accessToken!);
return { userId: user.userId, loginName: user.loginNames[0], password: 'Password1!' };
});
console.log(`setup: ${humans.length} users created`);
return { tokens, org, users: humans };
}
// implements the flow described in
// https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard
const otpSessionTrend = new Trend('otp_session_duration', true);
export default async function (data: any) {
const start = new Date();
let session = await createSession(data.org, data.tokens.accessToken, {
user: {
loginName: data.users[__VU - 1].loginName,
},
});
const sessionId = (session as any).sessionId;
session = await setSession(sessionId, session, data.tokens.accessToken, {
otpEmail: {
return_code: {},
},
});
session = await setSession(sessionId, session, data.tokens.accessToken, null, {
otpEmail: {
code: session.challenges.otpEmail,
},
});
otpSessionTrend.add(new Date().getTime() - start.getTime());
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
}

View File

@ -0,0 +1,49 @@
import { loginByUsernamePassword } from '../../login_ui';
import { createOrg, removeOrg } from '../../org';
import { createHuman, setEmailOTPOnHuman, User } from '../../user';
import { Trend } from 'k6/metrics';
import { Config, MaxVUs } from '../../config';
import { createSession, setSession } from '../../session';
export async function setup() {
const tokens = loginByUsernamePassword(Config.admin as User);
console.log('setup: admin signed in');
const org = await createOrg(tokens.accessToken!);
console.log(`setup: org (${org.organizationId}) created`);
const humanPromises = Array.from({ length: MaxVUs() }, (_, i) => {
return createHuman(`zitizen-${i}`, org, tokens.accessToken!);
});
const humans = (await Promise.all(humanPromises)).map((user) => {
setEmailOTPOnHuman(user, org, tokens.accessToken!);
return { userId: user.userId, loginName: user.loginNames[0], password: 'Password1!' };
});
console.log(`setup: ${humans.length} users created`);
return { tokens, org, users: humans };
}
const passwordSessionTrend = new Trend('password_session_duration', true);
export default async function (data: any) {
const start = new Date();
let session = await createSession(data.org, data.tokens.accessToken, {
user: {
loginName: data.users[__VU - 1].loginName,
},
});
const sessionId = (session as any).sessionId;
session = await setSession(sessionId, session, data.tokens.accessToken, null, {
password: {
password: data.users[__VU - 1].password,
},
});
passwordSessionTrend.add(new Date().getTime() - start.getTime());
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
}

View File

@ -19,7 +19,7 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let response = http.asyncRequest( let response = http.asyncRequest(
'POST', 'POST',
url('/v2beta/users/human'), url('/v2/users/human'),
JSON.stringify({ JSON.stringify({
username: username, username: username,
organization: { organization: {
@ -69,6 +69,23 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr
}); });
} }
const setEmailOTPOnHumanTrend = new Trend('set_human_email_otp_duration', true);
export async function setEmailOTPOnHuman(user: User, org: Org, accessToken: string): Promise<void> {
const response = await http.asyncRequest('POST', url(`/v2/users/${user.userId}/otp_email`), null, {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
check(response, {
'set email otp status ok': (r) => r.status === 200,
});
setEmailOTPOnHumanTrend.add(response.timings.duration);
return;
}
const updateHumanTrend = new Trend('update_human_duration', true); const updateHumanTrend = new Trend('update_human_duration', true);
export function updateHuman( export function updateHuman(
payload: any = {}, payload: any = {},
@ -172,8 +189,8 @@ export function addMachinePat(userId: string, org: Org, accessToken: string): Pr
} }
export type MachineSecret = { export type MachineSecret = {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
}; };
const addMachineSecretTrend = new Trend('user_add_machine_secret_duration', true); const addMachineSecretTrend = new Trend('user_add_machine_secret_duration', true);
@ -204,20 +221,23 @@ export type MachineKey = {
const addMachineKeyTrend = new Trend('user_add_machine_key_duration', true); const addMachineKeyTrend = new Trend('user_add_machine_key_duration', true);
export function addMachineKey(userId: string, org: Org, accessToken: string, publicKey?: string): Promise<MachineKey> { export function addMachineKey(userId: string, org: Org, accessToken: string, publicKey?: string): Promise<MachineKey> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let response = http.asyncRequest('POST', url(`/management/v1/users/${userId}/keys`), let response = http.asyncRequest(
JSON.stringify({ 'POST',
type: 'KEY_TYPE_JSON', url(`/management/v1/users/${userId}/keys`),
userId: userId, JSON.stringify({
// base64 encoded public key type: 'KEY_TYPE_JSON',
publicKey: publicKey userId: userId,
}), // base64 encoded public key
{ publicKey: publicKey,
headers: { }),
authorization: `Bearer ${accessToken}`, {
'Content-Type': 'application/json', headers: {
'x-zitadel-orgid': org.organizationId, authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
}, },
}); );
response.then((res) => { response.then((res) => {
check(res, { check(res, {
'generate machine key status ok': (r) => r.status === 200, 'generate machine key status ok': (r) => r.status === 200,

View File

@ -5,7 +5,7 @@ const GlobEntries = require('webpack-glob-entries');
module.exports = { module.exports = {
mode: 'production', mode: 'production',
entry: GlobEntries('./src/use_cases/*.ts'), // Generates multiple entry for each test entry: GlobEntries('./src/use_cases/**/*.ts'), // Generates multiple entry for each test
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
libraryTarget: 'commonjs', libraryTarget: 'commonjs',