Skip to content

Commit

Permalink
test(load-test): Performance tests for create dialog and search (#1331)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the Title above -->

## Description

See commits for details

## Related Issue(s)

- #1326 

## Verification

- [ ] **Your** code builds clean without any errors or warnings
- [x] Manual testing done (required)
- [ ] Relevant automated test added (if you find this hard, leave it and
we'll help out)

## Documentation

- [ ] Documentation is updated (either in `docs`-directory, Altinnpedia
or a separate linked PR in
[altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if
applicable)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced new GitHub Actions workflows for K6 performance testing,
allowing manual execution with customizable parameters.
- Added performance testing scripts for end-user interactions and dialog
creation using the K6 framework.

- **Bug Fixes**
- Improved handling of authorization headers in request parameter
functions.

- **Chores**
- Updated `.gitignore` to exclude sensitive files and generated token
files.
- Added a script for generating tokens for service owners and end users.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ole Jørgen Skogstad <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 31, 2024
1 parent 92967b6 commit b3a01e8
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 5 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/dispatch-k6-performance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Run K6 performance test

on:
workflow_dispatch:
inputs:
apiVersion:
description: 'API Version'
required: true
default: 'v1'
environment:
description: 'Environment'
required: true
default: 'staging'
type: choice
options:
- test
- staging
- performance
tokens:
description: 'Tokens to generate; for create dialog, search, none, or both'
required: true
default: 'both'
type: choice
options:
- both
- enterprise
- personal
- none
vus:
description: 'Number of VUS'
required: true
type: number
default: 10
duration:
description: 'Duration of test, ie 30s, 1m, 10m'
required: true
default: 1m
type: string
testSuitePath:
description: 'Path to test suite to run'
required: true
default: 'tests/k6/tests/serviceowner/performance/create-dialog.js'

jobs:
k6-performance:
name: "Run K6 performance test"
uses: ./.github/workflows/performance-workflows/workflow-run-k6-performance.yml
secrets:
TOKEN_GENERATOR_USERNAME: ${{ secrets.TOKEN_GENERATOR_USERNAME }}
TOKEN_GENERATOR_PASSWORD: ${{ secrets.TOKEN_GENERATOR_PASSWORD }}
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
with:
environment: ${{ inputs.environment }}
apiVersion: ${{ inputs.apiVersion }}
testSuitePath: ${{ inputs.testSuitePath }}
vus: ${{ inputs.vus }}
duration: ${{ inputs.duration }}
tokens: ${{ inputs.tokens }}

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Run K6 performance tests

on:
workflow_call:
inputs:
apiVersion:
required: true
type: string
environment:
required: true
type: string
testSuitePath:
required: true
type: string
vus:
required: true
type: number
duration:
required: true
type: string
tokens:
required: true
type: string
secrets:
TOKEN_GENERATOR_USERNAME:
required: true
TOKEN_GENERATOR_PASSWORD:
required: true
K6_CLOUD_TOKEN:
required: true
K6_CLOUD_PROJECT_ID:
required: true

jobs:
k6-test:
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup k6
uses: grafana/setup-k6-action@v1
- name: Run K6 tests (${{ inputs.testSuitePath }})
run: |
./tests/k6/tests/scripts/generate_tokens.sh ./tests/k6/tests/performancetest_data ${{ inputs.tokens }}
k6 run ${{ inputs.testSuitePath }} --quiet --log-output=stdout --include-system-env-vars --vus=${{ inputs.vus }} --duration=${{ inputs.duration }}
env:
API_ENVIRONMENT: ${{ inputs.environment }}
API_VERSION: ${{ inputs.apiVersion }}
TOKEN_GENERATOR_USERNAME: ${{ secrets.TOKEN_GENERATOR_USERNAME }}
TOKEN_GENERATOR_PASSWORD: ${{ secrets.TOKEN_GENERATOR_PASSWORD }}
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,10 @@ FodyWeavers.xsd

# MacOS
.DS_Store

# Secrets file used by act
.secrets

# Generated files with tokens
**/.endusers-with-tokens.csv
**/.serviceowners-with-tokens.csv
7 changes: 5 additions & 2 deletions tests/k6/common/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function resolveParams(defaultParams, params) {
function getServiceOwnerRequestParams(params = null, tokenOptions = null) {

params = params || {};
const headers = params.Headers || {};
const headers = params.headers || {};
const hasOverridenAuthorizationHeader = headers.Authorization !== undefined;

const defaultParams = {
Expand All @@ -33,11 +33,14 @@ function getServiceOwnerRequestParams(params = null, tokenOptions = null) {
}

function getEnduserRequestParams(params = null, tokenOptions = null) {
params = params || {};
const headers = params.headers || {};
const hasOverridenAuthorizationHeader = headers.Authorization !== undefined;
let defaultParams = {
headers: {
'Accept': 'application/json',
'User-Agent': 'dialogporten-k6',
'Authorization': 'Bearer ' + getEnduserTokenFromGenerator(tokenOptions)
'Authorization': hasOverridenAuthorizationHeader ? headers.Authorization : 'Bearer ' + getEnduserTokenFromGenerator(tokenOptions)
}
}

Expand Down
57 changes: 57 additions & 0 deletions tests/k6/tests/enduser/performance/simple-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getEU, expect, expectStatusFor, describe } from "../../../common/testimports.js";
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { randomItem } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

const filenameEndusers = '../../performancetest_data/.endusers-with-tokens.csv';

const endUsers = new SharedArray('endUsers', function () {
try {
const csvData = papaparse.parse(open(filenameEndusers), { header: true, skipEmptyLines: true }).data;
if (!csvData.length) {
throw new Error('No data found in CSV file');
}
csvData.forEach((user, index) => {
if (!user.token || !user.ssn) {
throw new Error(`Missing required fields at row ${index + 1}`);
}
});
return csvData;
} catch (error) {
throw new Error(`Failed to load end users: ${error.message}`);
}
});

export let options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)', 'p(99.5)', 'p(99.9)', 'count'],
thresholds: {
'http_req_duration{name:simple search}': [],
'http_reqs{name:simple search}': [],
},
};

export default function() {
if ((options.vus === undefined || options.vus === 1) && (options.iterations === undefined || options.iterations === 1)) {
simpleSearch(endUsers[0]);
}
else {
simpleSearch(randomItem(endUsers));
}
}

export function simpleSearch(enduser) {
let paramsWithToken = {
headers: {
Authorization: "Bearer " + enduser.token
},
tags: { name: 'simple search' }
}
let defaultParty = "urn:altinn:person:identifier-no:" + enduser.ssn;
let defaultFilter = "?Party=" + defaultParty;
describe('Perform simple dialog list', () => {
let r = getEU('dialogs' + defaultFilter, paramsWithToken);
expectStatusFor(r).to.equal(200);
expect(r, 'response').to.have.validJsonBody();
});
}

2 changes: 2 additions & 0 deletions tests/k6/tests/performancetest_data/endusers-staging.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ssn,resource,scopes
08895699684,super-simple-service,digdir:dialogporten
2 changes: 2 additions & 0 deletions tests/k6/tests/performancetest_data/serviceowners-staging.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org,orgno,scopes,resource
digdir,991825827,digdir:dialogporten.serviceprovider,super-simple-service
94 changes: 94 additions & 0 deletions tests/k6/tests/scripts/generate_tokens.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/bash

# Check if required environment variables are set
if [ -z "$TOKEN_GENERATOR_USERNAME" ] || [ -z "$TOKEN_GENERATOR_PASSWORD" ] || [ -z "$API_ENVIRONMENT" ]; then
echo "Error: TOKEN_GENERATOR_USERNAME, TOKEN_GENERATOR_PASSWORD, and API_ENVIRONMENT must be set"
exit 1
fi

# Function to display usage information
usage() {
echo "Usage: $0 <testdatafilepath> <tokens>"
echo " <testdatafilepath>: Path to the test data files"
echo " <tokens>: Type of tokens to generate (both, enterprise, or personal)"
exit 1
}

# Validate arguments
if [ $# -ne 2 ]; then
usage
fi

tokengenuser=${TOKEN_GENERATOR_USERNAME}
tokengenpasswd=${TOKEN_GENERATOR_PASSWORD}

env=""
case $API_ENVIRONMENT in
"test")
env="at21" ;;
"staging")
env="tt02" ;;
"performance")
env="yt01" ;;
*)
echo "Error: Unknown api environment $API_ENVIRONMENT"
exit 1 ;;
esac

testdatafilepath=$1
tokens=$2

# Validate tokens argument
if [[ ! "$tokens" =~ ^(both|enterprise|personal)$ ]]; then
echo "Error: Invalid token type. Must be 'both', 'enterprise', or 'personal'."
usage
fi

serviceowner_datafile="$testdatafilepath/serviceowners-$API_ENVIRONMENT.csv"
serviceowner_tokenfile="$testdatafilepath/.serviceowners-with-tokens.csv"
enduser_datafile="$testdatafilepath/endusers-$API_ENVIRONMENT.csv"
enduser_tokenfile="$testdatafilepath/.endusers-with-tokens.csv"

if [ "$tokens" = "both" ] || [ "$tokens" = "enterprise" ]; then
if [ ! -f "$serviceowner_datafile" ]; then
echo "Error: Input file not found: $serviceowner_datafile"
exit 1
fi
echo "org,orgno,scopes,resource,token" > $serviceowner_tokenfile
while IFS=, read -r org orgno scopes resource
do
url="https://altinn-testtools-token-generator.azurewebsites.net/api/GetEnterpriseToken?org=$org&env=$env&scopes=$scopes&orgno=$orgno&ttl=3600"
token=$(curl -s -f $url -u "$tokengenuser:$tokengenpasswd" )
if [ $? -ne 0 ]; then
echo "Error: Failed to generate enterprise token for: $env, $org, $orgno, $scopes "
continue
fi
echo "$org,$orgno,$scopes,$resource,$token" >> $serviceowner_tokenfile
status=$?
if [ $status -ne 0 ]; then
echo "Error: Failed to write enterprise token to file for: $env, $org, $orgno, $scopes"
fi
done < <(tail -n +2 $serviceowner_datafile)
fi

if [ "$tokens" = "both" ] || [ "$tokens" = "personal" ]; then
if [ ! -f "$enduser_datafile" ]; then
echo "Error: Input file not found: $enduser_datafile"
exit 1
fi
echo "ssn,resource,scopes,token" > $enduser_tokenfile
while IFS=, read -r ssn resource scopes
do
url="https://altinn-testtools-token-generator.azurewebsites.net/api/GetPersonalToken?env=$env&scopes=$scopes&pid=$ssn&ttl=3600"
token=$(curl -s -f $url -u "$tokengenuser:$tokengenpasswd" )
if [ $? -ne 0 ]; then
echo "Error: Failed to generate personal token for: $ssn, $scopes "
continue
fi
echo "$ssn,$resource,$scopes,$token" >> $enduser_tokenfile
status=$?
if [ $status -ne 0 ]; then
echo "Error: Failed to write personal token to file for: $ssn, $scopes"
fi
done < <(tail -n +2 $enduser_datafile)
fi
49 changes: 49 additions & 0 deletions tests/k6/tests/serviceowner/performance/create-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { postSO, expect, describe } from "../../../common/testimports.js";
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { default as dialogToInsert } from '../testdata/01-create-dialog.js';
import { randomItem } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

const filenameServiceowners = '../../performancetest_data/.serviceowners-with-tokens.csv';
const filenameEndusers = `../../performancetest_data/endusers-${__ENV.API_ENVIRONMENT}.csv`;

const serviceOwners = new SharedArray('serviceOwners', function () {
return papaparse.parse(open(filenameServiceowners), { header: true, skipEmptyLines: true }).data;
});

const endUsers = new SharedArray('endUsers', function () {
return papaparse.parse(open(filenameEndusers), { header: true, skipEmptyLines: true }).data;
});

export let options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)', 'p(99.5)', 'p(99.9)', 'count'],
thresholds: {
'http_req_duration{scenario:default}': [`max>=0`],
'http_req_duration{name:create dialog}': [],
'http_reqs{name:create dialog}': [],
},
};

export default function() {
if ((options.vus === undefined || options.vus === 1) && (options.iterations === undefined || options.iterations === 1)) {
createDialog(serviceOwners[0], endUsers[0]);
}
else {
createDialog(randomItem(serviceOwners), randomItem(endUsers));
}
}

export function createDialog(serviceOwner, endUser) {
var paramsWithToken = {
headers: {
Authorization: "Bearer " + serviceOwner.token
},
tags: { name: 'create dialog' }
}

describe('create dialog', () => {
let r = postSO('dialogs', dialogToInsert(endUser.ssn), paramsWithToken);
expect(r.status, 'response status').to.equal(201);
});

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { default as dialogToInsert } from '../testdata/01-create-dialog.js';
export function setup() {
// Get the token during setup stage so that it doesn't interfere with timings
return {
Headers: {
headers: {
Authorization: "Bearer " + getServiceOwnerTokenFromGenerator()
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/k6/tests/serviceowner/testdata/01-create-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { uuidv4 } from '../../../common/testimports.js'
import { getDefaultEnduserSsn } from "../../../common/token.js";
import { sentinelValue } from "../../../common/config.js";

export default function () {
export default function (endUser = getDefaultEnduserSsn()) {
return {
"serviceResource": "urn:altinn:resource:ttd-dialogporten-automated-tests", // urn starting with urn:altinn:resource:
"party": "urn:altinn:person:identifier-no:" + getDefaultEnduserSsn(), // or urn:altinn:organization:identifier-no:<9 digits>
"party": "urn:altinn:person:identifier-no:" + endUser, // or urn:altinn:organization:identifier-no:<9 digits>
"status": "new", // valid values: new, inprogress, waiting, signing, cancelled, completed
"extendedStatus": "urn:any/valid/uri",
"dueAt": "2033-11-25T06:37:54.2920190Z", // must be UTC
Expand Down

0 comments on commit b3a01e8

Please sign in to comment.