From 5ee7bad25cb8565183c17feff18e1b8b83abb6d4 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Fri, 27 Nov 2020 23:10:23 +0100 Subject: [PATCH] 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 --- tests/k6/README.md | 10 ++- tests/k6/package.json | 3 +- tests/k6/rollup.config.js | 5 +- tests/k6/src/lib/api.ts | 28 ------- tests/k6/src/lib/api/api.ts | 12 +++ tests/k6/src/lib/api/dav.ts | 45 ++++++++++ tests/k6/src/lib/api/index.ts | 3 + tests/k6/src/lib/api/users.ts | 17 ++++ tests/k6/src/lib/auth.ts | 87 ++++++++++++++++++++ tests/k6/src/lib/defaults.ts | 16 +++- tests/k6/src/lib/index.ts | 7 +- tests/k6/src/lib/playbook/dav.ts | 70 ++++++++++++++++ tests/k6/src/lib/playbook/index.ts | 1 + tests/k6/src/lib/types.ts | 14 ++++ tests/k6/src/lib/utils.ts | 7 +- tests/k6/src/test-issue-162.ts | 27 ------ tests/k6/src/test/benchmark/file-download.ts | 53 ++++++++++++ tests/k6/src/test/benchmark/file-upload.ts | 44 ++++++++++ tests/k6/src/test/issue/github/ocis/162.ts | 10 +++ tests/k6/yarn.lock | 19 +++++ 20 files changed, 411 insertions(+), 67 deletions(-) delete mode 100644 tests/k6/src/lib/api.ts create mode 100644 tests/k6/src/lib/api/api.ts create mode 100644 tests/k6/src/lib/api/dav.ts create mode 100644 tests/k6/src/lib/api/index.ts create mode 100644 tests/k6/src/lib/api/users.ts create mode 100644 tests/k6/src/lib/auth.ts create mode 100644 tests/k6/src/lib/playbook/dav.ts create mode 100644 tests/k6/src/lib/playbook/index.ts delete mode 100644 tests/k6/src/test-issue-162.ts create mode 100644 tests/k6/src/test/benchmark/file-download.ts create mode 100644 tests/k6/src/test/benchmark/file-upload.ts create mode 100644 tests/k6/src/test/issue/github/ocis/162.ts diff --git a/tests/k6/README.md b/tests/k6/README.md index d45473698..60635ae7d 100644 --- a/tests/k6/README.md +++ b/tests/k6/README.md @@ -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 ... ``` \ No newline at end of file diff --git a/tests/k6/package.json b/tests/k6/package.json index d2ff6c66a..fd7836fa2 100644 --- a/tests/k6/package.json +++ b/tests/k6/package.json @@ -44,6 +44,7 @@ "typescript": "^3.8.3" }, "dependencies": { - "lodash": "^4.17.20" + "lodash": "^4.17.20", + "query-string": "^6.13.7" } } diff --git a/tests/k6/rollup.config.js b/tests/k6/rollup.config.js index 3f78f0a1d..bc87e1023 100644 --- a/tests/k6/rollup.config.js +++ b/tests/k6/rollup.config.js @@ -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( diff --git a/tests/k6/src/lib/api.ts b/tests/k6/src/lib/api.ts deleted file mode 100644 index 61dfede2e..000000000 --- a/tests/k6/src/lib/api.ts +++ /dev/null @@ -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 = (account: types.Account, data: bytes, name: string): RefinedResponse => { - 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 = (account: any): RefinedResponse => { - return http.get( - `https://${defaults.host.name}/ocs/v1.php/cloud/users/${account.login}`, - { - headers: { - Authorization: `Basic ${encoding.b64encode(`${account.login}:${account.password}`)}`, - }, - } - ); -} \ No newline at end of file diff --git a/tests/k6/src/lib/api/api.ts b/tests/k6/src/lib/api/api.ts new file mode 100644 index 000000000..8b9f3e344 --- /dev/null +++ b/tests/k6/src/lib/api/api.ts @@ -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}`)}`, + } +} \ No newline at end of file diff --git a/tests/k6/src/lib/api/dav.ts b/tests/k6/src/lib/api/dav.ts new file mode 100644 index 000000000..77d03e8c9 --- /dev/null +++ b/tests/k6/src/lib/api/dav.ts @@ -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 = ( + {credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset } +): RefinedResponse => { + 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 = ( + {credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string } +): RefinedResponse => { + return http.get( + `${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${fileName}`, + { + headers: { + ...api.headersDefault({credential}) + } + } + ); +} + +export const fileDelete = ( + {credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string } +): RefinedResponse => { + return http.del( + `${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${fileName}`, + {}, + { + headers: { + ...api.headersDefault({credential}) + } + } + ); +} \ No newline at end of file diff --git a/tests/k6/src/lib/api/index.ts b/tests/k6/src/lib/api/index.ts new file mode 100644 index 000000000..52cba2cff --- /dev/null +++ b/tests/k6/src/lib/api/index.ts @@ -0,0 +1,3 @@ +export * as api from './api' +export * as dav from './dav' +export * as users from './users' \ No newline at end of file diff --git a/tests/k6/src/lib/api/users.ts b/tests/k6/src/lib/api/users.ts new file mode 100644 index 000000000..c15e36a51 --- /dev/null +++ b/tests/k6/src/lib/api/users.ts @@ -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 = ( + {credential, userName}: { credential: types.Account | types.Token; userName: string; } +): RefinedResponse => { + return http.get( + `${defaults.OC_OCIS_HOST}/ocs/v1.php/cloud/users/${userName}`, + { + headers: { + ...api.headersDefault({credential}) + }, + } + ); +} \ No newline at end of file diff --git a/tests/k6/src/lib/auth.ts b/tests/k6/src/lib/auth.ts new file mode 100644 index 000000000..73e784bf7 --- /dev/null +++ b/tests/k6/src/lib/auth.ts @@ -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 +} \ No newline at end of file diff --git a/tests/k6/src/lib/defaults.ts b/tests/k6/src/lib/defaults.ts index 1fac440ef..04746a834 100644 --- a/tests/k6/src/lib/defaults.ts +++ b/tests/k6/src/lib/defaults.ts @@ -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', diff --git a/tests/k6/src/lib/index.ts b/tests/k6/src/lib/index.ts index 7a9f89677..fe9d94422 100644 --- a/tests/k6/src/lib/index.ts +++ b/tests/k6/src/lib/index.ts @@ -1,3 +1,6 @@ -export * as defaults from './defaults' export * as api from './api' -export * as utils from './utils' \ No newline at end of file +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' diff --git a/tests/k6/src/lib/playbook/dav.ts b/tests/k6/src/lib/playbook/dav.ts new file mode 100644 index 000000000..089710847 --- /dev/null +++ b/tests/k6/src/lib/playbook/dav.ts @@ -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 + } +}; diff --git a/tests/k6/src/lib/playbook/index.ts b/tests/k6/src/lib/playbook/index.ts new file mode 100644 index 000000000..3d7e48162 --- /dev/null +++ b/tests/k6/src/lib/playbook/index.ts @@ -0,0 +1 @@ +export * as dav from './dav' \ No newline at end of file diff --git a/tests/k6/src/lib/types.ts b/tests/k6/src/lib/types.ts index 0a7783824..76567b290 100644 --- a/tests/k6/src/lib/types.ts +++ b/tests/k6/src/lib/types.ts @@ -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 diff --git a/tests/k6/src/lib/utils.ts b/tests/k6/src/lib/utils.ts index 00dc81e94..54f9dcb35 100644 --- a/tests/k6/src/lib/utils.ts +++ b/tests/k6/src/lib/utils.ts @@ -1,3 +1,8 @@ export const randomString = (): string => { return Math.random().toString(36).slice(2) -} \ No newline at end of file +} + +export const extension = (p: string): string | undefined => { + return (p.split('/').pop())!.split('.').pop() +} + diff --git a/tests/k6/src/test-issue-162.ts b/tests/k6/src/test-issue-162.ts deleted file mode 100644 index c09f1c1ce..000000000 --- a/tests/k6/src/test-issue-162.ts +++ /dev/null @@ -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); -}; \ No newline at end of file diff --git a/tests/k6/src/test/benchmark/file-download.ts b/tests/k6/src/test/benchmark/file-download.ts new file mode 100644 index 000000000..1506516dd --- /dev/null +++ b/tests/k6/src/test/benchmark/file-download.ts @@ -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) +}; \ No newline at end of file diff --git a/tests/k6/src/test/benchmark/file-upload.ts b/tests/k6/src/test/benchmark/file-upload.ts new file mode 100644 index 000000000..23fa5f9d3 --- /dev/null +++ b/tests/k6/src/test/benchmark/file-upload.ts @@ -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) +}; \ No newline at end of file diff --git a/tests/k6/src/test/issue/github/ocis/162.ts b/tests/k6/src/test/issue/github/ocis/162.ts new file mode 100644 index 000000000..ec574f947 --- /dev/null +++ b/tests/k6/src/test/issue/github/ocis/162.ts @@ -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; \ No newline at end of file diff --git a/tests/k6/yarn.lock b/tests/k6/yarn.lock index a58223d9f..1197148b1 100644 --- a/tests/k6/yarn.lock +++ b/tests/k6/yarn.lock @@ -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"