add benchmark tests

add more env variables
add oidc auth flow
add playbooks for better test reusability
add more api endpoints
update rollup config for test file transpilation
update test folder structure
split api package into smaller chunks
This commit is contained in:
Florian Schade
2020-11-27 23:10:23 +01:00
parent e39cd16617
commit 5ee7bad25c
20 changed files with 411 additions and 67 deletions

View File

@@ -11,5 +11,13 @@ $ yarn build
## How to run
```console
k6 run ./dist/TESTNAME.js
k6 run ./dist/test/NAME_OF_TEST.js
```
## Environment variables
```console
$ OC_OCIS_HOST=URL k6 run ...
$ OC_OIDC_HOST=URL k6 run ...
$ OC_OIDC=BOOL k6 run ...
$ OC_TEST_FILE=STRING k6 run ...
```

View File

@@ -44,6 +44,7 @@
"typescript": "^3.8.3"
},
"dependencies": {
"lodash": "^4.17.20"
"lodash": "^4.17.20",
"query-string": "^6.13.7"
}
}

View File

@@ -4,7 +4,6 @@ import resolve from '@rollup/plugin-node-resolve'
import babel from 'rollup-plugin-babel'
import { terser } from 'rollup-plugin-terser'
import multiInput from 'rollup-plugin-multi-input';
import path from 'path';
import utils from '@rollup/pluginutils';
import pkg from './package.json';
@@ -12,7 +11,7 @@ const extensions = ['.js', '.ts'];
export default [
{
input: ['src/test-*.ts'],
input: ['src/test/**/*.ts'],
external: utils.createFilter([
'k6/**',
...Object.keys(pkg.devDependencies),
@@ -27,7 +26,7 @@ export default [
],
plugins: [
multiInput({
transformOutputPath: (output, input) => path.basename(output),
transformOutputPath: (output, input) => `tests/${output.split('/').join('-')}`,
}),
json(),
resolve(

View File

@@ -1,28 +0,0 @@
import encoding from 'k6/encoding';
import {bytes} from "k6";
import http, {RefinedResponse, ResponseType} from "k6/http";
import * as defaults from "./defaults";
import * as types from "./types";
export const uploadFile = <RT extends ResponseType | undefined>(account: types.Account, data: bytes, name: string): RefinedResponse<RT> => {
return http.put(
`https://${defaults.host.name}/remote.php/dav/files/${account.login}/${name}`,
data as any,
{
headers: {
Authorization: `Basic ${encoding.b64encode(`${account.login}:${account.password}`)}`,
}
}
);
}
export const userInfo = <RT extends ResponseType | undefined>(account: any): RefinedResponse<RT> => {
return http.get(
`https://${defaults.host.name}/ocs/v1.php/cloud/users/${account.login}`,
{
headers: {
Authorization: `Basic ${encoding.b64encode(`${account.login}:${account.password}`)}`,
},
}
);
}

View File

@@ -0,0 +1,12 @@
import encoding from 'k6/encoding';
import * as types from "../types";
export const headersDefault = ({credential}: { credential: types.Account | types.Token }): { [key: string]: string } => {
const isOIDCGuard = (credential as types.Token).tokenType !== undefined;
const authOIDC = credential as types.Token;
const authBasic = credential as types.Account;
return {
Authorization: isOIDCGuard ? `${authOIDC.tokenType} ${authOIDC.accessToken}` : `Basic ${encoding.b64encode(`${authBasic.login}:${authBasic.password}`)}`,
}
}

View File

@@ -0,0 +1,45 @@
import http, {RefinedResponse, ResponseType} from "k6/http";
import * as api from './api'
import * as defaults from "../defaults";
import * as types from "../types";
export const fileUpload = <RT extends ResponseType | undefined>(
{credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset }
): RefinedResponse<RT> => {
return http.put(
`${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${asset.fileName}`,
asset.bytes as any,
{
headers: {
...api.headersDefault({credential})
}
}
);
}
export const fileDownload = <RT extends ResponseType | undefined>(
{credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string }
): RefinedResponse<RT> => {
return http.get(
`${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${fileName}`,
{
headers: {
...api.headersDefault({credential})
}
}
);
}
export const fileDelete = <RT extends ResponseType | undefined>(
{credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string }
): RefinedResponse<RT> => {
return http.del(
`${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${fileName}`,
{},
{
headers: {
...api.headersDefault({credential})
}
}
);
}

View File

@@ -0,0 +1,3 @@
export * as api from './api'
export * as dav from './dav'
export * as users from './users'

View File

@@ -0,0 +1,17 @@
import http, {RefinedResponse, ResponseType} from "k6/http";
import * as api from './api'
import * as defaults from "../defaults";
import * as types from "../types";
export const userInfo = <RT extends ResponseType | undefined>(
{credential, userName}: { credential: types.Account | types.Token; userName: string; }
): RefinedResponse<RT> => {
return http.get(
`${defaults.OC_OCIS_HOST}/ocs/v1.php/cloud/users/${userName}`,
{
headers: {
...api.headersDefault({credential})
},
}
);
}

87
tests/k6/src/lib/auth.ts Normal file
View File

@@ -0,0 +1,87 @@
import * as defaults from "./defaults";
import http from "k6/http";
import queryString from "query-string";
import * as types from "./types";
import {fail} from 'k6';
import {get} from 'lodash'
export const oidc = (account: types.Account): types.Token => {
const redirectUri = `${defaults.OC_OIDC_HOST}/oidc-callback.html`;
const logonUri = `${defaults.OC_OIDC_HOST}/signin/v1/identifier/_/logon`;
const logonResponse = http.post(
logonUri,
JSON.stringify(
{
params: [account.login, account.password, '1'],
hello: {
scope: 'openid profile email',
client_id: 'phoenix',
redirect_uri: redirectUri,
flow: 'oidc'
},
'state': 'vp42cf'
},
),
{
headers: {
'Kopano-Konnect-XSRF': '1',
Referer: defaults.OC_OIDC_HOST,
'Content-Type': 'application/json',
},
},
);
const authorizeURI = get(logonResponse.json(), 'hello.continue_uri');
if (logonResponse.status != 200 || !authorizeURI) {
fail(logonUri);
}
const authorizeUri = `${authorizeURI}?${
queryString.stringify(
{
client_id: 'phoenix',
prompt: 'none',
redirect_uri: redirectUri,
response_mode: 'query',
response_type: 'code',
scope: 'openid profile email',
},
)
}`;
const authorizeResponse = http.get(
authorizeUri,
{
redirects: 0,
},
)
const authCode = get(queryString.parseUrl(authorizeResponse.headers.Location), 'query.code')
if (authorizeResponse.status != 302 || !authCode) {
fail(authorizeURI);
}
const tokenUrl = `${defaults.OC_OIDC_HOST}/konnect/v1/token`;
const tokenResponse = http.post(
tokenUrl,
{
client_id: 'phoenix',
code: authCode,
redirect_uri: redirectUri,
grant_type: 'authorization_code'
}
)
const token = {
accessToken: get(tokenResponse.json(), 'access_token'),
tokenType: get(tokenResponse.json(), 'token_type'),
idToken: get(tokenResponse.json(), 'id_token'),
expiresIn: get(tokenResponse.json(), 'expires_in'),
}
if (tokenResponse.status != 200 || !token.accessToken || !token.tokenType || !token.idToken || !token.expiresIn) {
fail(authorizeURI);
}
return token
}

View File

@@ -1,10 +1,18 @@
import * as types from './types';
import {Options} from "k6/options";
export const host = {
name: __ENV.OC_HOST_NAME || 'localhost:9200'
const ocTestFile = '../_files/' + (__ENV.OC_TEST_FILE || 'kb_50.jpg').split('/').pop()
export const OC_OCIS_HOST = __ENV.OC_OCIS_HOST || 'https://localhost:9200'
export const OC_OIDC_HOST = __ENV.OC_OIDC_HOST || OC_OCIS_HOST
export const OC_OIDC = __ENV.OC_OIDC === 'true' || false
export const OC_TEST_FILE = {
fileName: ocTestFile,
bytes: open(ocTestFile, 'b'),
}
export const accounts: { [key: string]: types.Account; } = {
export const k6OptionsDefault: Options = {
insecureSkipTLSVerify: true,
};
export const knownAccounts: { [key: string]: types.Account; } = {
einstein: {
login: 'einstein',
password: 'relativity',

View File

@@ -1,3 +1,6 @@
export * as defaults from './defaults'
export * as api from './api'
export * as utils from './utils'
export * as playbook from './playbook'
export * as auth from './auth'
export * as defaults from './defaults'
export * as types from './types'
export * as utils from './utils'

View File

@@ -0,0 +1,70 @@
import {Gauge, Trend} from "k6/metrics";
import * as api from "../api";
import * as utils from "../utils";
import {bytes, check} from "k6";
import * as types from "../types";
export const fileUpload = () => {
const fileUploadTrend = new Trend('occ_file_upload_trend', true);
const fileUploadErrorRate = new Gauge('occ_file_upload_error_rate');
return ({credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset }): string => {
const fileName = `upload-${userName}-${__VU}-${__ITER}.${utils.extension(asset.fileName)}`;
const uploadResponse = api.dav.fileUpload({
credential: credential as any,
asset: {
fileName,
bytes: asset.bytes,
},
userName,
});
check(uploadResponse, {
'file upload status is 201': () => uploadResponse.status === 201,
}) || fileUploadErrorRate.add(1);
fileUploadTrend.add(uploadResponse.timings.duration)
return fileName
}
};
export const fileDelete = () => {
const fileDeleteTrend = new Trend('occ_file_delete_trend', true);
const fileDeleteErrorRate = new Gauge('occ_file_delete_error_rate');
return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }) => {
const deleteResponse = api.dav.fileDelete({
credential: credential as any,
fileName,
userName,
});
check(deleteResponse, {
'file delete status is 204': () => deleteResponse.status === 204,
}) || fileDeleteErrorRate.add(1);
fileDeleteTrend.add(deleteResponse.timings.duration)
}
};
export const fileDownload = () => {
const fileDownloadTrend = new Trend('occ_file_download_trend', true);
const fileDownloadErrorRate = new Gauge('occ_file_download_error_rate');
return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }): bytes => {
const downloadResponse = api.dav.fileDownload({
credential: credential as any,
fileName,
userName,
});
check(downloadResponse, {
'file download status is 200': () => downloadResponse.status === 200,
}) || fileDownloadErrorRate.add(1);
fileDownloadTrend.add(downloadResponse.timings.duration)
return downloadResponse.body as bytes
}
};

View File

@@ -0,0 +1 @@
export * as dav from './dav'

View File

@@ -1,3 +1,17 @@
import {bytes} from "k6";
export interface Asset {
bytes: bytes;
fileName: string;
}
export interface Token {
accessToken: string;
tokenType: string;
idToken: string;
expiresIn: string;
}
export interface Account {
login: string
password: string

View File

@@ -1,3 +1,8 @@
export const randomString = (): string => {
return Math.random().toString(36).slice(2)
}
}
export const extension = (p: string): string | undefined => {
return (p.split('/').pop())!.split('.').pop()
}

View File

@@ -1,27 +0,0 @@
import {sleep, check} from 'k6';
import {Options} from "k6/options";
import {defaults, api} from "./lib";
const files = {
'kb_50.jpg': open('./_files/kb_50.jpg', 'b'),
}
export let options: Options = {
insecureSkipTLSVerify: true,
iterations: 100,
vus: 100,
};
export default () => {
const res = api.uploadFile(
defaults.accounts.einstein,
files['kb_50.jpg'],
`kb_50-${__VU}-${__ITER}.jpg`,
);
check(res, {
'status is 204': () => res.status === 204,
});
sleep(1);
};

View File

@@ -0,0 +1,53 @@
import {defaults, playbook} from '../../lib'
import {Options} from 'k6/options';
import {sleep} from "k6";
import * as auth from "../../lib/auth";
import * as types from "../../lib/types";
interface dataI {
credential: types.Account | types.Token;
}
export const options: Options = {
...defaults.k6OptionsDefault,
iterations: 1,
vus: 1,
};
const account = defaults.knownAccounts.einstein;
const playbooks = {
fileUpload: playbook.dav.fileUpload(),
fileDownload: playbook.dav.fileDownload(),
fileDelete: playbook.dav.fileDelete(),
}
export const setup = (): dataI => {
return {
credential: defaults.OC_OIDC ? auth.oidc(account) : account,
}
}
export default (data: dataI) => {
const credential = data.credential;
const userName = account.login;
const fileName = playbooks.fileUpload({
credential,
userName,
asset: defaults.OC_TEST_FILE
});
sleep(1)
playbooks.fileDownload({
credential,
userName,
fileName,
});
sleep(1)
playbooks.fileDelete({
credential,
userName,
fileName,
});
sleep(1)
};

View File

@@ -0,0 +1,44 @@
import {defaults, playbook} from '../../lib'
import {Options} from 'k6/options';
import {sleep} from "k6";
import * as auth from "../../lib/auth";
import * as types from "../../lib/types";
interface dataI {
credential: types.Account | types.Token;
}
export const options: Options = {
...defaults.k6OptionsDefault,
iterations: 1,
vus: 1,
};
const account = defaults.knownAccounts.einstein;
const playbooks = {
fileUpload: playbook.dav.fileUpload(),
fileDelete: playbook.dav.fileDelete(),
}
export const setup = (): dataI => {
return {
credential: defaults.OC_OIDC ? auth.oidc(account) : account,
}
}
export default (data: dataI) => {
const credential = data.credential;
const userName = account.login;
const fileName = playbooks.fileUpload({
credential,
userName,
asset: defaults.OC_TEST_FILE
});
sleep(1)
playbooks.fileDelete({
credential,
userName,
fileName,
});
sleep(1)
};

View File

@@ -0,0 +1,10 @@
import {Options} from 'k6/options';
import * as uploadFilesBenchmark from '../../../benchmark/file-upload'
export const options: Options = {
...uploadFilesBenchmark.options,
iterations: 200,
vus: 50,
};
export const {setup} = uploadFilesBenchmark;
export default uploadFilesBenchmark.default;

View File

@@ -4850,6 +4850,15 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
query-string@^6.13.7:
version "6.13.7"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.7.tgz#af53802ff6ed56f3345f92d40a056f93681026ee"
integrity sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"
quick-lru@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
@@ -5528,6 +5537,11 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
split-on-first@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -5582,6 +5596,11 @@ stealthy-require@^1.1.1:
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-argv@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"