diff --git a/README.md b/README.md index 8b191c017..7d6e34175 100644 --- a/README.md +++ b/README.md @@ -112,14 +112,21 @@ irma server -vv --store-type redis --redis-addr "localhost:6379" --redis-allow-e ``` ## Performance tests -This project only includes performance tests for the `irma keyshare server`. These tests can be run using the [k6 load testing tool](https://k6.io/docs/) and need a running keyshare server instance to test against. Instructions on how to run a keyshare server locally can be found [above](#running). +This project only includes performance tests for the `irma server` and the `irma keyshare server`. These tests can be run using the [k6 load testing tool](https://k6.io/docs/) and need a running server instance to test against. -The performance tests can be started in the following way: +Instructions on how to run `irma server` locally with a Redis datastore can be found [here](#using-a-local-redis-datastore). Instructions on how to run a keyshare server locally can be found [here](#running). -``` -go install go.k6.io/k6@latest -k6 run ./testdata/performance/keyshare-server.js --env URL=http://localhost:8080 --env ISSUER_ID=test.test -``` +First, you need to install `k6`: + + go install go.k6.io/k6@latest + +The performance tests of the `irma server` can be started in the following way: + + k6 run ./testdata/performance/irma-server.js --env URL=http://localhost:8088 + +The performance tests of the keyshare server can be started in the following way: + + k6 run ./testdata/performance/keyshare-server.js --env URL=http://localhost:8080 --env ISSUER_ID=test.test By default, k6 runs a single test iteration using 1 virtual user. These defaults can be adjusted by specifying test stages using the [`-s` CLI parameter](https://k6.io/docs/using-k6/options/#stages). diff --git a/testdata/configurations/keyshareserver.yml b/testdata/configurations/keyshareserver.yml index 1d234de2c..ae4f9f669 100644 --- a/testdata/configurations/keyshareserver.yml +++ b/testdata/configurations/keyshareserver.yml @@ -1,6 +1,7 @@ verbose: 2 schemes_path: testdata/irma_configuration +schemes_assets_path: testdata/irma_configuration # To prevent assets from Docker image to be used schemes_update: 0 privkeys: testdata/privatekeys url: http://localhost:8080/ diff --git a/testdata/configurations/myirmaserver.yml b/testdata/configurations/myirmaserver.yml index 90ca1fade..f6d841494 100644 --- a/testdata/configurations/myirmaserver.yml +++ b/testdata/configurations/myirmaserver.yml @@ -1,6 +1,7 @@ verbose: 2 schemes_path: testdata/irma_configuration +schemes_assets_path: testdata/irma_configuration # To prevent assets from Docker image to be used schemes_update: 0 url: http://localhost:port/ port: 8081 diff --git a/testdata/performance/irma-server.js b/testdata/performance/irma-server.js new file mode 100644 index 000000000..8aa5f208f --- /dev/null +++ b/testdata/performance/irma-server.js @@ -0,0 +1,70 @@ +import { check, fail, sleep } from 'k6'; +import http from 'k6/http'; + +const url = __ENV.URL; + +export const options = { + minIterationDuration: '30s', + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms + }, +}; + +function checkResponse(response, expectedOutput = '') { + const checkOutput = check(response, { + 'verify response': (r) => r.error === '', + 'verify status code': (r) => r.status === 200, + 'verify body': (r) => r.body != null && r.body.includes(expectedOutput), + }); + if (!checkOutput) fail(`unexpected response: url ${response.request.url}, status ${response.status}, error "${response.error}", body "${response.body}"`); +} + +export default function () { + const newSessionResp = http.post(`${url}/session`, JSON.stringify({ + "@context": "https://irma.app/ld/request/disclosure/v2", + "disclose": [ + [ + ["irma-demo.sidn-pbdf.email.email"] + ] + ] + }), { + headers: { + 'Content-Type': 'application/json', + }, + }); + checkResponse(newSessionResp); + + const sessionPackage = newSessionResp.json(); + const sessionPtrUrl = sessionPackage.sessionPtr.u; + + for (let i = 0; i < 10; i++) { + let statusResp = http.get(`${sessionPtrUrl}/status`); + checkResponse(statusResp, 'INITIALIZED'); + sleep(1); + } + + const sessionResp = http.get(sessionPtrUrl, { + headers: { + 'Authorization': '12345', + 'X-IRMA-MinProtocolVersion': '2.8', + 'X-IRMA-MaxProtocolVersion': '2.8', + }, + }); + checkResponse(sessionResp, '"protocolVersion":"2.8"'); + + for (let i = 0; i < 20; i++) { + let statusResp = http.get(`${sessionPtrUrl}/status`); + checkResponse(statusResp, 'CONNECTED'); + sleep(1); + } + + const sessionDeletedResp = http.del(sessionPtrUrl); + checkResponse(sessionDeletedResp); + + let statusResp = http.get(`${sessionPtrUrl}/status`); + checkResponse(statusResp, 'CANCELLED'); + + let sessionResultResp = http.get(`${url}/session/${sessionPackage.token}/result`); + checkResponse(sessionResultResp, 'CANCELLED'); +} diff --git a/testdata/performance/keyshare-server.js b/testdata/performance/keyshare-server.js index 0599adcda..4bba16adf 100644 --- a/testdata/performance/keyshare-server.js +++ b/testdata/performance/keyshare-server.js @@ -1,4 +1,4 @@ -import { fail } from 'k6'; +import { check, fail } from 'k6'; import { instance, vu } from 'k6/execution'; import http from 'k6/http'; @@ -14,6 +14,15 @@ export const options = { }, }; +function checkResponse(response, expectedOutput = '') { + const checkOutput = check(response, { + 'verify response': (r) => r.error === '', + 'verify status code': (r) => r.status === 200, + 'verify body': (r) => r.body != null && r.body.includes(expectedOutput), + }); + if (!checkOutput) fail(`unexpected response: url ${response.request.url}, status ${response.status}, error "${response.error}", body "${response.body}"`); +} + export function setup() { if (!url || !issuerID) { fail('Must specify URL and ISSUER_ID options via environment variables'); @@ -27,12 +36,13 @@ export function setup() { const registerPayloadStr = JSON.stringify(registerPayload); // An IRMA account cannot be used in parallel, so every VU needs its own account. - const testAccounts = Array.from({length: instance.vusInitialized}, () => { + const testAccounts = Array.from({ length: instance.vusInitialized }, () => { const registerResp = http.post(`${url}/client/register`, registerPayloadStr, { headers: { 'Content-Type': 'application/json', }, }); + checkResponse(registerResp); const sessionResp = http.get(registerResp.json().u, { headers: { @@ -41,8 +51,9 @@ export function setup() { 'X-IRMA-MaxProtocolVersion': '2.8', }, }); + checkResponse(sessionResp); - http.del(registerResp.json().u); + checkResponse(http.del(registerResp.json().u)); return { id: Object.values(sessionResp.json().request.credentials[0].attributes)[0], @@ -59,6 +70,7 @@ export default function ({ testAccounts }) { const testAccount = testAccounts[vu.idInTest - 1]; const pinResp = http.post(`${url}/users/verify/pin`, JSON.stringify(testAccount)); + checkResponse(pinResp); const proveParams = { headers: { @@ -67,7 +79,7 @@ export default function ({ testAccounts }) { }, }; - http.post(`${url}/prove/getCommitments`, `["${issuerID}-0"]`, proveParams); + checkResponse(http.post(`${url}/prove/getCommitments`, `["${issuerID}-0"]`, proveParams)); - http.post(`${url}/prove/getResponse`, '"5adEmlEg9U2zjNlPxyPvRym2AzWkBo4kIZJ7ytNg0q0="', proveParams); + checkResponse(http.post(`${url}/prove/getResponse`, '"5adEmlEg9U2zjNlPxyPvRym2AzWkBo4kIZJ7ytNg0q0="', proveParams)); }