Skip to content

Commit

Permalink
Dev (#2)
Browse files Browse the repository at this point in the history
* setup project + first uc

* finish helper for loading related resources

* add default test profiles

* add uc user-list; logic for usermix

* refactor helpers

* finish show-user-list uc

* refactor show-start; stub oidc-provider uc

* clean-up + csv-output
  • Loading branch information
clauyan authored Oct 10, 2024
1 parent fbbe455 commit d87e76a
Show file tree
Hide file tree
Showing 22 changed files with 2,400 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

loadtest/data/users.json
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"eslint.useFlatConfig": true
}
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,37 @@
# schulportal-load-tests

Load and performance tests for the schulportal

## How to run

You will need a working installation of k6. ([How to install k6](https://grafana.com/docs/k6/latest/set-up/install-k6/)).

Tests are categorized as

```
spike
stress
breakpoint
```

To run all usecases with the stress-configuration against `https://example.env/`:

```sh
./run.sh "https://example.env/" stress
```

And you can selectively run usecases by providing a regex for the filename:

```sh
./run.sh "https://example.env/" stress login
```

### Configuration

You have to provide `loadtest/data/users.json` with usernames, passwords and role for the tests to work. An example is provided in `loadtest/data/`.

## Development

```sh
npm run check # to format, lint, typecheck the code
```
23 changes: 23 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";

export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{
languageOptions: {
globals: globals.node,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
ignores: ["eslint.config.mjs"],
},
pluginJs.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
];
7 changes: 7 additions & 0 deletions loadtest/data/users.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"username": "myUsername",
"password": "myPassword",
"role": "role value; see loadtest/util/users.ts"
}
]
10 changes: 10 additions & 0 deletions loadtest/usecases/1_goto-sp-oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { sleep } from "k6";
import showStart from "../usecases/1_show-start.ts";
import { getDefaultAdminMix } from "../util/users.ts";
import { goToOIDCServiceProvider } from "../util/page.ts";

export default function main(users = getDefaultAdminMix()) {
showStart(users);
goToOIDCServiceProvider();
sleep(1);
}
72 changes: 72 additions & 0 deletions loadtest/usecases/1_login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { check, fail, group, sleep } from "k6";
import http from "k6/http";
import { defaultHttpCheck, defaultTimingCheck } from "../util/checks.ts";
import { getDefaultOptions, getFrontendUrl } from "../util/config.ts";
import { loadLinkedResourcesAndCheck } from "../util/load-linked-resources.ts";
import { getDefaultUserMix } from "../util/users.ts";

const SPSH_BASE = getFrontendUrl();
// not needed yet
// const KC_BASE = __ENV["KC_BASE"];

export const options = {
...getDefaultOptions(),
};

export default function main(users = getDefaultUserMix()) {
/**
* URL for final login, which we obtain from keycloak during oidc-login
*/
let loginUrl = "";

group("load spsh", () => {
const pageResponse = http.get(SPSH_BASE);
check(pageResponse, defaultHttpCheck);
loadLinkedResourcesAndCheck(pageResponse);
});

group("go to kc login and submit form", () => {
// load page
const loginPageResponse = http.get(
SPSH_BASE + "api/auth/login?redirectUrl=/",
);
check(loginPageResponse, defaultHttpCheck);
loadLinkedResourcesAndCheck(loginPageResponse);

// submit form
const doc = loginPageResponse.html();
const actionUrl = doc.find("#kc-form-login").attr("action");
if (!actionUrl) fail("action for #kc-form-login was not found");

const user = users.getLogin();
const loginData = {
...user,
credentialId: "",
};
const loginResponse = http.post(actionUrl, loginData, { redirects: 0 });
check(loginResponse, {
"submitting login form to kc succeeded": () =>
loginResponse.status === 302,
...defaultTimingCheck,
});

// retrieve the loginUrl from the response
// this includes state, session_state, iss and security code
loginUrl = loginResponse.headers["Location"];
if (!loginUrl) {
fail("did not find Location in kc response");
}
});

group("finish login", () => {
const response = http.get(loginUrl);
check(response, {
"login succeeded": () =>
response.status === 200 || response.status === 302,
...defaultTimingCheck,
});
loadLinkedResourcesAndCheck(response);
});

sleep(1);
}
23 changes: 23 additions & 0 deletions loadtest/usecases/1_show-start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { group, sleep } from "k6";
import {
getLoginInfo,
getServiceProviderLogos,
getServiceProviders,
} from "../util/api.ts";
import { goToStartPage } from "../util/page.ts";
import { getDefaultAdminMix } from "../util/users.ts";
import login from "./1_login.ts";

export default function main(users = getDefaultAdminMix()) {
login(users);

group("load start page", () => {
goToStartPage();

getLoginInfo();
const providers = getServiceProviders();
getServiceProviderLogos(providers);
});

sleep(1);
}
134 changes: 134 additions & 0 deletions loadtest/usecases/1_show-user-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { group, sleep } from "k6";
import {
getLoginInfo,
getOrganisationen,
getPersonen,
getPersonenUebersicht,
getRollen,
Paginated,
PersonDatensatz,
PersonenUebersicht,
} from "../util/api.ts";
import { getDefaultAdminMix } from "../util/users.ts";
import goToStart from "./1_show-start.ts";

export default function main(users = getDefaultAdminMix()) {
goToStart(users);

// these are used to test the filters
let orgId = "";
let rolleId = "";
let personenuebersicht: PersonenUebersicht | undefined = undefined;

group("load user page", () => {
getLoginInfo();
const organisationen = getOrganisationen([
"limit=25",
"systemrechte=PERSONEN_VERWALTEN",
"excludeTyp=KLASSE",
]);
orgId = pickRandomItem(organisationen).id;

// TODO: see if this behaviour should be emulated
for (let i = 0; i < 2; i++) {
const personIds = getPersonenIds();
const personenuebersichten = getPersonenUebersicht(personIds);
personenuebersicht = pickRandomItem(personenuebersichten);
}

const rollen = getRollen(["rolleName="]);
rolleId = pickRandomItem(rollen).id;
});

group("hit pages", () => {
for (let offset = 0; offset < 5; offset++) {
getPersonen([`offset=${offset}`, "limit=30", "suchFilter="]);
}
});

group("toggle filters", () => {
group("schule", () => {
getOrganisationen([
"limit=25",
"searchString=",
"typ=KLASSE",
`administriertVon=${orgId}`,
]);
const personen = getPersonen([
"offset=0",
"limit=30",
`organisationIDs=${orgId}`,
"suchFilter=",
]);
const personIds = getPersonenIds(personen);
getPersonenUebersicht(personIds);

emulateFilterReset();
});

group("rolle", () => {
getOrganisationen([
"limit=25",
"searchString=",
"typ=KLASSE",
`administriertVon=${orgId}`,
]);
const personen = getPersonen([
"offset=0",
"limit=30",
`rolleIDs=${rolleId}`,
"suchFilter=",
]);
const personIds = getPersonenIds(personen);
getPersonenUebersicht(personIds);

emulateFilterReset();
});

group("filter list", () => {
const filters = [
personenuebersicht?.benutzername,
personenuebersicht?.vorname,
personenuebersicht?.nachname,
];
for (const filter of filters) {
const personen = getPersonen([
"offset=0",
"limit=30",
`suchFilter=${filter}`,
]);
const personIds = getPersonenIds(personen);
getPersonenUebersicht(personIds);

emulateFilterReset();
}
});
});

sleep(1);
}

function getPersonenIds(personen?: Paginated<PersonDatensatz>): Set<string> {
if (!personen) personen = getPersonen();
return new Set(personen.items.map(({ person }) => person.id));
}

function emulateFilterReset() {
const personIds = getPersonenIds();
getPersonenUebersicht(personIds);
getOrganisationen([
"limit=25",
"systemrechte=PERSONEN_VERWALTEN",
"excludeTyp=KLASSE",
]);
getOrganisationen([
"limit=25",
"searchString=",
"systemrechte=PERSONEN_VERWALTEN",
"excludeTyp=KLASSE",
]);
}

function pickRandomItem<T>(array: Array<T>): T {
return array[Math.floor(Math.random() * array.length)];
}
Loading

0 comments on commit d87e76a

Please sign in to comment.