From ffd4f3dd3fa1d5e130a9bf641c5bedcc52095c28 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Tue, 10 Mar 2026 10:59:53 -0400 Subject: [PATCH 01/10] Initial commit: Integration tests for CPI gateway --- .github/workflows/cpi-integration-test.yml | 97 ++++++++++ .../integration_test/cpi_gateway_test.js | 179 ++++++++++++++++++ dpc-load-testing/resource-request-bodies.js | 6 +- dpc-load-testing/utils/test-utils.js | 49 +++++ 4 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/cpi-integration-test.yml create mode 100644 dpc-load-testing/integration_test/cpi_gateway_test.js diff --git a/.github/workflows/cpi-integration-test.yml b/.github/workflows/cpi-integration-test.yml new file mode 100644 index 0000000000..be5a24e169 --- /dev/null +++ b/.github/workflows/cpi-integration-test.yml @@ -0,0 +1,97 @@ +name: 'CPI-Integration Test' + +on: + workflow_dispatch: + workflow_call: + schedule: + - cron: '0 6 * * 1-5' + +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) + if: ${{ inputs.env == 'dev' || inputs.env == 'test' }} + 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-${{ inputs.env }}-github-actions + - 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 diff --git a/dpc-load-testing/integration_test/cpi_gateway_test.js b/dpc-load-testing/integration_test/cpi_gateway_test.js new file mode 100644 index 0000000000..77a0b98994 --- /dev/null +++ b/dpc-load-testing/integration_test/cpi_gateway_test.js @@ -0,0 +1,179 @@ +/*global console*/ +/* eslint no-console: "off" */ + +import { check } from 'k6'; +import exec from 'k6/execution'; +import { isEmptyObject, isObjectType, isDate, getToken, isArrayType } from '../utils/test-utils.js'; +import http from 'k6/http'; + +var PROVIDERS_PATH = "api/1.0/ppr/providers"; +var PROFILE_PATH = "api/1.0/ppr/providers/profiles"; +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() { + if (__ENV.ENVIRONMENT != 'local') { + return { + ...JSON.parse(__ENV.CPI_API_GW_TESTDATA), + ...{ clientId: __ENV.CLIENT_ID, clientSecret: __ENV.CLIENT_SECRET } + }; + } else { + return { + ...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 = getDataForProvider(cpiBaseUrl, token, "ind", "ssn", testDataForMedSanctions.ao_ssn); + + const checkProviderResponse = 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 = getDataForProvider(cpiBaseUrl, token, "ind", "ssn", testDataForWaivers.ao_ssn); + const checkWaiverResponse = check( + waiverResponse, + { + 'get provider returns 200': res => res.status == 200, + 'provider data type': 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), + } + ) +} + +function getDataForProvider(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 providerRequest(type, idType, id) { + return JSON.stringify( + { + "providerID": { + "providerType": type, + "identity": { + "idType": idType, + "id": id + } + }, + "dataSets": { + "all": true + } + }); +} + +function getProviderOrganization(cpiBaseUrl, token, npi) { + const url = `${cpiBaseUrl}/${PROFILE_PATH}`; + const payload = JSON.stringify( + { + "providerID": { + "npi": npi + } + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + }; + return http.post(url, payload, params); + +} + + + + + + + + diff --git a/dpc-load-testing/resource-request-bodies.js b/dpc-load-testing/resource-request-bodies.js index 8d98c2656f..593fd0ef57 100644 --- a/dpc-load-testing/resource-request-bodies.js +++ b/dpc-load-testing/resource-request-bodies.js @@ -103,12 +103,12 @@ export function generateBundle(entries) { "resourceType": "Parameters", "parameter": [ { - "name": "resource", - "resource": { + "name": "resource", + "resource": { "resourceType": "Bundle", "type": "collection", "entry": entries, - } + } } ] } diff --git a/dpc-load-testing/utils/test-utils.js b/dpc-load-testing/utils/test-utils.js index 6fb0ce2725..7b4924df0f 100644 --- a/dpc-load-testing/utils/test-utils.js +++ b/dpc-load-testing/utils/test-utils.js @@ -1,3 +1,52 @@ +import { b64encode } from 'k6/encoding'; +import http from 'k6/http'; + export function isArrayUnique(arr) { return Array.isArray(arr) && new Set(arr).size === arr.length; } + +export function isEmptyObject(obj) { + return obj && Object.keys(obj).length === 0 && obj.constructor === Object; +} + +export function isObjectType(obj, key) { + return obj && obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key]); +} + +export function isArrayType(obj, key) { + return obj && obj[key] && Array.isArray(obj[key]); +} + +export function isDate(dateString) { + const date = Date.parse(dateString); + return !isNaN(date); +} + +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 b64encode(`${userName}:${password}`); +} \ No newline at end of file From 9da045f0e6e47a418a22706977b68362fa37efc4 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Tue, 10 Mar 2026 22:29:22 -0400 Subject: [PATCH 02/10] Utilize ssm params to enable dev testing --- .github/workflows/cpi-integration-test.yml | 2 +- Makefile | 7 +++ .../integration_test/cpi_gateway_test.js | 27 ++++----- scripts/export_ssm_parameters.sh | 58 +++++++++++++++++++ 4 files changed, 77 insertions(+), 17 deletions(-) create mode 100755 scripts/export_ssm_parameters.sh diff --git a/.github/workflows/cpi-integration-test.yml b/.github/workflows/cpi-integration-test.yml index be5a24e169..06a4f40821 100644 --- a/.github/workflows/cpi-integration-test.yml +++ b/.github/workflows/cpi-integration-test.yml @@ -57,7 +57,7 @@ jobs: AWS_REGION: ${{ vars.AWS_REGION }} with: params: | - CPI_API_GW_TESTDATA=/dpc/test/web_portal/cpi_api_gw_testdata + 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 diff --git a/Makefile b/Makefile index a7e046d6db..96ce47da0a 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # ============== SMOKE_THREADS ?= 10 +SHELL := /bin/bash venv: venv/bin/activate @@ -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. diff --git a/dpc-load-testing/integration_test/cpi_gateway_test.js b/dpc-load-testing/integration_test/cpi_gateway_test.js index 77a0b98994..6f912bea33 100644 --- a/dpc-load-testing/integration_test/cpi_gateway_test.js +++ b/dpc-load-testing/integration_test/cpi_gateway_test.js @@ -8,7 +8,8 @@ import http from 'k6/http'; var PROVIDERS_PATH = "api/1.0/ppr/providers"; var PROFILE_PATH = "api/1.0/ppr/providers/profiles"; -var testData = __ENV.ENVIRONMENT == 'local' ? JSON.parse(open('./cpi_test_data.secret')) : {} +// 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: { @@ -29,19 +30,10 @@ export const options = { // This function aggregates the config data from different sources based upon // the environment and presents unified source for subsequent tests. function getConfig() { - if (__ENV.ENVIRONMENT != 'local') { - return { - ...JSON.parse(__ENV.CPI_API_GW_TESTDATA), - ...{ clientId: __ENV.CLIENT_ID, clientSecret: __ENV.CLIENT_SECRET } - }; - } else { - return { - ...testData, ...{ - clientId: __ENV.CLIENT_ID, - clientSecret: __ENV.CLIENT_SECRET - } - }; - } + return { + ...JSON.parse(__ENV.CPI_API_GW_TESTDATA), + ...{ clientId: __ENV.CLIENT_ID, clientSecret: __ENV.CLIENT_SECRET } + }; } export function setup() { @@ -91,7 +83,7 @@ export function runCPITests(params) { var testDataForMedSanctions = testData["AO_WITH_MED_SANCTIONS"]; const providerResponse = getDataForProvider(cpiBaseUrl, token, "ind", "ssn", testDataForMedSanctions.ao_ssn); - const checkProviderResponse = check( + check( providerResponse, { 'get provider returns 200': res => res.status == 200, @@ -106,7 +98,7 @@ export function runCPITests(params) { var testDataForWaivers = testData["AO_WITH_WAIVERS"]; const waiverResponse = getDataForProvider(cpiBaseUrl, token, "ind", "ssn", testDataForWaivers.ao_ssn); - const checkWaiverResponse = check( + check( waiverResponse, { 'get provider returns 200': res => res.status == 200, @@ -151,6 +143,8 @@ function providerRequest(type, idType, id) { }); } +/* Commenting this code out to pass linter rules till we get the +required data. function getProviderOrganization(cpiBaseUrl, token, npi) { const url = `${cpiBaseUrl}/${PROFILE_PATH}`; const payload = JSON.stringify( @@ -169,6 +163,7 @@ function getProviderOrganization(cpiBaseUrl, token, npi) { return http.post(url, payload, params); } +*/ diff --git a/scripts/export_ssm_parameters.sh b/scripts/export_ssm_parameters.sh new file mode 100755 index 0000000000..e657402d10 --- /dev/null +++ b/scripts/export_ssm_parameters.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Array with values using "ENV_VAR_NAME:SSM_PARAMETER_NAME" format +PROJECT="dpc" +ENV="test" +APP="web-portal" +PREFIX="/${PROJECT}/${ENV}/${APP}" + +ENV_SSM_MAPPINGS=( + "CLIENT_ID:${PREFIX}/cpi_api_gw_client_id" + "CLIENT_SECRET:${PREFIX}/cpi_api_gw_client_secret" + "CPI_API_GW_TESTDATA:${PREFIX}/cpi_api_gw_testdata" +) + +# 1. Validation: Check AWS Credentials +echo "Validating AWS credentials..." +if ! aws sts get-caller-identity --query "Arn" --output text > /dev/null 2>&1; then + echo "Error: AWS credentials not found or expired." + return 1 2>/dev/null || exit 1 +fi + +# 2. Collect all paths for the batch call by splitting the "ENV_VAR:SSM_PATH" entries +SSM_PATHS="" +for ENTRY in "${ENV_SSM_MAPPINGS[@]}"; do + SSM_PATH="${ENTRY#*:}" # Extract everything after the colon + SSM_PATHS="$SSM_PATHS $SSM_PATH" +done + +echo "Fetching parameters in batch..." + +# 3. Batch fetch (returns "Path Value" pairs) +# We use --output json and 'jq' if available, but here's a pure CLI way: +JSON_RESPONSE=$(aws ssm get-parameters \ + --names $SSM_PATHS \ + --with-decryption \ + --output json) + +# 4. Loop through our mappings and extract values using jq +for ENTRY in "${ENV_SSM_MAPPINGS[@]}"; do + ENV_VAR="${ENTRY%%:*}" + SSM_PATH="${ENTRY#*:}" + + echo "Searching for ENV_VAR: $ENV_VAR, SSM_PATH: $SSM_PATH" + + # Use jq to find the value where Name == SSM_PATH + # The '-r' flag in jq is CRITICAL: it outputs "raw" text (unquoted) + + VALUE=$(echo "$JSON_RESPONSE" | jq -r --arg PATH "$SSM_PATH" \ + '.Parameters[] | select(.Name == $PATH) | .Value') + + if [ -n "$VALUE" ]; then + export "$ENV_VAR"="$VALUE" + # echo "Exported $ENV_VAR (Length: ${#VALUE} chars)" + else + echo "Error: $SSM_PATH not found." + return 1 2>/dev/null || exit 1 + fi +done \ No newline at end of file From 309ae7bfbed3a42d2d40b2bf166648d7ae89221b Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Tue, 10 Mar 2026 22:42:12 -0400 Subject: [PATCH 03/10] Add eslint instructions to ignore missing console declarations --- dpc-load-testing/utils/test-utils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dpc-load-testing/utils/test-utils.js b/dpc-load-testing/utils/test-utils.js index 7b4924df0f..16d19eaf7b 100644 --- a/dpc-load-testing/utils/test-utils.js +++ b/dpc-load-testing/utils/test-utils.js @@ -1,3 +1,6 @@ +/*global console*/ +/* eslint no-console: "off" */ + import { b64encode } from 'k6/encoding'; import http from 'k6/http'; From 52c262ad5fdf541792201bc14c4427f4b842f023 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Tue, 10 Mar 2026 22:57:09 -0400 Subject: [PATCH 04/10] Minor change. Comment code awaiting test data --- dpc-load-testing/integration_test/cpi_gateway_test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dpc-load-testing/integration_test/cpi_gateway_test.js b/dpc-load-testing/integration_test/cpi_gateway_test.js index 6f912bea33..046aa240e4 100644 --- a/dpc-load-testing/integration_test/cpi_gateway_test.js +++ b/dpc-load-testing/integration_test/cpi_gateway_test.js @@ -7,7 +7,9 @@ import { isEmptyObject, isObjectType, isDate, getToken, isArrayType } from '../u import http from 'k6/http'; var PROVIDERS_PATH = "api/1.0/ppr/providers"; -var PROFILE_PATH = "api/1.0/ppr/providers/profiles"; + +//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')) : {} From e8b7f8c9c2c50d9dc2b03f9234b4679e62df05d1 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Tue, 10 Mar 2026 23:19:58 -0400 Subject: [PATCH 05/10] Minor refactoring to fix failing tests --- .../integration_test/cpi_gateway_test.js | 3 +- dpc-load-testing/oauth-client.js | 34 ++++++++++++++++++ dpc-load-testing/utils/test-utils.js | 35 ------------------- 3 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 dpc-load-testing/oauth-client.js diff --git a/dpc-load-testing/integration_test/cpi_gateway_test.js b/dpc-load-testing/integration_test/cpi_gateway_test.js index 046aa240e4..c4da6e3f38 100644 --- a/dpc-load-testing/integration_test/cpi_gateway_test.js +++ b/dpc-load-testing/integration_test/cpi_gateway_test.js @@ -3,7 +3,8 @@ import { check } from 'k6'; import exec from 'k6/execution'; -import { isEmptyObject, isObjectType, isDate, getToken, isArrayType } from '../utils/test-utils.js'; +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"; diff --git a/dpc-load-testing/oauth-client.js b/dpc-load-testing/oauth-client.js new file mode 100644 index 0000000000..ed59de6e42 --- /dev/null +++ b/dpc-load-testing/oauth-client.js @@ -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}`); +} \ No newline at end of file diff --git a/dpc-load-testing/utils/test-utils.js b/dpc-load-testing/utils/test-utils.js index 16d19eaf7b..a195811421 100644 --- a/dpc-load-testing/utils/test-utils.js +++ b/dpc-load-testing/utils/test-utils.js @@ -1,9 +1,3 @@ -/*global console*/ -/* eslint no-console: "off" */ - -import { b64encode } from 'k6/encoding'; -import http from 'k6/http'; - export function isArrayUnique(arr) { return Array.isArray(arr) && new Set(arr).size === arr.length; } @@ -24,32 +18,3 @@ export function isDate(dateString) { const date = Date.parse(dateString); return !isNaN(date); } - -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 b64encode(`${userName}:${password}`); -} \ No newline at end of file From 8472a3939e79abb4b4911345626441cdb854cc33 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Wed, 11 Mar 2026 13:10:17 -0400 Subject: [PATCH 06/10] Add push trigger to feature branch to test GHA workflow without merging --- .github/workflows/cpi-integration-test.yml | 2 ++ scripts/export_ssm_parameters.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cpi-integration-test.yml b/.github/workflows/cpi-integration-test.yml index 06a4f40821..c3a083072c 100644 --- a/.github/workflows/cpi-integration-test.yml +++ b/.github/workflows/cpi-integration-test.yml @@ -5,6 +5,8 @@ on: workflow_call: schedule: - cron: '0 6 * * 1-5' + push: + branches: [ 'mw/dpc-5238-integration-test' ] permissions: id-token: write diff --git a/scripts/export_ssm_parameters.sh b/scripts/export_ssm_parameters.sh index e657402d10..ac29c9bf68 100755 --- a/scripts/export_ssm_parameters.sh +++ b/scripts/export_ssm_parameters.sh @@ -29,7 +29,7 @@ done echo "Fetching parameters in batch..." # 3. Batch fetch (returns "Path Value" pairs) -# We use --output json and 'jq' if available, but here's a pure CLI way: +# Use --output json to enable querying using jq in next step JSON_RESPONSE=$(aws ssm get-parameters \ --names $SSM_PATHS \ --with-decryption \ From 2de5b69a23c7e4898c2c2d8d6e9a8c4610da6df6 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Wed, 11 Mar 2026 22:27:44 -0400 Subject: [PATCH 07/10] Pin go version to fix invalid go version '1.24.0' error --- .github/workflows/cpi-integration-test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/cpi-integration-test.yml b/.github/workflows/cpi-integration-test.yml index c3a083072c..857da11313 100644 --- a/.github/workflows/cpi-integration-test.yml +++ b/.github/workflows/cpi-integration-test.yml @@ -46,6 +46,10 @@ jobs: with: aws-region: ${{ vars.AWS_REGION }} role-to-assume: arn:aws:iam::${{ secrets.NON_PROD_ACCOUNT_ID }}:role/delegatedadmin/developer/dpc-${{ inputs.env }}-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 From 719ea8038b1ae45c9e3acafc91397380f1f8c858 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Thu, 12 Mar 2026 09:53:24 -0400 Subject: [PATCH 08/10] Remove the check for conditional execution as want auto triggers to trigger GHA and execution is for just test env. --- .github/workflows/cpi-integration-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cpi-integration-test.yml b/.github/workflows/cpi-integration-test.yml index 857da11313..d0b41e8666 100644 --- a/.github/workflows/cpi-integration-test.yml +++ b/.github/workflows/cpi-integration-test.yml @@ -41,7 +41,6 @@ jobs: - name: "Checkout code" uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: AWS Credentials (non-prod) - if: ${{ inputs.env == 'dev' || inputs.env == 'test' }} uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1 with: aws-region: ${{ vars.AWS_REGION }} From eac5395ad63a0ec14dc425a9cfa843dc471456fe Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Thu, 12 Mar 2026 10:17:35 -0400 Subject: [PATCH 09/10] Fix role value --- .github/workflows/cpi-integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cpi-integration-test.yml b/.github/workflows/cpi-integration-test.yml index d0b41e8666..1b375a191b 100644 --- a/.github/workflows/cpi-integration-test.yml +++ b/.github/workflows/cpi-integration-test.yml @@ -44,7 +44,7 @@ jobs: 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-${{ inputs.env }}-github-actions + 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: From e23386f9124175da01c722f780d8e50fa81c4b25 Mon Sep 17 00:00:00 2001 From: Manoj Wadhwa Date: Wed, 25 Mar 2026 20:30:09 -0400 Subject: [PATCH 10/10] Add new test cases --- .../integration_test/cpi_gateway_test.js | 94 ++++++++++++++----- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/dpc-load-testing/integration_test/cpi_gateway_test.js b/dpc-load-testing/integration_test/cpi_gateway_test.js index c4da6e3f38..61209edd5b 100644 --- a/dpc-load-testing/integration_test/cpi_gateway_test.js +++ b/dpc-load-testing/integration_test/cpi_gateway_test.js @@ -8,6 +8,8 @@ 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"; @@ -84,7 +86,7 @@ export function runCPITests(params) { const { cpiBaseUrl, token, testData } = params; var testDataForMedSanctions = testData["AO_WITH_MED_SANCTIONS"]; - const providerResponse = getDataForProvider(cpiBaseUrl, token, "ind", "ssn", testDataForMedSanctions.ao_ssn); + const providerResponse = getProvider(cpiBaseUrl, token, "ind", "ssn", testDataForMedSanctions.ao_ssn); check( providerResponse, @@ -100,12 +102,12 @@ export function runCPITests(params) { ) var testDataForWaivers = testData["AO_WITH_WAIVERS"]; - const waiverResponse = getDataForProvider(cpiBaseUrl, token, "ind", "ssn", testDataForWaivers.ao_ssn); + const waiverResponse = getProvider(cpiBaseUrl, token, "ind", "ssn", testDataForWaivers.ao_ssn); check( waiverResponse, { 'get provider returns 200': res => res.status == 200, - 'provider data type': res => isObjectType(res.json(), 'provider'), + '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, @@ -115,9 +117,55 @@ export function runCPITests(params) { 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 getDataForProvider(cpiBaseUrl, token, type, idType, id) { +function getProvider(cpiBaseUrl, token, type, idType, id) { const url = `${cpiBaseUrl}/${PROVIDERS_PATH}`; const payload = providerRequest(type, idType, id); const params = { @@ -130,6 +178,22 @@ function getDataForProvider(cpiBaseUrl, token, type, idType, id) { } +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( { @@ -146,28 +210,6 @@ function providerRequest(type, idType, id) { }); } -/* Commenting this code out to pass linter rules till we get the -required data. -function getProviderOrganization(cpiBaseUrl, token, npi) { - const url = `${cpiBaseUrl}/${PROFILE_PATH}`; - const payload = JSON.stringify( - { - "providerID": { - "npi": npi - } - }); - - const params = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - }; - return http.post(url, payload, params); - -} -*/ -