diff --git a/tests/k6/README.md b/tests/k6/README.md index d454736982..60635ae7d8 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 d2ff6c66a0..fd7836fa22 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 3f78f0a1dc..bc87e10234 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 61dfede2eb..0000000000 --- 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 0000000000..8b9f3e3449 --- /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 0000000000..77d03e8c98 --- /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 0000000000..52cba2cff5 --- /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 0000000000..c15e36a51c --- /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 0000000000..73e784bf79 --- /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 1fac440ef0..04746a834a 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 7a9f896779..fe9d944220 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 0000000000..0897108475 --- /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 0000000000..3d7e48162a --- /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 0a77838241..76567b2909 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 00dc81e949..54f9dcb354 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 c09f1c1ceb..0000000000 --- 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 0000000000..1506516dd6 --- /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 0000000000..23fa5f9d3e --- /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 0000000000..ec574f9473 --- /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 a58223d9f4..1197148b13 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"