Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .github/workflows/cpi-integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: 'CPI-Integration Test'

on:
workflow_dispatch:
workflow_call:
schedule:
- cron: '0 6 * * 1-5'
push:
branches: [ 'mw/dpc-5238-integration-test' ]

permissions:
id-token: write
contents: read

concurrency:
group: integration-test-${{ inputs.env }}
cancel-in-progress: false

jobs:
cpi-integration-test:
defaults:
run:
working-directory: ./dpc-load-testing
name: CPI Integration Test
runs-on: codebuild-dpc-app-${{github.run_id}}-${{github.run_attempt}}
steps:
- uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
name: Slack Starting
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
# Sends to dpc-deploys
payload: |
channel: "CMC1E4AEQ"
attachments:
- color: warning
text: "Integration test for CPI Gateway has started"
footer: "<${{ github.server_url}}/${{ github.repository}}/actions/runs/${{ github.run_id }}|Integration test - Build ${{ github.run_id }}>"
mrkdown_in:
- footer
- name: "Checkout code"
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: AWS Credentials (non-prod)
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
with:
aws-region: ${{ vars.AWS_REGION }}
role-to-assume: arn:aws:iam::${{ secrets.NON_PROD_ACCOUNT_ID }}:role/delegatedadmin/developer/dpc-test-github-actions
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: '>=1.24.4'
- name: Build K6
run: |
# Install xk6
go install go.k6.io/xk6/cmd/xk6@v1.1.5

# Build the k6 binary
xk6 build
- name: Set env vars from AWS params
uses: cmsgov/cdap/actions/aws-params-env-action@main
env:
AWS_REGION: ${{ vars.AWS_REGION }}
with:
params: |
CPI_API_GW_TESTDATA=/dpc/test/web-portal/cpi_api_gw_testdata
CLIENT_ID=/dpc/test/web-portal/cpi_api_gw_client_id
CLIENT_SECRET=/dpc/test/web-portal/cpi_api_gw_client_secret
- name: Run CPI Gateway Integration Test
id: run-cpi-gateway-integration-test
env:
ENVIRONMENT: test
run: |
./k6 run ./integration_test/cpi_gateway_test.js
- uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
name: Slack Success
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
# Sends to dpc-deploys
payload: |
channel: "CMC1E4AEQ"
attachments:
- color: good
text: "Integration test for CPI Gateway has succeeded"
footer: "<${{ github.server_url}}/${{ github.repository}}/actions/runs/${{ github.run_id }}|Integration Test - Build ${{ github.run_id }}>"
mrkdown_in:
- footer
- uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
name: Slack failure
if: ${{ failure() }}
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
# Sends to dpc-deploys
payload: |
channel: "CMC1E4AEQ"
attachments:
- color: danger
text: "Integration test for CPI Gateway has has failed"
footer: "<${{ github.server_url}}/${{ github.repository}}/actions/runs/${{ github.run_id }}|integration Test - Build ${{ github.run_id }}>"
mrkdown_in:
- footer
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# ==============

SMOKE_THREADS ?= 10
SHELL := /bin/bash

venv: venv/bin/activate

Expand All @@ -18,6 +19,12 @@ smoke/local: start-dpc
@echo "Running Smoke Tests against Local env"
docker run --rm -v $(shell pwd)/dpc-load-testing:/src -e ENVIRONMENT=local -i grafana/k6:1.4.2 run /src/smoke-test.js


.PHONY: it/local
it/local:
. ./scripts/export_ssm_parameters.sh && \
docker run --rm -v $(shell pwd)/dpc-load-testing:/src -e ENVIRONMENT=local -e CLIENT_ID -e CLIENT_SECRET -e CPI_API_GW_TESTDATA -i grafana/k6:1.4.2 run /src/integration_test/cpi_gateway_test.js

# Build commands
#
# These commands build/compile our applications and docker images.
Expand Down
219 changes: 219 additions & 0 deletions dpc-load-testing/integration_test/cpi_gateway_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*global console*/
/* eslint no-console: "off" */

import { check } from 'k6';
import exec from 'k6/execution';
import { isEmptyObject, isObjectType, isDate, isArrayType } from '../utils/test-utils.js';
import { getToken } from '../oauth-client.js';
import http from 'k6/http';

var PROVIDERS_PATH = "api/1.0/ppr/providers";
var ORG_PROVIDER_PROFILE_PATH = "api/1.0/ppr/providers/profile";


//var PROFILE_PATH = "api/1.0/ppr/providers/profiles";

// No longer needed as we started to use SSM Parameter Store to aid local dev and testing.
//var testData = __ENV.ENVIRONMENT == 'local' ? JSON.parse(open('./cpi_test_data.secret')) : {}

export const options = {
thresholds: {
checks: ['rate===1'],
},
insecureSkipTLSVerify: true,
scenarios: {
smokeTests: {
executor: 'per-vu-iterations',
vus: 1,
iterations: 1,
exec: "runCPITests"

}
}
};

// This function aggregates the config data from different sources based upon
// the environment and presents unified source for subsequent tests.
function getConfig() {
return {
...JSON.parse(__ENV.CPI_API_GW_TESTDATA),
...{ clientId: __ENV.CLIENT_ID, clientSecret: __ENV.CLIENT_SECRET }
};
}

export function setup() {

var env = __ENV.ENVIRONMENT;
console.log(`Running CPI Gateway Integration Test with environment ${env}`);

var config = getConfig();
//console.log(`Merged Config for CPI Gateway Integration Test: ${JSON.stringify(config)}`);

if (isEmptyObject(config)) {
console.error("\nTest data not found in environment variable CPI_API_GW_TESTDATA");
exec.test.abort('failed to check for existing orgs');
}

var cpiConfig = config.meta.configuration['test'];

const oauthEndpointBaseUrl = cpiConfig["OAUTH_URL"];
const oauthEndpointTokenPath = cpiConfig["TOKEN_ENDPOINT"];
const clientId = config.clientId;
const clientSecret = config.clientSecret;

if (!oauthEndpointBaseUrl || !oauthEndpointTokenPath || !clientId || !clientSecret) {
console.error("Missing required environment variables for OAuth token retrieval");
exec.test.abort("Error: missing environment variables");
}

const baseUrl = `${oauthEndpointBaseUrl}/${oauthEndpointTokenPath}`;
const token = getToken(baseUrl, clientId, clientSecret, "READ");
if (!token) {
console.error("Failed to retrieve access token for CPI Gateway Integration Test");
exec.test.abort("Error: Failed to retrieve access token");
}

return {
cpiBaseUrl: cpiConfig["BASE_URL"],
token: token,
testData: config.data
};

}

export function runCPITests(params) {
//console.log("Running CPI Gateway Integration Test with config: " + JSON.stringify(params));
const { cpiBaseUrl, token, testData } = params;

var testDataForMedSanctions = testData["AO_WITH_MED_SANCTIONS"];
const providerResponse = getProvider(cpiBaseUrl, token, "ind", "ssn", testDataForMedSanctions.ao_ssn);

check(
providerResponse,
{
'get provider returns 200': res => res.status == 200,
'provider data type': res => isObjectType(res.json(), 'provider'),
'provider data returned': res => !isEmptyObject(res.json().provider),
'med sanctions data returned': res => isArrayType(res.json().provider, 'medSanctions') &&
res.json().provider.medSanctions.length > 0,
'med sanction code returned': res => res.json().provider.medSanctions[0].sanctionCode != null,
'med sanction date returned in right format': res => isDate(res.json().provider.medSanctions[0].sanctionDate),
}
)

var testDataForWaivers = testData["AO_WITH_WAIVERS"];
const waiverResponse = getProvider(cpiBaseUrl, token, "ind", "ssn", testDataForWaivers.ao_ssn);
check(
waiverResponse,
{
'get provider returns 200': res => res.status == 200,
'provider data type is object': res => isObjectType(res.json(), 'provider'),
'provider data returned': res => !isEmptyObject(res.json().provider),
'waiver data returned': res => isArrayType(res.json().provider, 'waiverInfo') &&
res.json().provider.waiverInfo.length > 0,
'waiver effectiveDate returned': res => res.json().provider.waiverInfo[0].effectiveDate != null &&
isDate(res.json().provider.waiverInfo[0].effectiveDate),
'waiver endDate valid': res => res.json().provider.waiverInfo[0].endDate &&
isDate(res.json().provider.waiverInfo[0].endDate),
}
)

var testDataForUnapprovedEnrollmentStatus = testData["UNAPPROVED_ENROLLMENT_STATUS"];
const orgProviderResponse = getProviderOrg(cpiBaseUrl, token, testDataForUnapprovedEnrollmentStatus.org_npi);
check(
orgProviderResponse,
{
'get org provider returns 200': res => res.status == 200,
'org provider data type is object': res => isObjectType(res.json(), 'provider'),
'org provider data returned': res => !isEmptyObject(res.json().provider),
'enrollments data returned': res => isArrayType(res.json().provider, 'enrollments') &&
res.json().provider.enrollments.length > 0,
'unapproved enrollment status': res => res.json().provider.enrollments.every(
enrollment => enrollment.status != "APPROVED")
}
)

var testDataForOrgWithAOInfo = testData["ORG_WITH_AO_SSN"];
const orgProviderWithAOResponse = getProviderOrg(cpiBaseUrl, token, testDataForOrgWithAOInfo.org_npi);
const ssnNoHyphens = /^\d{9}$/;
check(
orgProviderWithAOResponse,
{
'get org provider returns 200': res => res.status == 200,
'org provider data type is object': res => isObjectType(res.json(), 'provider'),
'org provider data returned': res => !isEmptyObject(res.json().provider),
'enrollments data returned': res => isArrayType(res.json().provider, 'enrollments') &&
res.json().provider.enrollments.length > 0,
'approved enrollment status': res => res.json().provider.enrollments.some(
enrollment => enrollment.status === "APPROVED"),
'roles data returned': res => {
let activeEnrollment = res.json().provider.enrollments.find(enrollment => enrollment.status === "APPROVED");
return !!activeEnrollment && isArrayType(activeEnrollment, 'roles') && activeEnrollment.roles.length > 0;
},
'AO info returned for org': res => {
let activeEnrollment = res.json().provider.enrollments.find(enrollment => enrollment.status === "APPROVED");
let roles = activeEnrollment ? activeEnrollment.roles : [];
return roles.some(role => role.roleCode === "10"
&& role.dataIndicator === "CURRENT"
&& !!role.ssn
&& ssnNoHyphens.test(role.ssn)
);
}

}
)

}

function getProvider(cpiBaseUrl, token, type, idType, id) {
const url = `${cpiBaseUrl}/${PROVIDERS_PATH}`;
const payload = providerRequest(type, idType, id);
const params = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
};
return http.post(url, payload, params);

}

function getProviderOrg(cpiBaseUrl, token, orgNpi) {
const url = `${cpiBaseUrl}/${ORG_PROVIDER_PROFILE_PATH}`;
const payload = orgProviderRequest(orgNpi);
const params = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
};
return http.post(url, payload, params);
}

function orgProviderRequest(npi) {
return JSON.stringify({ "providerID": { "npi": npi } });
}

function providerRequest(type, idType, id) {
return JSON.stringify(
{
"providerID": {
"providerType": type,
"identity": {
"idType": idType,
"id": id
}
},
"dataSets": {
"all": true
}
});
}








34 changes: 34 additions & 0 deletions dpc-load-testing/oauth-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*global console*/
/* eslint no-console: "off" */

import encoding from 'k6/encoding';
import http from 'k6/http';

export function getToken(oauthTokenUrl, clientId, clientSecret, scope) {

const payload = {
grant_type: 'client_credentials',
scope: scope
};

let encodedCreds = 'Basic ' + encodeCreds(clientId, clientSecret);
const params = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': encodedCreds
},
};

const response = http.post(oauthTokenUrl, payload, params);
if (response.status !== 200) {
console.error(`Failed to retrieve token: ${response.body}`);
return null;
}

const responseBody = response.json();
return responseBody.access_token;
}

export function encodeCreds(userName, password) {
return encoding.b64encode(`${userName}:${password}`);
}
6 changes: 3 additions & 3 deletions dpc-load-testing/resource-request-bodies.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ export function generateBundle(entries) {
"resourceType": "Parameters",
"parameter": [
{
"name": "resource",
"resource": {
"name": "resource",
"resource": {
"resourceType": "Bundle",
"type": "collection",
"entry": entries,
}
}
}
]
}
Expand Down
Loading
Loading