Skip to content

Commit

Permalink
Merge branch 'main' into remove-unused-files
Browse files Browse the repository at this point in the history
  • Loading branch information
arlowatts committed Aug 24, 2024
2 parents 8657e22 + b19aba3 commit 1be1f4a
Show file tree
Hide file tree
Showing 85 changed files with 521 additions and 225 deletions.
129 changes: 29 additions & 100 deletions test/k6/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,127 +14,56 @@
// limitations under the License.
//-------------------------------------------------------------------------

import { check, fail, sleep } from "k6";
import { post } from "k6/http";
import { Rate } from "k6/metrics";

// Rates provide additional statistics in the end-of-test summary
const authSuccessRate = new Rate("authentication_successful");
const refreshTokenSuccessRate = new Rate("auth_refresh_successful");
// rates provide additional statistics in the end-of-test summary
const authenticationSuccessful = new Rate("_authentication_successful");

// tries to ensure that the client is authorized
// requests a new JWT or refreshes the current token if necessary
export function authorizeClient(client, tokenUrl) {
if ((__ITER == 0) && (client.token == null)) {
let responseCode = authenticateClient(client, tokenUrl);

if (!check(responseCode, {"Authentication successful": responseCode === 200})) {
fail("Authentication failed with response code " + responseCode);
}
// tries to ensure that the client is authenticated by requesting a new token if
// the current token is expired or expires within 45 seconds
export function authenticateClient(client, tokenUrl) {
// there is no need to refresh the token if there are at least 45 seconds
// until it expires
if (client.token !== null && client.expires >= Date.now() + 45000) {
return 200;
}

refreshClientToken(client, tokenUrl);
}

// helper function to provide the client with a token
function authenticateClient(client, tokenUrl) {
// assemble the data to be passed to the keycloak endpoint
let authFormData = {
// assemble the form data for the authentication request
let formData = {
"grant_type": "client_credentials",
"client_id": client.clientId,
"client_id": client.id,
"audience": "pharmanet",
"scope": client.scopes,
"client_secret": client.clientSecret
"client_secret": client.secret
};

// submit the request and receive the response
let response = post(tokenUrl, authFormData);
let jsonResponse = JSON.parse(response.body);
let response = post(tokenUrl, formData);
let body = JSON.parse(response.body);

if (response.status == 200) {
// load the data into the client object
client.token = jsonResponse["access_token"];
client.refresh = jsonResponse["refresh_token"];
client.expires = getAbsoluteTime(jsonResponse["expires_in"]);
if (response.status === 200) {
// update the rate
authenticationSuccessful.add(1);
console.log("Client authentication successful");

// log and record the success
authSuccessRate.add(1);
console.log("Authenticated client: " + client.clientId);
console.log("Token: " + client.token);
// update the client object
client.token = body.access_token;
client.expires = Date.now() + 1000 * body.expires_in;
}
else {
console.log("Authentication failed with response code " + response.status);
console.log(
"clientId=\"" + client.clientId + "\", " +
"clientSecret=\"" + client.clientSecret + "\", " +
"scopes=\"" + client.scopes + "\", " +
"error=\"" + jsonResponse["error"] + ": " + jsonResponse["error_description"] + "\""
);

authSuccessRate.add(0);
client.token = null;
}

return response.status;
}

// check if the token expires soon and refresh it if necessary
// refresh 45 seconds before expiry
// refresh the token by requesting a new JWT from keycloak
function refreshClientToken(client, tokenUrl) {
if ((client.refresh == null) || (client.expires >= (Date.now() + 45000))) {
// don't need to refresh
return;
}
// update the rate
authenticationSuccessful.add(0);
console.log("Client authentication not successful: " + response.status);

if (client.token == null) {
// previous refresh failed
return authenticateClient(client, tokenUrl);
}

let refreshFormData = {
"grant_type": "refresh_token",
"client_id": client.clientId,
"refresh_token": client.refresh,
};
console.log("error='" + body.error + ": " + body.error_description + "'");

let response = post(tokenUrl, refreshFormData);
let jsonResponse = JSON.parse(response.body);

if (response.status == 200) {
// load the data into the client object
client.token = jsonResponse["access_token"];
client.refresh = jsonResponse["refresh_token"];
client.expires = getAbsoluteTime(jsonResponse["expires_in"]);

// log and record the success
refreshTokenSuccessRate.add(1);
console.log("Re-authenticated client: " + client.clientId);
console.log("Token: " + client.token);
}
else {
console.log("Re-authentication failed with response code " + response.status + ". ");
console.log(
"clientId=\"" + client.clientId + "\", " +
"clientSecret=\"" + client.clientSecret + "\", " +
"scopes=\"" + client.scopes + "\", " +
"error=\"" + jsonResponse["error"] + ": " + jsonResponse["error_description"] + "\""
"client.id=" + client.id + ", " +
"client.secret=" + client.secret + ", " +
"client.scopes='" + client.scopes + "'"
);

refreshTokenSuccessRate.add(0);
client.token = null;

console.log("Attempting authentication with new token.")

// pause before trying to authenticate the client with a new token
sleep(1);
return authenticateClient(client, tokenUrl);
}

return response.status;
}

// return an absolute time given the number of seconds until then
function getAbsoluteTime(seconds) {
return Date.now() + seconds * 1000;
}
123 changes: 69 additions & 54 deletions test/k6/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,88 +16,103 @@

import { sleep } from "k6";

import { authorizeClient } from "./auth.js";
import { authenticateClient } from "./auth.js";
import { submitMessage } from "./transaction.js";
import { examples } from "./examples.js";
import { meanDelaySeconds } from "./options.js";

// these are the options that are read by the k6 test framework
// here you can define the vus, iterations, duration, or stages
// see https://k6.io/docs/using-k6/k6-options/ for more information
export const options = {
vus: __ENV.ERX_VUS ? __ENV.ERX_VUS : 1,
iterations: __ENV.ERX_ITERATIONS ? __ENV.ERX_ITERATIONS : 1
export { options } from "./options.js";

// dictionary to select the correct base api url by environment
const environmentBaseApiUrls = {
"dev": "https://pnet-dev.api.gov.bc.ca/api/v1/",
"vs1": "https://pnet-vs1.api.gov.bc.ca/api/v1/",
"tr1": "https://pnet-tr1.api.gov.bc.ca/api/v1/",
"vc2": "https://pnet-vc2.api.gov.bc.ca/api/v1/",
"vc1": "https://pnet-vc1.api.gov.bc.ca/api/v1/",
"prd": "https://pnet.api.gov.bc.ca/api/v1/",
};

// dictionary to select the correct keycloak token url by environment
const environmentTokenUrls = {
"dev": "https://common-logon-dev.hlth.gov.bc.ca/auth/realms/v2_pos/protocol/openid-connect/token",
"vs1": "https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token",
"tr1": "https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token",
"vc2": "https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token",
"vc1": "https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token",
"prd": "https://common-logon.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token",
};

// dictionary to capitalize the service name by service
const serviceNames = {
"claim": "Claim",
"consent": "Consent",
"location": "Location",
"medication": "Medication",
"medicationdispense": "MedicationDispense",
"medicationrequest": "MedicationRequest",
"medicationstatement": "MedicationStatement",
"patient": "Patient",
"practitioner": "Practitioner",
};

// dictionary to select the keycloak scopes by service
const serviceScopes = {
"claim": "openid system/Claim.write system/Claim.read",
"consent": "openid system/Patient.read system/Consent.write system/Consent.read",
"location": "openid system/Location.read",
"medication": "openid system/Medication.read",
"medicationdispense": "openid system/MedicationDispense.write system/MedicationDispense.read",
"medicationrequest": "openid system/MedicationRequest.write system/MedicationRequest.read",
"medicationstatement": "openid system/MedicationStatement.read",
"patient": "openid system/Patient.read system/Patient.write",
"practitioner": "openid system/Practitioner.read",
};

// environment and service are defined by environment variables
// their values are set when the k6 command to execute the test is run
// environment and service are defined by environment variables set when the k6
// command to execute the test is run
const environment = __ENV.ERX_ENV;
const service = __ENV.ERX_SERVICE;
const iterationLength = __ENV.ERX_ITERATION_LENGTH ? __ENV.ERX_ITERATION_LENGTH : -1;

// the services follow a common naming scheme by environment
// they look like https://pnet-{env}.api.gov.bc.ca/api/v1/{service}
// except prod, which is https://pnet.api.gov.bc.ca/api/v1/{service}
const baseUrl = environment == "prd" ?
"https://pnet.api.gov.bc.ca/api/v1/" :
"https://pnet-" + environment + ".api.gov.bc.ca/api/v1/";

const serviceUrl = baseUrl + service;

// the keycloak url that will return the access token
// the url to use depends on the environment
// the dev environment uses the dev url, the prd environment uses the prod url,
// and the other environments all use the test url
const tokenUrl = environment == "dev" ?
"https://common-logon-dev.hlth.gov.bc.ca/auth/realms/v2_pos/protocol/openid-connect/token" : // dev
environment == "prd" ?
"https://common-logon.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token" : // prod
"https://common-logon-test.hlth.gov.bc.ca/auth/realms/moh_applications/protocol/openid-connect/token"; // test

// each service requires different scopes from its users
// the test framework uses these with its token requests to keycloak
const scopes = {
"Claim": "openid system/Claim.write system/Claim.read",
"Consent": "openid system/Patient.read system/Consent.write system/Consent.read",
"Location": "openid system/Location.read",
"Medication": "openid system/Medication.read",
"MedicationDispense": "openid system/MedicationDispense.write system/MedicationDispense.read",
"MedicationRequest": "openid system/MedicationRequest.write system/MedicationRequest.read",
"MedicationStatement": "openid system/MedicationStatement.read",
"Patient": "openid system/Patient.read system/Patient.write",
"Practitioner": "openid system/Practitioner.read"
};
const iterationLength = __ENV.ERX_ITERATION_LENGTH ? __ENV.ERX_ITERATION_LENGTH : 1;

const serviceUrl = environmentBaseApiUrls[environment] + serviceNames[service];
const tokenUrl = environmentTokenUrls[environment];

// the client object stores information about the simulated client
const client = {
clientId: __ENV.ERX_CLIENT,
clientSecret: __ENV.ERX_CLIENT_SECRET,
id: __ENV.ERX_CLIENT,
secret: __ENV.ERX_CLIENT_SECRET,
token: null,
refresh: null,
expires: null,
scopes: scopes[service]
scopes: serviceScopes[service],
};

// this is the function k6 runs for each vu
// one execution of this function is one iteration
// k6 runs this function once for each iteration
export default function() {
// choose a random test message from the list for each transaction
for (let i = 0; i < iterationLength; i++) {
authorizeClient(client, tokenUrl);
authenticateClient(client, tokenUrl);

let transaction = examples[Math.floor(Math.random() * examples.length)];

submitMessage(client, serviceUrl, transaction);

sleep(1);
sleep(randomExp(meanDelaySeconds));
}

// run all test cases only when the user sets iterationLength to -1
// when the user sets iterationLength to -1, run all available transactions
if (iterationLength == -1) {
examples.forEach(transaction => {
authorizeClient(client, tokenUrl);
authenticateClient(client, tokenUrl);

submitMessage(client, serviceUrl, transaction);

sleep(1);
sleep(randomExp(meanDelaySeconds));
});
}
}

// generate a random number, exponentially distributed with the given mean
function randomExp(mean) {
return Math.log(1 - Math.random()) * -mean;
}
2 changes: 1 addition & 1 deletion test/k6/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "3.4"
services:
k6:
image: grafana/k6:latest
Expand All @@ -9,6 +8,7 @@ services:
- "./transaction.js:/transaction.js"
- "./uuid.js:/uuid.js"
- "./examples/${ERX_ENV}/${ERX_SERVICE}.js:/examples.js"
- "./options/${ERX_SERVICE}.js:/options.js"
environment:
- "ERX_ENV=${ERX_ENV}"
- "ERX_CLIENT=${ERX_CLIENT}"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
26 changes: 26 additions & 0 deletions test/k6/options/claim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//-------------------------------------------------------------------------
// Copyright © 2021 Province of British Columbia
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//-------------------------------------------------------------------------

// these are the options that are read by the k6 test framework
// here you can define the vus, iterations, duration, or stages
// see https://k6.io/docs/using-k6/k6-options/ for more information
export const options = {
vus: __ENV.ERX_VUS ? __ENV.ERX_VUS : 1,
iterations: __ENV.ERX_ITERATIONS ? __ENV.ERX_ITERATIONS : 1
};

// the mean time delay in seconds between each transaction
export const meanDelaySeconds = 1;
26 changes: 26 additions & 0 deletions test/k6/options/consent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//-------------------------------------------------------------------------
// Copyright © 2021 Province of British Columbia
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//-------------------------------------------------------------------------

// these are the options that are read by the k6 test framework
// here you can define the vus, iterations, duration, or stages
// see https://k6.io/docs/using-k6/k6-options/ for more information
export const options = {
vus: __ENV.ERX_VUS ? __ENV.ERX_VUS : 1,
iterations: __ENV.ERX_ITERATIONS ? __ENV.ERX_ITERATIONS : 1
};

// the mean time delay in seconds between each transaction
export const meanDelaySeconds = 1;
Loading

0 comments on commit 1be1f4a

Please sign in to comment.