diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 625c2e889..1230bc4b4 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,15 +1,64 @@
-### Information
-- Fixes #_.
-- JIRA story _.
-
-### Changes proposed in this PR:
-- *(Please list what you changed.)*
--
--
-
-### Checklist
-- [ ] All JUnit tests pass (`mvn clean verify`).
-- [ ] New unit tests written to cover new functionality.
-- [ ] Added and updated JavaDocs for non-test classes and methods.
-- [ ] No local design debt. Do you feel that something is "ugly" after your changes?
-- [ ] Updated documentation (`README.md`, etc.) depending if the changes require it.
+
+
+## Related Tickets & Documents
+
+https://jira.cms.gov/browse/QPPA-XXXX
+
+---
+
+## Description
+See ticket description.
+
+---
+## What type of PR is this?
+
+- [ ] π Feature
+- [ ] π Bug Fix
+- [ ] π Documentation Update
+- [ ] π§βπ» Code Refactor
+- [ ] π₯ Performance Improvements
+- [ ] β
Test
+- [ ] π€ Build
+- [ ] π CI
+- [ ] π¦ Chore
+- [ ] β© Revert
+
+---
+
+## Added tests?
+- [ ] π yes
+- [ ] π
no, because they aren't needed
+- [ ] π no, because I need help
+---
+
+## Added to documentation?
+- [ ] π README.md
+- [ ] π Confluence
+- [ ] π
no documentation needed
+
+---
+### [ β₯ Optional ] Are there any post-deployment tasks we need to perform?
+---
+
+
+### [ β₯ Optional] What gif best describes this PR or how it makes you feel?
+
+* For Chromium users: https://chrome.google.com/webstore/detail/gifs-for-github/dkgjnpbipbdaoaadbdhpiokaemhlphep
+* For Firefox users: https://addons.mozilla.org/en-US/firefox/addon/gifs-for-github
+* After installing the extension, click on the "GIF" button in the comment box and search for a gif that describes your PR!
+
+
+
diff --git a/.github/workflows/codebuild-trigger-dev.yml b/.github/workflows/codebuild-trigger-dev.yml
index b417a9328..17a361fd3 100644
--- a/.github/workflows/codebuild-trigger-dev.yml
+++ b/.github/workflows/codebuild-trigger-dev.yml
@@ -5,7 +5,7 @@ on:
jobs:
build:
- name: conversion tool codebuil job
+ name: conversion tool codebuild job
permissions:
id-token: write
contents: read
diff --git a/.github/workflows/codebuild-trigger-devpre.yml b/.github/workflows/codebuild-trigger-devpre.yml
index 661d34f4a..3593f6f60 100644
--- a/.github/workflows/codebuild-trigger-devpre.yml
+++ b/.github/workflows/codebuild-trigger-devpre.yml
@@ -5,7 +5,7 @@ on:
jobs:
build:
- name: conversion tool codebuil job
+ name: conversion tool codebuild job
permissions:
id-token: write
contents: read
diff --git a/.github/workflows/codebuild-trigger-impl.yml b/.github/workflows/codebuild-trigger-impl.yml
index ad89dd6ea..387766f52 100644
--- a/.github/workflows/codebuild-trigger-impl.yml
+++ b/.github/workflows/codebuild-trigger-impl.yml
@@ -5,7 +5,7 @@ on:
jobs:
build:
- name: conversion tool codebuil job
+ name: conversion tool codebuild job
permissions:
id-token: write
contents: read
@@ -25,4 +25,4 @@ jobs:
- name: Execute ssh command
run: |
- aws codebuild start-build --project-name "qppa-conversion-tool-impl" --source-version "${GITHUB_REF##*/}"
\ No newline at end of file
+ aws codebuild start-build --project-name "qppa-conversion-tool-impl" --source-version "release/${GITHUB_REF##*/}"
\ No newline at end of file
diff --git a/.github/workflows/codebuild-trigger-prod.yml b/.github/workflows/codebuild-trigger-prod.yml
index a1b6453b9..f45b80ddc 100644
--- a/.github/workflows/codebuild-trigger-prod.yml
+++ b/.github/workflows/codebuild-trigger-prod.yml
@@ -5,7 +5,7 @@ on:
jobs:
build:
- name: conversion tool codebuil job
+ name: conversion tool codebuild job
permissions:
id-token: write
contents: read
diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml
new file mode 100644
index 000000000..de50a9044
--- /dev/null
+++ b/.github/workflows/draft-new-release.yml
@@ -0,0 +1,94 @@
+name: "Draft new release"
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "The version you want to release."
+ required: true
+
+jobs:
+ draft-new-release:
+ name: "Draft a new release"
+ runs-on: ubuntu-latest
+ if: github.ref_name == 'develop'
+ outputs:
+ commitSha: ${{ steps.make-commit.outputs.commit }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.ACTIONS_NICHOLAS_PAT }}
+
+ - name: Create release branch
+ run: |
+ git checkout -b release/v${{ github.event.inputs.version }};
+
+ - name: Create draft release
+ uses: release-drafter/release-drafter@v6
+ with:
+ config-name: release-draft.yml
+ version: v${{ github.event.inputs.version }}
+ tag: v${{ github.event.inputs.version }}
+ name: v${{ github.event.inputs.version }}
+ prerelease: false
+ publish: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.ACTIONS_NICHOLAS_PAT }}
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: "17"
+ distribution: "corretto"
+ cache: 'maven'
+
+ - name: Bump version in pom.xml files
+ run: |
+
+ # Update parent and child module pom.xml files
+ echo "Updating version in parent and module pom.xml files to ${{ github.event.inputs.version }}-RELEASE..."
+ mvn versions:set -DnewVersion=${{ github.event.inputs.version }}-RELEASE -DgenerateBackupPoms=false
+
+ # Update standalone module pom.xml files
+ echo "Updating standalone module versions..."
+ mvn versions:set -DnewVersion=${{ github.event.inputs.version }}-RELEASE -DgenerateBackupPoms=false -f acceptance-tests/pom.xml
+ mvn versions:set -DnewVersion=${{ github.event.inputs.version }}-RELEASE -DgenerateBackupPoms=false -f generate-race-cpcplus/pom.xml
+ mvn versions:set -DnewVersion=${{ github.event.inputs.version }}-RELEASE -DgenerateBackupPoms=false -f qrda3-update-measures/pom.xml
+ echo "Version update complete."
+
+ # In order to make a commit, we need to initialize a user.
+ # You may choose to write something less generic here if you want, it doesn't matter functionality-wise.
+ - name: Initialize mandatory git config
+ run: |
+ git config user.name "GitHub actions"
+ git config user.email noreply@github.com
+
+ - name: Commit pom files
+ run: |
+ git add '**/pom.xml' pom.xml
+ git commit --message "Prepare release v${{ github.event.inputs.version }}"
+
+ - name: Fetch commit hash
+ id: make-commit
+ run: echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
+
+ - name: Push new branch
+ run: |
+ git push origin release/v${{ github.event.inputs.version }}
+
+ - name: Create pull request to master branch
+ uses: thomaseizinger/create-pull-request@1.4.0
+ env:
+ GITHUB_TOKEN: ${{ secrets.ACTIONS_NICHOLAS_PAT }} # MUST use a PAT here in order to trigger the next workflow: CodeBuild Trigger; see https://docs.github.com/en/actions/using-workflows/triggering-a-workflow
+ with:
+ head: release/v${{ github.event.inputs.version }}
+ base: master
+ draft: true
+ title: Deploy Release version v${{ github.event.inputs.version }} to Prod
+ body: |
+ Hi @${{ github.actor }}!
+
+ This PR was created in response to a manual trigger of the release workflow here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}.
+ The changelog was updated and the version was bumped in the manifest files in this commit: ${{ steps.make-commit.outputs.commit }}.
+
+ Merging this PR will create a GitHub release.
diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml
deleted file mode 100644
index 281637537..000000000
--- a/.github/workflows/draft-release.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Draft Release Notes
-
-on: create
-
-jobs:
-
- release_draft:
- if: github.ref == 'refs/heads/release/'
- name: Create a new release
- runs-on: ubuntu-latest
- steps:
-
- - name: Checkout codebase
- uses: actions/checkout@v2
- with:
- fetch-depth: 2
-
- - name: Detect new tag version from pom.xml
- id: package-version-prod-impl
- uses: salsify/action-detect-and-tag-new-version@68bbe8670f415d304e02942186441939c4692aa6 # pin@v1.0.3
- with:
- version-command: |
- mvn org.apache.maven.plugins:maven-help-plugin:3.4.0:evaluate -Dexpression=project.version | grep -v '\['
-
- - name: Draft release notes
- uses: release-drafter/release-drafter@06d4616a80cd7c09ea3cf12214165ad6c1859e67 #v5.11
- with:
- config-name: release-draft.yml
- version: v${{ steps.package-version.outputs.current-version }}
- tag: v${{ steps.package-version.outputs.current-version }}
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
new file mode 100644
index 000000000..70475584d
--- /dev/null
+++ b/.github/workflows/publish-release.yml
@@ -0,0 +1,67 @@
+name: Publish GitHub Release and Backfill
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ publish_and_backfill:
+ name: Publish GitHub release and create backfill PR
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Checkout codebase
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Get latest draft release
+ id: get_release
+ run: |
+ # Get the latest draft release
+ RELEASE_DATA=$(gh release list --limit 1 --json isDraft,tagName,name | jq -r '.[0]')
+ IS_DRAFT=$(echo "$RELEASE_DATA" | jq -r '.isDraft')
+ TAG_NAME=$(echo "$RELEASE_DATA" | jq -r '.tagName')
+
+ echo "is_draft=${IS_DRAFT}" >> $GITHUB_OUTPUT
+ echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
+ echo "Found release: ${TAG_NAME} (draft: ${IS_DRAFT})"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Publish draft release
+ if: steps.get_release.outputs.is_draft == 'true'
+ run: |
+ gh release edit ${{ steps.get_release.outputs.tag_name }} --draft=false --latest
+ echo "Published release ${{ steps.get_release.outputs.tag_name }} as latest"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Create backfill PR to develop
+ run: |
+ # Check if a PR already exists
+ EXISTING_PR=$(gh pr list --base develop --head master --json number --jq '.[0].number' || echo "")
+
+ if [ -n "$EXISTING_PR" ]; then
+ echo "PR #${EXISTING_PR} already exists for master -> develop"
+ exit 0
+ fi
+
+ # Create the backfill PR
+ gh pr create \
+ --base develop \
+ --head master \
+ --title "Backfill master into develop" \
+ --body "This PR backfills changes from master into develop after production deployment.
+
+ ## Changes
+ This includes all changes that were merged to master.
+
+ ## Notes
+ - Review for any conflicts
+ - Merge after verifying all changes are appropriate for develop branch"
+
+ echo "Created backfill PR from master to develop"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/acceptance-tests/pom.xml b/acceptance-tests/pom.xml
index 5bffbf7e0..681cb59fd 100644
--- a/acceptance-tests/pom.xml
+++ b/acceptance-tests/pom.xml
@@ -3,7 +3,7 @@
4.0.0
acceptance-tests
gov.cms.qpp.conversion
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
conversion-tests
jar
diff --git a/buildspec/build_deploy.yaml b/buildspec/build_deploy.yaml
index 9f96caa64..a9fb9b535 100644
--- a/buildspec/build_deploy.yaml
+++ b/buildspec/build_deploy.yaml
@@ -60,7 +60,8 @@ phases:
fi
- echo "${CLUSTER_NAME}"
- SERVICE_NAME="${ENV}-conversion-tool"
- - BRANCH=$(echo "${CODEBUILD_SOURCE_VERSION}")
+ # Extract the branch name from the CodeBuild source version. The source version is in the format "branch/branch-name" for GitHub source, so we can use 'sed' to remove the "branch/" prefix and get just the branch name.
+ - BRANCH=$(echo "${CODEBUILD_SOURCE_VERSION}" | sed 's/.*\///')
- echo ${CODEBUILD_RESOLVED_SOURCE_VERSION}
- echo "${BRANCH}"
- COMMIT_SHORT_SHA=$(echo "${CODEBUILD_RESOLVED_SOURCE_VERSION}" | cut -c1-7)
diff --git a/commandline/pom.xml b/commandline/pom.xml
index 46ac280a8..8da3eba94 100644
--- a/commandline/pom.xml
+++ b/commandline/pom.xml
@@ -6,7 +6,7 @@
gov.cms.qpp.conversion
qpp-conversion-tool-parent
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
../pom.xml
diff --git a/commons/pom.xml b/commons/pom.xml
index 0edb846b9..11fdf3d7d 100644
--- a/commons/pom.xml
+++ b/commons/pom.xml
@@ -6,7 +6,7 @@
gov.cms.qpp.conversion
qpp-conversion-tool-parent
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
../pom.xml
diff --git a/converter/pom.xml b/converter/pom.xml
index d0aec7a52..5854a098d 100644
--- a/converter/pom.xml
+++ b/converter/pom.xml
@@ -6,7 +6,7 @@
gov.cms.qpp.conversion
qpp-conversion-tool-parent
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
../pom.xml
@@ -185,7 +185,7 @@
gov.cms.qpp.conversion
commons
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
compile
diff --git a/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/CorrelationTest.java b/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/CorrelationTest.java
new file mode 100644
index 000000000..166403cea
--- /dev/null
+++ b/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/CorrelationTest.java
@@ -0,0 +1,54 @@
+package gov.cms.qpp.conversion.correlation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+class CorrelationTest {
+
+ @Test
+ void gettersAndSetters_work() {
+ Correlation correlation = new Correlation();
+
+ correlation.setCorrelationId("corr-1");
+ assertThat(correlation.getCorrelationId()).isEqualTo("corr-1");
+ }
+
+ @Test
+ void getConfig_returnsDefensiveCopy_changesToReturnedListDontAffectInternalState() {
+ Correlation correlation = new Correlation();
+
+ List first = correlation.getConfig();
+ assertThat(first).isEmpty();
+
+ first.add(new CorrelationConfig());
+
+ assertThat(correlation.getConfig()).isEmpty();
+ }
+
+ @Test
+ void setConfig_copiesInputList_laterMutationsToInputDontAffectInternalState() {
+ Correlation correlation = new Correlation();
+
+ List input = new ArrayList<>();
+ input.add(new CorrelationConfig());
+
+ correlation.setConfig(input);
+ assertThat(correlation.getConfig()).hasSize(1);
+
+ input.add(new CorrelationConfig());
+
+ assertThat(correlation.getConfig()).hasSize(1);
+ }
+
+ @Test
+ void setConfig_null_throwsNpe() {
+ Correlation correlation = new Correlation();
+
+ assertThrows(NullPointerException.class, () -> correlation.setConfig(null));
+ }
+}
diff --git a/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/GoodsTest.java b/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/GoodsTest.java
new file mode 100644
index 000000000..faa31cee3
--- /dev/null
+++ b/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/GoodsTest.java
@@ -0,0 +1,30 @@
+package gov.cms.qpp.conversion.correlation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class GoodsTest {
+
+ @Test
+ void gettersAndSetters_work() {
+ Goods goods = new Goods();
+
+ goods.setRelativeXPath("/ClinicalDocument/id");
+ goods.setXmltype("QRDA-III");
+
+ assertThat(goods.getRelativeXPath()).isEqualTo("/ClinicalDocument/id");
+ assertThat(goods.getXmltype()).isEqualTo("QRDA-III");
+ }
+
+ @Test
+ void allowsNulls() {
+ Goods goods = new Goods();
+
+ goods.setRelativeXPath(null);
+ goods.setXmltype(null);
+
+ assertThat(goods.getRelativeXPath()).isNull();
+ assertThat(goods.getXmltype()).isNull();
+ }
+}
diff --git a/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/TemplateTest.java b/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/TemplateTest.java
new file mode 100644
index 000000000..b5e342d35
--- /dev/null
+++ b/converter/src/test/java/gov/cms/qpp/conversion/correlation/model/TemplateTest.java
@@ -0,0 +1,30 @@
+package gov.cms.qpp.conversion.correlation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class TemplateTest {
+
+ @Test
+ void gettersAndSetters_work() {
+ Template template = new Template();
+
+ template.setTemplateId("2.16.840.1.113883.10.20.27.3.1");
+ template.setCorrelationId("corr-123");
+
+ assertThat(template.getTemplateId()).isEqualTo("2.16.840.1.113883.10.20.27.3.1");
+ assertThat(template.getCorrelationId()).isEqualTo("corr-123");
+ }
+
+ @Test
+ void allowsNulls() {
+ Template template = new Template();
+
+ template.setTemplateId(null);
+ template.setCorrelationId(null);
+
+ assertThat(template.getTemplateId()).isNull();
+ assertThat(template.getCorrelationId()).isNull();
+ }
+}
diff --git a/generate-race-cpcplus/pom.xml b/generate-race-cpcplus/pom.xml
index ae0224206..7f2a135e2 100644
--- a/generate-race-cpcplus/pom.xml
+++ b/generate-race-cpcplus/pom.xml
@@ -2,15 +2,20 @@
-
- qpp-conversion-tool-parent
- gov.cms.qpp.conversion
- 2026.01.28.01-RELEASE
- ../
-
4.0.0
+ gov.cms.qpp.conversion
generateRaceCpcPlus
+ 2026.02.17.01-RELEASE
+ generate-race-cpcplus
+ jar
+
+
+ 17
+ UTF-8
+ ${java.version}
+ ${java.version}
+
diff --git a/generate/pom.xml b/generate/pom.xml
index 52e563ed5..ebca4f022 100644
--- a/generate/pom.xml
+++ b/generate/pom.xml
@@ -5,7 +5,7 @@
qpp-conversion-tool-parent
gov.cms.qpp.conversion
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
../pom.xml
4.0.0
diff --git a/pom.xml b/pom.xml
index b7f2a8651..12ce54b7d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
gov.cms.qpp.conversion
qpp-conversion-tool-parent
pom
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
QPP Conversion Tool
diff --git a/qrda3-update-measures/pom.xml b/qrda3-update-measures/pom.xml
index 1798b3887..374c4795f 100644
--- a/qrda3-update-measures/pom.xml
+++ b/qrda3-update-measures/pom.xml
@@ -1,15 +1,19 @@
-
- qpp-conversion-tool-parent
- gov.cms.qpp.conversion
- 2026.01.28.01-RELEASE
- ../
-
+ 4.0.0
+ gov.cms.qpp.conversion
qpp-update-measures
+ 2026.02.17.01-RELEASE
+ qrda3-update-measures
jar
- 4.0.0
+
+
+ 17
+ UTF-8
+ ${java.version}
+ ${java.version}
+
diff --git a/rest-api/pom.xml b/rest-api/pom.xml
index c4449322b..ee1a28586 100644
--- a/rest-api/pom.xml
+++ b/rest-api/pom.xml
@@ -19,7 +19,7 @@
gov.cms.qpp.conversion
qpp-conversion-tool-parent
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
../pom.xml
diff --git a/rest-api/src/main/java/gov/cms/qpp/conversion/api/config/SecurityConfig.java b/rest-api/src/main/java/gov/cms/qpp/conversion/api/config/SecurityConfig.java
index 23be0533d..b0d147390 100644
--- a/rest-api/src/main/java/gov/cms/qpp/conversion/api/config/SecurityConfig.java
+++ b/rest-api/src/main/java/gov/cms/qpp/conversion/api/config/SecurityConfig.java
@@ -49,4 +49,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.build();
}
-}
+}
\ No newline at end of file
diff --git a/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImpl.java b/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImpl.java
index bd06213d3..746c9d13b 100644
--- a/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImpl.java
+++ b/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImpl.java
@@ -20,6 +20,8 @@
import gov.cms.qpp.conversion.api.model.Constants;
import gov.cms.qpp.conversion.api.services.StorageService;
+import java.io.FilterInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@@ -99,12 +101,11 @@ public InputStream getFileByLocationId(String fileLocationId) {
API_LOG.info("Retrieving file {} from bucket {}", fileLocationId, bucketName);
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, fileLocationId);
-
- S3Object s3Object = amazonS3.getObject(getObjectRequest);
+ InputStream objectContent = retrieveManagedContentStream(getObjectRequest);
API_LOG.info("Successfully retrieved file {} from S3 bucket {}", getObjectRequest.getKey(), getObjectRequest.getBucketName());
- return s3Object.getObjectContent();
+ return objectContent;
}
/**
@@ -123,9 +124,7 @@ public InputStream getCpcPlusValidationFile() {
API_LOG.info("Retrieving CPC+ validation file from bucket {}", bucketName);
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key);
- S3Object s3Object = amazonS3.getObject(getObjectRequest);
-
- return s3Object.getObjectContent();
+ return retrieveManagedContentStream(getObjectRequest);
}
/**
@@ -143,9 +142,15 @@ public InputStream getApmValidationFile(String fileName) {
API_LOG.info("Retrieving APM validation file from bucket {}", bucketName);
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, fileName);
- S3Object s3Object = amazonS3.getObject(getObjectRequest);
+ return retrieveManagedContentStream(getObjectRequest);
+ }
- return s3Object.getObjectContent();
+ /**
+ * Provides an {@link InputStream} whose close also closes the backing {@link S3Object}.
+ */
+ private InputStream retrieveManagedContentStream(GetObjectRequest getObjectRequest) {
+ S3Object s3Object = amazonS3.getObject(getObjectRequest);
+ return new ManagedS3InputStream(s3Object);
}
/**
@@ -176,4 +181,34 @@ protected String asynchronousAction(Supplier objectToActOn) {
protected String getActionName() {
return "Write to Storage";
}
+
+ private static final class ManagedS3InputStream extends FilterInputStream {
+ private final S3Object s3Object;
+
+ ManagedS3InputStream(S3Object s3Object) {
+ super(s3Object.getObjectContent());
+ this.s3Object = s3Object;
+ }
+
+ @Override
+ public void close() throws IOException {
+ IOException firstException = null;
+ try {
+ super.close();
+ } catch (IOException e) {
+ firstException = e;
+ throw e;
+ } finally {
+ try {
+ s3Object.close();
+ } catch (IOException e) {
+ if (firstException != null) {
+ firstException.addSuppressed(e);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/RestApiApplicationTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/RestApiApplicationTest.java
index c9743ae26..a16c57eb3 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/RestApiApplicationTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/RestApiApplicationTest.java
@@ -1,19 +1,13 @@
package gov.cms.qpp.conversion.api;
-import org.junit.jupiter.api.Test;
+import static org.junit.Assert.assertNotNull;
-@SpringTest
-class RestApiApplicationTest {
+import org.junit.Test;
-// @Test
-// void contextLoads() {
-// }
-//
-// @Test
-// void testMain() {
-// RestApiApplication.main();
-//
-//
-// }
+public class RestApiApplicationTest {
+ @Test
+ public void mainMethodExists() throws Exception {
+ assertNotNull(RestApiApplication.class.getMethod("main", String[].class));
+ }
}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v2/ZipControllerTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v2/ZipControllerTest.java
index be8e49eac..7be5c52bc 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v2/ZipControllerTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v2/ZipControllerTest.java
@@ -3,34 +3,29 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
+import java.io.UncheckedIOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import gov.cms.qpp.conversion.ConversionReport;
import gov.cms.qpp.conversion.Source;
-import gov.cms.qpp.conversion.api.exceptions.AuditException;
import gov.cms.qpp.conversion.api.exceptions.BadZipException;
import gov.cms.qpp.conversion.api.model.ConvertResponse;
import gov.cms.qpp.conversion.api.model.Metadata;
@@ -38,21 +33,11 @@
import gov.cms.qpp.conversion.api.services.QrdaService;
import gov.cms.qpp.conversion.api.services.ValidationService;
import gov.cms.qpp.conversion.encode.JsonWrapper;
-import gov.cms.qpp.conversion.model.error.TransformException;
import gov.cms.qpp.test.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ZipControllerTest {
- private static final String GOOD_FILE_CONTENT = "good-file";
-
- static final Path validationJsonFilePath = Path.of("src/test/resources/testCpcPlusValidationFile.json");
- static final Path goodZipFilePath = Path.of("src/test/resources/good-file.zip");
-
- private MultipartFile multipartFile;
-
- private InputStream validationInputStream;
-
@InjectMocks
private ZipController objectUnderTest;
@@ -68,125 +53,127 @@ class ZipControllerTest {
@Mock
private ConversionReport report;
- @Mock
- private CompletableFuture mockMetadata;// = Mockito.mock(CompletableFuture.class);
-
-
@BeforeEach
- void initialization() throws IOException {
+ void setup() {
JsonWrapper wrapper = new JsonWrapper();
wrapper.put("key", "Good Qpp");
- validationInputStream = Files.newInputStream(validationJsonFilePath);
-
when(report.getEncodedWithMetadata()).thenReturn(wrapper);
-
- multipartFile = new MockMultipartFile(GOOD_FILE_CONTENT, Files.newInputStream(goodZipFilePath));
+ when(qrdaService.retrieveCpcPlusValidationFile()).thenReturn(null);
}
@Test
- void uploadQrdaFile() {
- Metadata metadata = Metadata.create();
+ void uploadQrdaFile_happyPath_returnsResponsesForFiles() throws Exception {
+ byte[] zipBytes = zipOf(
+ entry("a.xml", "".getBytes()),
+ entry("b.xml", "".getBytes())
+ );
+
+ MultipartFile multipartFile =
+ new MockMultipartFile("good.zip", "good.zip", "application/zip", zipBytes);
+
when(qrdaService.convertQrda3ToQpp(any(Source.class))).thenReturn(report);
- when(qrdaService.retrieveCpcPlusValidationFile()).thenReturn(validationInputStream);
- when(auditService.success(any(ConversionReport.class)))
- .then(invocation -> CompletableFuture.completedFuture(metadata));
+ when(auditService.success(any(ConversionReport.class))).thenReturn(null);
- ResponseEntity> qppResponse = objectUnderTest.uploadQrdaFile(multipartFile, null);
+ ResponseEntity> response =
+ objectUnderTest.uploadQrdaFile(multipartFile, null);
+ assertThat(response.getBody()).hasSize(2);
+ assertThat(response.getBody().get(0).getQpp()).isNotNull();
verify(qrdaService, atLeastOnce()).convertQrda3ToQpp(any(Source.class));
-
- assertThat(qppResponse.getBody().get(0).getQpp().toString())
- .isEqualTo("{\n"
- + " \"key\" : \"Good Qpp\"\n"
- + "}");
}
@Test
- void uploadTestQrdaFile() {
- ArgumentCaptor peopleCaptor = ArgumentCaptor.forClass(Source.class);
+ void uploadQrdaFile_skipsDirectoryEntries() throws Exception {
+ byte[] zipBytes = zipOf(
+ dir("folder/"),
+ entry("folder/a.xml", "".getBytes())
+ );
- when(qrdaService.convertQrda3ToQpp(peopleCaptor.capture())).thenReturn(report);
- when(qrdaService.retrieveCpcPlusValidationFile()).thenReturn(validationInputStream);
- when(auditService.success(any(ConversionReport.class)))
- .then(invocation -> null);
+ MultipartFile multipartFile = new MockMultipartFile("good.zip", "good.zip", "application/zip", zipBytes);
- when(report.getPurpose()).thenReturn("Test");
- ResponseEntity> qppResponse = objectUnderTest.uploadQrdaFile(multipartFile, "Test");
+ when(qrdaService.convertQrda3ToQpp(any(Source.class))).thenReturn(report);
+ when(auditService.success(any(ConversionReport.class))).thenReturn(null);
- assertThat(qppResponse).isNotNull();
- assertThat(peopleCaptor.getValue().getPurpose()).isEqualTo("Test");
- }
+ ResponseEntity> response = objectUnderTest.uploadQrdaFile(multipartFile, null);
- @Test
- void uploadNullQrdaFile() {
- Assertions.assertThrows(BadZipException.class, () -> {
- objectUnderTest.uploadQrdaFile(new MockMultipartFile("null.zip", new byte[0]), "Test");
- });
+ assertThat(response.getBody()).hasSize(2);
+
+ verify(qrdaService, atLeastOnce()).convertQrda3ToQpp(any(Source.class));
}
@Test
- void uploadQrdaFile_auditInterruptionException() throws Exception {
- ArgumentCaptor peopleCaptor = ArgumentCaptor.forClass(Source.class);
-
- when(qrdaService.convertQrda3ToQpp(peopleCaptor.capture())).thenReturn(report);
- when(qrdaService.retrieveCpcPlusValidationFile()).thenReturn(validationInputStream);
- when(auditService.success(any(ConversionReport.class))).thenReturn(mockMetadata);
- when(mockMetadata.get()).thenThrow(new InterruptedException("Testing Audit Exception Handling"));
-
- String purpose = "Test";
- when(report.getPurpose()).thenReturn(purpose);
- assertThrows(AuditException.class, () -> objectUnderTest.uploadQrdaFile(multipartFile, purpose));
+ void uploadQrdaFile_invalidZipBytes_throwsBadZipException() {
+ MultipartFile multipartFile = new MockMultipartFile(
+ "bad.zip", "bad.zip", "application/zip", "not-a-zip".getBytes());
+
+ assertThrows(BadZipException.class, () -> objectUnderTest.uploadQrdaFile(multipartFile, null));
}
@Test
- void uploadQrdaFile_auditExecutionException() throws Exception {
- ArgumentCaptor peopleCaptor = ArgumentCaptor.forClass(Source.class);
-
- when(qrdaService.convertQrda3ToQpp(peopleCaptor.capture())).thenReturn(report);
- when(qrdaService.retrieveCpcPlusValidationFile()).thenReturn(validationInputStream);
- when(auditService.success(any(ConversionReport.class))).thenReturn(mockMetadata);
- when(mockMetadata.get()).thenThrow(new ExecutionException(new RuntimeException("Testing Audit Exception Handling")));
-
- String purpose = "Test";
- when(report.getPurpose()).thenReturn(purpose);
- assertThrows(AuditException.class, () -> objectUnderTest.uploadQrdaFile(multipartFile, purpose));
+ void uploadQrdaFile_whenTransferToThrowsIOException_throwsUncheckedIOException() throws Exception {
+ MultipartFile brokenFile = org.mockito.Mockito.mock(MultipartFile.class);
+
+ org.mockito.Mockito.doThrow(new IOException("boom"))
+ .when(brokenFile)
+ .transferTo(any(java.io.File.class));
+
+ assertThrows(UncheckedIOException.class, () -> objectUnderTest.uploadQrdaFile(brokenFile, null));
}
@Test
- void uploadQrdaFile_nullCpcValidationMap() {
- ArgumentCaptor peopleCaptor = ArgumentCaptor.forClass(Source.class);
+ void uploadQrdaFile_whenAuditReturnsMetadata_setsLocation() throws Exception {
+ byte[] zipBytes = zipOf(entry("a.xml", "".getBytes()));
+ MultipartFile multipartFile =
+ new MockMultipartFile("good.zip", "good.zip", "application/zip", zipBytes);
- when(qrdaService.convertQrda3ToQpp(peopleCaptor.capture())).thenReturn(report);
- when(qrdaService.retrieveCpcPlusValidationFile()).thenReturn(null);
- when(auditService.success(any(ConversionReport.class))).then(invocation -> null);
+ when(qrdaService.convertQrda3ToQpp(any(Source.class))).thenReturn(report);
+
+ Metadata metadata = Metadata.create();
+ metadata.setUuid("uuid-123");
+
+ when(auditService.success(any(ConversionReport.class)))
+ .thenReturn(CompletableFuture.completedFuture(metadata));
- String purpose = "Test";
- when(report.getPurpose()).thenReturn(purpose);
- ResponseEntity> qppResponse = objectUnderTest.uploadQrdaFile(multipartFile, purpose);
-
- assertThat(qppResponse).isNotNull();
+ ResponseEntity> response =
+ objectUnderTest.uploadQrdaFile(multipartFile, null);
+
+ assertThat(response.getBody()).hasSize(1);
+ assertThat(response.getBody().get(0).getLocation()).isEqualTo("uuid-123");
}
- @Test
- void testFailedQppValidation() {
- String transformationErrorMessage = "Test failed QPP validation";
-
- when(qrdaService.convertQrda3ToQpp(any(Source.class)))
- .thenReturn(null);
- when(qrdaService.retrieveCpcPlusValidationFile()).thenReturn(validationInputStream);
- Mockito.doThrow(new TransformException(transformationErrorMessage, null, null))
- .when(validationService).validateQpp(isNull());
-
- try {
- ResponseEntity> qppResponse = objectUnderTest.uploadQrdaFile(multipartFile, null);
- Assertions.fail("An exception should have occurred. Instead was " + qppResponse);
- } catch(TransformException exception) {
- assertThat(exception.getMessage())
- .isEqualTo(transformationErrorMessage);
- } catch (Exception exception) {
- Assertions.fail("The wrong exception occurred.");
+ private static ZipSpec entry(String name, byte[] bytes) {
+ return new ZipSpec(name, bytes, false);
+ }
+
+ private static ZipSpec dir(String name) {
+ return new ZipSpec(name, new byte[0], true);
+ }
+
+ private static byte[] zipOf(ZipSpec... specs) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+ for (ZipSpec spec : specs) {
+ ZipEntry ze = new ZipEntry(spec.name);
+ zos.putNextEntry(ze);
+ if (!spec.isDir && spec.bytes.length > 0) {
+ zos.write(spec.bytes);
+ }
+ zos.closeEntry();
+ }
}
+ return baos.toByteArray();
}
+ private static final class ZipSpec {
+ private final String name;
+ private final byte[] bytes;
+ private final boolean isDir;
+
+ private ZipSpec(String name, byte[] bytes, boolean isDir) {
+ this.name = name;
+ this.bytes = bytes;
+ this.isDir = isDir;
+ }
+ }
}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/helper/AdvancedApmHelperTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/helper/AdvancedApmHelperTest.java
new file mode 100644
index 000000000..a0f0d55a1
--- /dev/null
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/helper/AdvancedApmHelperTest.java
@@ -0,0 +1,99 @@
+package gov.cms.qpp.conversion.api.helper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import gov.cms.qpp.conversion.api.model.Constants;
+import gov.cms.qpp.conversion.api.model.Metadata;
+import gov.cms.qpp.conversion.api.model.UnprocessedFileData;
+
+class AdvancedApmHelperTest {
+
+ @AfterEach
+ void tearDown() {
+ System.clearProperty(Constants.NO_CPC_PLUS_API_ENV_VARIABLE);
+ }
+
+ @Test
+ void isPcfFile_nullMetadata_false() {
+ assertThat(AdvancedApmHelper.isPcfFile(null)).isFalse();
+ }
+
+ @Test
+ void isPcfFile_pcfNull_false() {
+ Metadata m = new Metadata();
+ m.setPcf(null);
+
+ assertThat(AdvancedApmHelper.isPcfFile(m)).isFalse();
+ }
+
+ @Test
+ void isPcfFile_pcfSet_true() {
+ Metadata m = new Metadata();
+ m.setPcf("PCF");
+
+ assertThat(AdvancedApmHelper.isPcfFile(m)).isTrue();
+ }
+
+ @Test
+ void isAValidUnprocessedFile_notPcf_false() {
+ Metadata m = new Metadata();
+ m.setPcf(null);
+ m.setCpcProcessed(false);
+ m.setRtiProcessed(false);
+
+ assertThat(AdvancedApmHelper.isAValidUnprocessedFile(m)).isFalse();
+ }
+
+ @Test
+ void isAValidUnprocessedFile_pcf_andEitherFlagFalse_true() {
+ Metadata m = new Metadata();
+ m.setPcf("PCF");
+ m.setCpcProcessed(true);
+ m.setRtiProcessed(false);
+
+ assertThat(AdvancedApmHelper.isAValidUnprocessedFile(m)).isTrue();
+ }
+
+ @Test
+ void isAValidUnprocessedFile_pcf_andBothTrue_false() {
+ Metadata m = new Metadata();
+ m.setPcf("PCF");
+ m.setCpcProcessed(true);
+ m.setRtiProcessed(true);
+
+ assertThat(AdvancedApmHelper.isAValidUnprocessedFile(m)).isFalse();
+ }
+
+ @Test
+ void transformMetaDataToUnprocessedFileData_mapsAllItems() {
+ Metadata m1 = new Metadata();
+ Metadata m2 = new Metadata();
+
+ List result =
+ AdvancedApmHelper.transformMetaDataToUnprocessedFileData(Arrays.asList(m1, m2));
+
+ assertThat(result).hasSize(2);
+ assertThat(result.get(0)).isInstanceOf(UnprocessedFileData.class);
+ assertThat(result.get(1)).isInstanceOf(UnprocessedFileData.class);
+ }
+
+ @Test
+ void blockAdvancedApmApis_whenEnvVarNotPresent_false() {
+ System.clearProperty(Constants.NO_CPC_PLUS_API_ENV_VARIABLE);
+
+ assertThat(AdvancedApmHelper.blockAdvancedApmApis()).isFalse();
+ }
+
+ @Test
+ void blockAdvancedApmApis_whenEnvVarPresent_true() {
+ System.setProperty(Constants.NO_CPC_PLUS_API_ENV_VARIABLE, "true");
+
+ assertThat(AdvancedApmHelper.blockAdvancedApmApis()).isTrue();
+ }
+}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/model/ReportTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/model/ReportTest.java
index d69f05b39..0f1f4fb96 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/model/ReportTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/model/ReportTest.java
@@ -1,7 +1,13 @@
package gov.cms.qpp.conversion.api.model;
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
import org.junit.jupiter.api.Test;
+import gov.cms.qpp.conversion.model.error.Detail;
import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.Warning;
@@ -15,4 +21,73 @@ void testEqualsContract() {
.verify();
}
+ @Test
+ void gettersAndSetters_basicFields() {
+ Report r = new Report();
+
+ r.setProgramName("MIPS");
+ r.setPracticeSiteId("PSI-1");
+ r.setTimestamp(123L);
+
+ assertThat(r.getProgramName()).isEqualTo("MIPS");
+ assertThat(r.getPracticeSiteId()).isEqualTo("PSI-1");
+ assertThat(r.getTimestamp()).isEqualTo(123L);
+ }
+
+ @Test
+ void setWarnings_copiesInput_and_getWarnings_returnsDefensiveCopy() {
+ Report r = new Report();
+
+ Detail d1 = new Detail();
+ Detail d2 = new Detail();
+
+ List input = new ArrayList<>();
+ input.add(d1);
+
+ r.setWarnings(input);
+
+ input.add(d2);
+ assertThat(r.getWarnings()).containsExactly(d1);
+
+ List returned = r.getWarnings();
+ returned.clear();
+ assertThat(r.getWarnings()).containsExactly(d1);
+ }
+
+ @Test
+ void setErrors_copiesInput_and_getErrors_returnsDefensiveCopy() {
+ Report r = new Report();
+
+ Detail e1 = new Detail();
+ Detail e2 = new Detail();
+
+ List input = new ArrayList<>();
+ input.add(e1);
+
+ r.setErrors(input);
+
+ input.add(e2);
+ assertThat(r.getErrors()).containsExactly(e1);
+
+ List returned = r.getErrors();
+ returned.clear();
+ assertThat(r.getErrors()).containsExactly(e1);
+ }
+
+ @Test
+ void setWarnings_and_setErrors_null_clearsLists() {
+ Report r = new Report();
+
+ r.setWarnings(List.of(new Detail()));
+ r.setErrors(List.of(new Detail(), new Detail()));
+
+ assertThat(r.getWarnings()).isNotEmpty();
+ assertThat(r.getErrors()).isNotEmpty();
+
+ r.setWarnings(null);
+ r.setErrors(null);
+
+ assertThat(r.getWarnings()).isEmpty();
+ assertThat(r.getErrors()).isEmpty();
+ }
}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/security/JwtAuthorizationFilterTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/security/JwtAuthorizationFilterTest.java
index 649a4d327..91027cff5 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/security/JwtAuthorizationFilterTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/security/JwtAuthorizationFilterTest.java
@@ -1,154 +1,138 @@
package gov.cms.qpp.conversion.api.security;
-import com.google.common.truth.Truth;
-import org.junit.Before;
-import org.junit.BeforeClass;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.context.SecurityContext;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
-import gov.cms.qpp.conversion.api.helper.JwtPayloadHelper;
-import gov.cms.qpp.conversion.api.helper.JwtTestHelper;
+import gov.cms.qpp.test.MockitoExtension;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import java.io.IOException;
-import java.util.Set;
+@ExtendWith(MockitoExtension.class)
+class JwtAuthorizationFilterTest {
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
+ @Mock
+ private HttpServletRequest request;
+
+ @Mock
+ private ServletResponse response;
+
+ @Mock
+ private FilterChain chain;
+
+ @AfterEach
+ void cleanupSecurityContext() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ void doFilter_validTokenAndOrg_setsAuthentication_andContinuesChain() throws Exception {
+ JwtAuthorizationFilter filter = new JwtAuthorizationFilter();
+
+ String jwt = jwtWithData(Map.of(
+ "id", "user-123",
+ "name", JwtAuthorizationFilter.DEFAULT_ORG_NAME, // "cpc-test"
+ "orgType", "CPC"
+ ));
+
+ when(request.getHeader("Authorization")).thenReturn("Bearer " + jwt);
+
+ filter.doFilter(request, response, chain);
+
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ assertEquals("user-123", auth.getPrincipal());
+ verify(chain).doFilter(request, response);
+ }
+
+ @Test
+ void doFilter_invalidOrg_doesNotSetAuthentication_andContinuesChain() throws Exception {
+ JwtAuthorizationFilter filter = new JwtAuthorizationFilter();
+
+ String jwt = jwtWithData(Map.of(
+ "id", "user-123",
+ "name", "not-allowed-org",
+ "orgType", "CPC"
+ ));
+
+ when(request.getHeader("Authorization")).thenReturn("Bearer " + jwt);
+
+ filter.doFilter(request, response, chain);
+
+ assertNull(SecurityContextHolder.getContext().getAuthentication());
+ verify(chain).doFilter(request, response);
+ }
+
+ @Test
+ void doFilter_missingHeader_doesNotSetAuthentication_andContinuesChain() throws Exception {
+ JwtAuthorizationFilter filter = new JwtAuthorizationFilter();
+
+ when(request.getHeader("Authorization")).thenReturn(null);
+
+ filter.doFilter(request, response, chain);
+
+ assertNull(SecurityContextHolder.getContext().getAuthentication());
+ verify(chain).doFilter(request, response);
+ }
+
+ @Test
+ void doFilter_nonHttpRequest_doesNotSetAuthentication_andContinuesChain() throws Exception {
+ JwtAuthorizationFilter filter = new JwtAuthorizationFilter();
+
+ ServletRequest nonHttp = org.mockito.Mockito.mock(ServletRequest.class);
+
+ filter.doFilter(nonHttp, response, chain);
+
+ assertNull(SecurityContextHolder.getContext().getAuthentication());
+ verify(chain).doFilter(nonHttp, response);
+ }
+
+ /**
+ * Builds a compact JWT string "header.payload.signature" where header/payload are base64url JSON.
+ * Your filter strips the signature and parses it using parseClaimsJwt("header.payload.").
+ */
+ private static String jwtWithData(Map data) {
+ String headerJson = "{\"alg\":\"none\",\"typ\":\"JWT\"}";
+
+ // payload contains: { "data": { ... } }
+ StringBuilder dataJson = new StringBuilder();
+ dataJson.append("{\"data\":{");
+ boolean first = true;
+ for (Map.Entry e : data.entrySet()) {
+ if (!first) dataJson.append(",");
+ first = false;
+ dataJson.append("\"").append(escape(e.getKey())).append("\":")
+ .append("\"").append(escape(e.getValue())).append("\"");
+ }
+ dataJson.append("}}");
+
+ String header = base64Url(headerJson);
+ String payload = base64Url(dataJson.toString());
+
+ // signature can be anything; filter removes it
+ return header + "." + payload + ".sig";
+ }
+
+ private static String base64Url(String s) {
+ return java.util.Base64.getUrlEncoder()
+ .withoutPadding()
+ .encodeToString(s.getBytes(StandardCharsets.UTF_8));
+ }
-//Using jUnit 4 for power mock
-//@RunWith(PowerMockRunner.class)
-//@PrepareForTest({SecurityContextHolder.class})
-@PowerMockIgnore({"org.apache.xerces.*", "javax.xml.parsers.*", "org.xml.sax.*", "com.sun.org.apache.xerces.*", "javax.crypto.*"})
-
-public class JwtAuthorizationFilterTest {
-
- private static final String ORG_TYPE = "registry";
-
- private MockHttpServletRequest request;
- private MockHttpServletResponse response;
- private FilterChain filterChain;
-
-// @Before
-// public void setUp() {
-// request = new MockHttpServletRequest();
-// response = new MockHttpServletResponse();
-// filterChain = mock(FilterChain.class);
-// }
-//
-// @Test
-// public void testdoFilter() throws IOException, ServletException {
-// JwtPayloadHelper payload = new JwtPayloadHelper()
-// .withName(JwtAuthorizationFilter.DEFAULT_ORG_NAME)
-// .withOrgType(ORG_TYPE);
-//
-// request.addHeader("Authorization", JwtTestHelper.createJwt(payload));
-// JwtAuthorizationFilter testJwtAuthFilter = new JwtAuthorizationFilter();
-//
-// PowerMockito.mockStatic(SecurityContextHolder.class);
-// SecurityContext mockSecurityContext = PowerMockito.mock(SecurityContext.class);
-//
-// PowerMockito.when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext);
-//
-// testJwtAuthFilter.doFilter(request, response, filterChain);
-//
-// verify(filterChain, times(1)).doFilter(any(MockHttpServletRequest.class), any(MockHttpServletResponse.class));
-// verify(SecurityContextHolder.getContext(), times(1)).setAuthentication(any(UsernamePasswordAuthenticationToken.class));
-// }
-//
-// @Test
-// public void testdoFilterWithInvalidOrgName() throws IOException, ServletException {
-// JwtPayloadHelper payload = new JwtPayloadHelper()
-// .withName("invalid-name")
-// .withOrgType(ORG_TYPE);
-//
-// request.addHeader("Authorization", JwtTestHelper.createJwt(payload));
-// JwtAuthorizationFilter testJwtAuthFilter = new JwtAuthorizationFilter();
-//
-// PowerMockito.mockStatic(SecurityContextHolder.class);
-// SecurityContext mockSecurityContext = PowerMockito.mock(SecurityContext.class);
-//
-// PowerMockito.when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext);
-//
-// testJwtAuthFilter.doFilter(request, response, filterChain);
-//
-// verify(filterChain, times(1)).doFilter(any(MockHttpServletRequest.class), any(MockHttpServletResponse.class));
-// verify(SecurityContextHolder.getContext(), times(0)).setAuthentication(any(UsernamePasswordAuthenticationToken.class));
-// }
-//
-// @Test
-// public void testdoFilterWithNoOrgId() throws IOException, ServletException {
-// JwtPayloadHelper payload = new JwtPayloadHelper()
-// .withOrgType(ORG_TYPE);
-//
-// request.addHeader("Authorization", JwtTestHelper.createJwt(payload));
-// JwtAuthorizationFilter testJwtAuthFilter = new JwtAuthorizationFilter();
-//
-// PowerMockito.mockStatic(SecurityContextHolder.class);
-// SecurityContext mockSecurityContext = PowerMockito.mock(SecurityContext.class);
-//
-// PowerMockito.when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext);
-//
-// testJwtAuthFilter.doFilter(request, response, filterChain);
-//
-// verify(filterChain, times(1)).doFilter(any(MockHttpServletRequest.class), any(MockHttpServletResponse.class));
-// verify(SecurityContextHolder.getContext(), times(0)).setAuthentication(any(UsernamePasswordAuthenticationToken.class));
-// }
-//
-// @Test
-// public void testdoFilterWithNoOrgType() throws IOException, ServletException {
-// JwtPayloadHelper payload = new JwtPayloadHelper()
-// .withName(JwtAuthorizationFilter.DEFAULT_ORG_NAME);
-//
-// request.addHeader("Authorization", JwtTestHelper.createJwt(payload));
-// JwtAuthorizationFilter testJwtAuthFilter = new JwtAuthorizationFilter();
-//
-// PowerMockito.mockStatic(SecurityContextHolder.class);
-// SecurityContext mockSecurityContext = PowerMockito.mock(SecurityContext.class);
-//
-// PowerMockito.when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext);
-//
-// testJwtAuthFilter.doFilter(request, response, filterChain);
-//
-// verify(filterChain, times(1)).doFilter(any(MockHttpServletRequest.class), any(MockHttpServletResponse.class));
-// verify(SecurityContextHolder.getContext(), times(0)).setAuthentication(any(UsernamePasswordAuthenticationToken.class));
-// }
-//
-// @Test
-// public void testdoFilterWithNoHeader() throws IOException, ServletException {
-// JwtAuthorizationFilter testJwtAuthFilter = new JwtAuthorizationFilter();
-//
-// PowerMockito.mockStatic(SecurityContextHolder.class);
-// SecurityContext mockSecurityContext = PowerMockito.mock(SecurityContext.class);
-//
-// PowerMockito.when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext);
-//
-// testJwtAuthFilter.doFilter(request, response, filterChain);
-//
-// verify(filterChain, times(1)).doFilter(any(MockHttpServletRequest.class), any(MockHttpServletResponse.class));
-// verify(SecurityContextHolder.getContext(), times(0)).setAuthentication(any(UsernamePasswordAuthenticationToken.class));
-// }
-//
-// @Test
-// public void testDefaultOrgName() {
-// JwtAuthorizationFilter testJwtAuthFilter = new JwtAuthorizationFilter(JwtAuthorizationFilter.DEFAULT_ORG_SET);
-// Truth.assertThat(testJwtAuthFilter.orgName).contains(JwtAuthorizationFilter.DEFAULT_ORG_NAME);
-// }
-//
-// @Test
-// public void testGivenOrgName() {
-// Set expected = Set.of("some org name");
-// JwtAuthorizationFilter testJwtAuthFilter = new JwtAuthorizationFilter(expected);
-// Truth.assertThat(testJwtAuthFilter.orgName).isEqualTo(expected);
-// }
+ private static String escape(String s) {
+ return s.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/AdvancedApmFileServiceImplTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/AdvancedApmFileServiceImplTest.java
new file mode 100644
index 000000000..3ded26334
--- /dev/null
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/AdvancedApmFileServiceImplTest.java
@@ -0,0 +1,118 @@
+package gov.cms.qpp.conversion.api.services.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import gov.cms.qpp.conversion.api.exceptions.InvalidFileTypeException;
+import gov.cms.qpp.conversion.api.exceptions.NoFileInDatabaseException;
+import gov.cms.qpp.conversion.api.helper.AdvancedApmHelper;
+import gov.cms.qpp.conversion.api.model.Constants;
+import gov.cms.qpp.conversion.api.model.FileStatusUpdateRequest;
+import gov.cms.qpp.conversion.api.model.Metadata;
+import gov.cms.qpp.conversion.api.services.DbService;
+import gov.cms.qpp.conversion.api.services.StorageService;
+import gov.cms.qpp.test.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class AdvancedApmFileServiceImplTest {
+
+ @Mock
+ private DbService dbService;
+
+ @Mock
+ private StorageService storageService;
+
+ @InjectMocks
+ private AdvancedApmFileServiceImpl objectUnderTest;
+
+ @Test
+ void getMetadataById_nullMetadata_throwsNoFileInDatabase() {
+ when(dbService.getMetadataById("id1")).thenReturn(null);
+
+ assertThrows(NoFileInDatabaseException.class, () -> objectUnderTest.getMetadataById("id1"));
+ }
+
+ @Test
+ void getMetadataById_notPcf_throwsInvalidFileType() {
+ Metadata metadata = new Metadata();
+
+ when(dbService.getMetadataById("id1")).thenReturn(metadata);
+
+ assertThrows(InvalidFileTypeException.class, () -> objectUnderTest.getMetadataById("id1"));
+ }
+
+ @Test
+ void getMetadataById_validPcf_returnsMetadata() {
+ Metadata metadata = new Metadata();
+ metadata.setPcf("PCF_0");
+
+ when(dbService.getMetadataById("id1")).thenReturn(metadata);
+
+ Metadata result = objectUnderTest.getMetadataById("id1");
+
+ assertThat(result).isSameInstanceAs(metadata);
+ }
+
+ @Test
+ void updateFileStatus_processedFalse_unprocessesForCpc() {
+ Metadata metadata = new Metadata();
+ metadata.setPcf("PCF_0");
+
+ when(dbService.getMetadataById("file1")).thenReturn(metadata);
+ when(dbService.write(any(Metadata.class))).thenReturn(CompletableFuture.completedFuture(metadata));
+
+ FileStatusUpdateRequest req = new FileStatusUpdateRequest();
+ req.setProcessed(false);
+
+ String msg = objectUnderTest.updateFileStatus("file1", Constants.CPC_ORG, req);
+
+ assertThat(metadata.getCpcProcessed()).isFalse();
+ assertThat(msg).isEqualTo(AdvancedApmHelper.FILE_FOUND_UNPROCESSED);
+ verify(dbService).write(any(Metadata.class));
+ }
+
+ @Test
+ void updateFileStatus_processedTrue_processesForRti() {
+ Metadata metadata = new Metadata();
+ metadata.setPcf("PCF_0");
+
+ when(dbService.getMetadataById("file1")).thenReturn(metadata);
+ when(dbService.write(any(Metadata.class))).thenReturn(CompletableFuture.completedFuture(metadata));
+
+ FileStatusUpdateRequest req = new FileStatusUpdateRequest();
+ req.setProcessed(true);
+
+ String msg = objectUnderTest.updateFileStatus("file1", Constants.RTI_ORG, req);
+
+ assertThat(metadata.getRtiProcessed()).isTrue();
+ assertThat(msg).isEqualTo(AdvancedApmHelper.FILE_FOUND_PROCESSED);
+ verify(dbService).write(any(Metadata.class));
+ }
+
+ @Test
+ void updateFileStatus_unknownOrg_returnsNotFound_andDoesNotWrite() {
+ Metadata metadata = new Metadata();
+ metadata.setPcf("PCF_0");
+
+ when(dbService.getMetadataById("file1")).thenReturn(metadata);
+
+ FileStatusUpdateRequest req = new FileStatusUpdateRequest();
+ req.setProcessed(true);
+
+ String msg = objectUnderTest.updateFileStatus("file1", "SOME_OTHER_ORG", req);
+
+ assertThat(msg).isEqualTo(AdvancedApmHelper.FILE_NOT_FOUND);
+ verify(dbService, never()).write(any(Metadata.class));
+ }
+}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/AuditServiceImplTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/AuditServiceImplTest.java
index 6f3e2cd75..d99acc020 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/AuditServiceImplTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/AuditServiceImplTest.java
@@ -1,220 +1,188 @@
package gov.cms.qpp.conversion.api.services.internal;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import org.springframework.core.env.Environment;
+
import gov.cms.qpp.conversion.ConversionReport;
-import gov.cms.qpp.conversion.InputStreamSupplierSource;
import gov.cms.qpp.conversion.Source;
-import gov.cms.qpp.conversion.api.exceptions.UncheckedInterruptedException;
-import gov.cms.qpp.conversion.api.helper.MetadataHelper;
+import gov.cms.qpp.conversion.api.exceptions.AuditException;
import gov.cms.qpp.conversion.api.model.Constants;
import gov.cms.qpp.conversion.api.model.Metadata;
import gov.cms.qpp.conversion.api.services.DbService;
import gov.cms.qpp.conversion.api.services.StorageService;
-import gov.cms.qpp.conversion.encode.JsonWrapper;
-import gov.cms.qpp.conversion.model.Node;
-import net.jodah.concurrentunit.Waiter;
-import org.junit.Before;
-import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.springframework.core.env.Environment;
+import gov.cms.qpp.conversion.model.error.AllErrors;
+import gov.cms.qpp.conversion.model.error.Detail;
+import gov.cms.qpp.conversion.model.error.Error;
+import gov.cms.qpp.test.MockitoExtension;
-import java.io.ByteArrayInputStream;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeoutException;
+@ExtendWith(MockitoExtension.class)
+class AuditServiceImplTest {
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-
-@PowerMockIgnore({"org.apache.xerces.*", "javax.xml.parsers.*", "org.xml.sax.*", "com.sun.org.apache.xerces.*" })
-public class AuditServiceImplTest {
-// private static final String AN_ID = "1234567890";
-// private static final String FILENAME = "file";
-//
-// @InjectMocks
-// private AuditServiceImpl underTest;
-//
-// @Mock
-// private StorageService storageService;
-//
-// @Mock
-// private DbService dbService;
-//
-// @Mock
-// private ConversionReport report;
-//
-// @Mock
-// private Environment environment;
-//
-// private Metadata metadata;
-// private String content = "Hello";
-// private Source fileContentSource = new InputStreamSupplierSource(FILENAME, new ByteArrayInputStream(content.getBytes()));
-//
-// @Before
-// public void before() {
-// metadata = Metadata.create();
-//
-// mockStatic(MetadataHelper.class);
-// when(MetadataHelper.generateMetadata(any(Node.class), any(MetadataHelper.Outcome.class)))
-// .thenReturn(metadata);
-// doReturn(CompletableFuture.completedFuture(metadata))
-// .when(dbService).write(metadata);
-// }
-//
-// @Test
-// public void testAuditHappyPath() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn(null);
-// successfulEncodingPrep();
-// allGood();
-// underTest.success(report);
-//
-// assertThat(metadata.getQppLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getSubmissionLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getFileName()).isSameInstanceAs(FILENAME);
-// }
-//
-// @Test
-// public void testAuditHappyPathNoAuditIsEmpty() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn("");
-// successfulEncodingPrep();
-// allGood();
-// underTest.success(report);
-//
-// assertThat(metadata.getQppLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getSubmissionLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getFileName()).isSameInstanceAs(FILENAME);
-// }
-//
-// @Test
-// public void testAuditHappyPathNoAudit() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn("yep");
-// successfulEncodingPrep();
-// allGood();
-// underTest.success(report);
-//
-// verify(storageService, times(0)).store(any(String.class), any(), anyLong());
-// verify(dbService, times(0)).write(metadata);
-// }
-//
-// @Test
-// public void testAuditHappyPathWrite() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn(null);
-// successfulEncodingPrep();
-// allGood();
-// underTest.success(report);
-//
-// verify(dbService, times(1)).write(metadata);
-// }
-//
-// @Test
-// public void testFileUploadFailureException() throws TimeoutException, InterruptedException {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn(null);
-// successfulEncodingPrep();
-// problematic();
-// Waiter waiter = new Waiter();
-// CompletableFuture future = underTest.success(report);
-//
-// future.whenComplete((nada, ex) -> {
-// waiter.assertNull(metadata.getQppLocator());
-// waiter.assertNull(metadata.getSubmissionLocator());
-// waiter.assertTrue(ex.getCause() instanceof UncheckedInterruptedException);
-// waiter.resume();
-// });
-//
-// waiter.await(5000);
-// }
-//
-//
-// @Test
-// public void testAuditConversionFailureHappy() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn(null);
-// errorPrep();
-// allGood();
-// underTest.failConversion(report);
-//
-// assertThat(metadata.getConversionErrorLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getSubmissionLocator()).isSameInstanceAs(AN_ID);
-// }
-//
-// @Test
-// public void testAuditConversionFailureNoAudit() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn("yep");
-// errorPrep();
-// allGood();
-// underTest.failConversion(report);
-//
-// verify(storageService, times(0)).store(any(String.class), any(), anyLong());
-// verify(dbService, times(0)).write(metadata);
-// }
-//
-// @Test
-// public void testAuditQppValidationFailureHappy() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn(null);
-// successfulEncodingPrep();
-// errorPrep();
-// allGood();
-// underTest.failValidation(report);
-//
-// assertThat(metadata.getRawValidationErrorLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getValidationErrorLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getQppLocator()).isSameInstanceAs(AN_ID);
-// assertThat(metadata.getSubmissionLocator()).isSameInstanceAs(AN_ID);
-// }
-//
-// @Test
-// public void testAuditQppValidationFailureNoAudit() {
-// when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn("yep");
-// successfulEncodingPrep();
-// errorPrep();
-// allGood();
-// underTest.failValidation(report);
-//
-// verify(storageService, times(0)).store(any(String.class), any(), anyLong());
-// verify(dbService, times(0)).write(metadata);
-// }
-//
-// private void successfulEncodingPrep() {
-// prepOverlap();
-// JsonWrapper wrapper = new JsonWrapper();
-// wrapper.put("meep", "mawp");
-//
-// when(report.getQppSource()).thenReturn(wrapper.toSource());
-// }
-//
-// private void errorPrep() {
-// prepOverlap();
-//
-//
-// when(report.getRawValidationErrorsOrEmptySource()).thenReturn(fileContentSource);
-// when(report.getValidationErrorsSource()).thenReturn(fileContentSource);
-// }
-//
-// private void prepOverlap() {
-// Node node = new Node();
-// node.putValue("meep", "mawp");
-//
-// when(report.getQrdaSource()).thenReturn(fileContentSource);
-// when(report.getDecoded()).thenReturn(node);
-// }
-//
-// private void allGood() {
-// when(storageService.store(any(String.class), any(), anyLong()))
-// .thenReturn(CompletableFuture.completedFuture(AN_ID));
-// }
-//
-// private void problematic() {
-// when(storageService.store(any(String.class), any(), anyLong()))
-// .thenReturn(CompletableFuture.supplyAsync(() -> {
-// throw new UncheckedInterruptedException(new InterruptedException());
-// }));
-// }
+ @Mock private StorageService storageService;
+ @Mock private DbService dbService;
+ @Mock private Environment environment;
-}
+ @Mock private ConversionReport report;
+
+ @Mock private Source qrdaSource;
+ @Mock private Source qppSource;
+ @Mock private Source validationErrorSource;
+ @Mock private Source rawValidationErrorSource;
+
+ private AuditServiceImpl objectUnderTest;
+
+ @BeforeEach
+ void setUp() {
+ objectUnderTest = new AuditServiceImpl(storageService, dbService, environment);
+
+ // Default: auditing enabled
+ when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn(null);
+
+ // Keep MetadataHelper.generateMetadata(report.getDecoded(), outcome) simple (node = null)
+ when(report.getDecoded()).thenReturn(null);
+
+ when(report.getPurpose()).thenReturn("TestPurpose");
+
+ when(report.getQrdaSource()).thenReturn(qrdaSource);
+ when(report.getQppSource()).thenReturn(qppSource);
+ when(report.getValidationErrorsSource()).thenReturn(validationErrorSource);
+ when(report.getRawValidationErrorsOrEmptySource()).thenReturn(rawValidationErrorSource);
+
+ when(report.getReportDetails()).thenReturn(null);
+ when(qrdaSource.getName()).thenReturn("qrda.xml");
+ // sizes are required by storeContent(...)
+ when(qrdaSource.getSize()).thenReturn(10L);
+ when(qppSource.getSize()).thenReturn(20L);
+ when(validationErrorSource.getSize()).thenReturn(30L);
+ when(rawValidationErrorSource.getSize()).thenReturn(40L);
+
+ // dbService.write returns whatever metadata it was asked to save
+ when(dbService.write(any(Metadata.class)))
+ .thenAnswer(inv -> CompletableFuture.completedFuture(inv.getArgument(0)));
+ }
+
+ @Test
+ void success_noAuditEnabled_returnsNull_andDoesNothing() {
+ when(environment.getProperty(Constants.NO_AUDIT_ENV_VARIABLE)).thenReturn("true");
+
+ assertThat(objectUnderTest.success(report)).isNull();
+
+ verifyNoInteractions(storageService);
+ verifyNoInteractions(dbService);
+ }
+
+ @Test
+ void success_happyPath_setsLocators_andWritesMetadata() {
+ when(storageService.store(anyString(), anySupplier(), anyLong()))
+ .thenReturn(
+ CompletableFuture.completedFuture("submission-loc"),
+ CompletableFuture.completedFuture("qpp-loc")
+ );
+
+ Metadata result = objectUnderTest.success(report).join();
+
+ assertThat(result.getSubmissionLocator()).isEqualTo("submission-loc");
+ assertThat(result.getQppLocator()).isEqualTo("qpp-loc");
+ assertThat(result.getFileName()).isEqualTo("qrda.xml");
+ assertThat(result.getPurpose()).isEqualTo("TestPurpose");
+
+ verify(storageService, times(2)).store(anyString(), anySupplier(), anyLong());
+ verify(dbService, times(1)).write(any(Metadata.class));
+ }
+
+ @Test
+ void failConversion_happyPath_setsConversionErrorLocator_andSubmissionLocator() {
+ when(storageService.store(anyString(), anySupplier(), anyLong()))
+ .thenReturn(
+ CompletableFuture.completedFuture("conversion-error-loc"),
+ CompletableFuture.completedFuture("submission-loc")
+ );
+
+ CompletableFuture future = objectUnderTest.failConversion(report);
+ future.join();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Metadata.class);
+ verify(dbService).write(captor.capture());
+
+ Metadata saved = captor.getValue();
+ assertThat(saved.getConversionErrorLocator()).isEqualTo("conversion-error-loc");
+ assertThat(saved.getSubmissionLocator()).isEqualTo("submission-loc");
+ assertThat(saved.getFileName()).isEqualTo("qrda.xml");
+ assertThat(saved.getPurpose()).isEqualTo("TestPurpose");
+ }
+
+ @Test
+ void failValidation_happyPath_setsAllLocators() {
+ when(storageService.store(anyString(), anySupplier(), anyLong()))
+ .thenReturn(
+ CompletableFuture.completedFuture("raw-validation-loc"),
+ CompletableFuture.completedFuture("validation-loc"),
+ CompletableFuture.completedFuture("qpp-loc"),
+ CompletableFuture.completedFuture("submission-loc")
+ );
+
+ CompletableFuture future = objectUnderTest.failValidation(report);
+ future.join();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Metadata.class);
+ verify(dbService).write(captor.capture());
+
+ Metadata saved = captor.getValue();
+ assertThat(saved.getRawValidationErrorLocator()).isEqualTo("raw-validation-loc");
+ assertThat(saved.getValidationErrorLocator()).isEqualTo("validation-loc");
+ assertThat(saved.getQppLocator()).isEqualTo("qpp-loc");
+ assertThat(saved.getSubmissionLocator()).isEqualTo("submission-loc");
+ }
+
+ @Test
+ void success_whenStoreFails_joinThrowsCompletionException_withRuntimeCause_andDoesNotWrite() {
+ CompletableFuture failed = new CompletableFuture<>();
+ failed.completeExceptionally(new RuntimeException("boom"));
+
+ when(storageService.store(anyString(), anySupplier(), anyLong()))
+ .thenReturn(
+ failed,
+ CompletableFuture.completedFuture("qpp-loc")
+ );
+
+ CompletionException ex = assertThrows(
+ CompletionException.class,
+ () -> objectUnderTest.success(report).join()
+ );
+
+ // Primary cause is the original store failure (matches what you observed)
+ assertThat(ex.getCause()).isInstanceOf(RuntimeException.class);
+ assertThat(ex.getCause()).hasMessageThat().contains("boom");
+
+ // persist() threw AuditException, so db write should not happen
+ verify(dbService, never()).write(any(Metadata.class));
+
+ // Optional (donβt make the test brittle): AuditException may appear as suppressed depending on JDK behavior.
+ // If you want to assert it, uncomment:
+ // assertThat(java.util.Arrays.stream(ex.getSuppressed()).anyMatch(t -> t instanceof AuditException)).isTrue();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Supplier anySupplier() {
+ return (Supplier) org.mockito.ArgumentMatchers.any(Supplier.class);
+ }
+}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/QrdaServiceImplTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/QrdaServiceImplTest.java
index f1d38c2dc..97ba91707 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/QrdaServiceImplTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/QrdaServiceImplTest.java
@@ -1,12 +1,14 @@
package gov.cms.qpp.conversion.api.services.internal;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.Spy;
import gov.cms.qpp.conversion.ConversionReport;
import gov.cms.qpp.conversion.Converter;
@@ -20,108 +22,111 @@
import gov.cms.qpp.conversion.model.error.TransformException;
import gov.cms.qpp.test.MockitoExtension;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
@ExtendWith(MockitoExtension.class)
class QrdaServiceImplTest {
- private static final Source MOCK_SUCCESS_QRDA_SOURCE =
- new InputStreamSupplierSource("Good Qrda", new ByteArrayInputStream("Good Qrda".getBytes()));
- private static final Source MOCK_ERROR_QRDA_SOURCE =
- new InputStreamSupplierSource("Error Qrda", new ByteArrayInputStream("Error Qrda".getBytes()));
- private static final String KEY = "key";
- private static final String MOCK_SUCCESS_QPP_STRING = "Good Qpp";
- private static final String MOCK_ERROR_SOURCE_IDENTIFIER = "Error Identifier";
- private static final Path VALIDATION_JSON_FILE_PATH = Path.of("src/test/resources/testCpcPlusValidationFile.json");
- private static final Path VALIDATION_APM_FILE_PATH = Path.of("src/test/resources/test_apm_entity_ids.json");
- private InputStream MOCK_INPUT_STREAM;
- private InputStream MOCK_APM_INPUT_STREAM;
-
- @Spy
- @InjectMocks
- private QrdaServiceImpl objectUnderTest;
-
- @Mock
- private StorageService storageService;
-
- @BeforeEach
- void mockConverter() throws IOException {
- MOCK_INPUT_STREAM = Files.newInputStream(VALIDATION_JSON_FILE_PATH);
- MOCK_APM_INPUT_STREAM = Files.newInputStream(VALIDATION_APM_FILE_PATH);
- Converter success = successConverter();
- when(objectUnderTest.initConverter(MOCK_SUCCESS_QRDA_SOURCE))
- .thenReturn(success);
-
- when(objectUnderTest.retrieveCpcPlusValidationFile())
- .thenReturn(MOCK_INPUT_STREAM);
-
- when(objectUnderTest.retrieveApmValidationFile(Constants.CPC_PLUS_APM_FILE_NAME_KEY))
- .thenReturn(MOCK_APM_INPUT_STREAM);
-
- Converter error = errorConverter();
- when(objectUnderTest.initConverter(MOCK_ERROR_QRDA_SOURCE))
- .thenReturn(error);
- }
+ private static final Source GOOD_SOURCE =
+ new InputStreamSupplierSource("Good Qrda", new ByteArrayInputStream("".getBytes()));
+ private static final Source ERROR_SOURCE =
+ new InputStreamSupplierSource("Error Qrda", new ByteArrayInputStream("".getBytes()));
- @AfterEach
- void tearDown() throws IOException {
- MOCK_APM_INPUT_STREAM.close();
- MOCK_APM_INPUT_STREAM.close();
- }
+ private static final String KEY = "key";
+ private static final String GOOD_QPP = "Good Qpp";
@Test
- void testConvertQrda3ToQppSuccess() {
- JsonWrapper qpp = objectUnderTest.convertQrda3ToQpp(MOCK_SUCCESS_QRDA_SOURCE).getEncodedWithMetadata();
- assertThat(qpp.getString(KEY)).isSameInstanceAs(MOCK_SUCCESS_QPP_STRING);
- }
+ void convertQrda3ToQpp_success_callsTransform_andReturnsReport() {
+ StorageService storage = mock(StorageService.class);
+ QrdaServiceImpl service = spy(new QrdaServiceImpl(storage));
-// @Test
-// void testConvertQrda3ToQppError() {
-// TransformException exception = assertThrows(TransformException.class,
-// () -> objectUnderTest.convertQrda3ToQpp(MOCK_ERROR_QRDA_SOURCE));
-// AllErrors allErrors = exception.getDetails();
-// assertThat(allErrors.getErrors().get(0).getSourceIdentifier()).isSameInstanceAs(MOCK_ERROR_SOURCE_IDENTIFIER);
-// }
+ Converter converter = mock(Converter.class);
+ ConversionReport report = mock(ConversionReport.class);
- @Test
- void testPostConstructForCoverage() {
- objectUnderTest.preloadMeasureConfigs();
- }
+ JsonWrapper wrapper = new JsonWrapper();
+ wrapper.put(KEY, GOOD_QPP);
- private Converter successConverter() {
- Converter mockConverter = mock(Converter.class);
+ when(service.initConverter(GOOD_SOURCE)).thenReturn(converter);
- JsonWrapper qpp = new JsonWrapper();
- qpp.put(KEY, MOCK_SUCCESS_QPP_STRING);
+ when(converter.transform()).thenReturn(wrapper);
- ConversionReport report = mock(ConversionReport.class);
+ when(converter.getReport()).thenReturn(report);
+ when(report.getEncodedWithMetadata()).thenReturn(wrapper);
- when(report.getEncodedWithMetadata()).thenReturn(qpp);
- when(mockConverter.getReport()).thenReturn(report);
+ ConversionReport result = service.convertQrda3ToQpp(GOOD_SOURCE);
- return mockConverter;
+ verify(converter).transform();
+ verify(converter).getReport();
+ assertThat(result.getEncodedWithMetadata().getString(KEY)).isEqualTo(GOOD_QPP);
}
- private Converter errorConverter() {
- Converter mockConverter = mock(Converter.class);
+ @Test
+ void convertQrda3ToQpp_whenTransformThrows_propagatesTransformException() {
+ StorageService storage = mock(StorageService.class);
+ QrdaServiceImpl service = spy(new QrdaServiceImpl(storage));
+
+ Converter converter = mock(Converter.class);
+
AllErrors allErrors = new AllErrors();
- allErrors.addError(new Error(MOCK_ERROR_SOURCE_IDENTIFIER, null));
+ allErrors.addError(new Error("Error Identifier", null));
ConversionReport report = mock(ConversionReport.class);
when(report.getReportDetails()).thenReturn(allErrors);
- TransformException transformException = new TransformException("mock problem", new NullPointerException(), report);
- when(mockConverter.transform()).thenThrow(transformException);
+ TransformException boom = new TransformException("mock problem", new NullPointerException(), report);
+
+ when(service.initConverter(ERROR_SOURCE)).thenReturn(converter);
+ when(converter.transform()).thenThrow(boom);
+
+ assertThrows(TransformException.class, () -> service.convertQrda3ToQpp(ERROR_SOURCE));
+ }
+
+ @Test
+ void retrieveCpcPlusValidationFile_delegatesToStorageService() {
+ StorageService storage = mock(StorageService.class);
+ QrdaServiceImpl service = new QrdaServiceImpl(storage);
+
+ InputStream expected = new ByteArrayInputStream("x".getBytes());
+ when(storage.getCpcPlusValidationFile()).thenReturn(expected);
+
+ InputStream actual = service.retrieveCpcPlusValidationFile();
+
+ assertThat(actual).isSameInstanceAs(expected);
+ verify(storage).getCpcPlusValidationFile();
+ }
+
+ @Test
+ void retrieveApmValidationFile_delegatesToStorageService() {
+ StorageService storage = mock(StorageService.class);
+ QrdaServiceImpl service = new QrdaServiceImpl(storage);
+
+ InputStream expected = new ByteArrayInputStream("{}".getBytes());
+ when(storage.getApmValidationFile("file.json")).thenReturn(expected);
+
+ InputStream actual = service.retrieveApmValidationFile("file.json");
+
+ assertThat(actual).isSameInstanceAs(expected);
+ verify(storage).getApmValidationFile("file.json");
+ }
+
+ @Test
+ void loadApmData_thenInitConverter_fetchesApmFile_once_dueToMemoization() {
+ StorageService storage = mock(StorageService.class);
+ QrdaServiceImpl service = new QrdaServiceImpl(storage);
+
+ when(storage.getApmValidationFile(Constants.PCF_APM_FILE_NAME_KEY)).thenReturn(null);
+
+ service.loadApmData();
+
+ assertThat(service.initConverter(GOOD_SOURCE)).isNotNull();
+ assertThat(service.initConverter(GOOD_SOURCE)).isNotNull();
+
+ verify(storage, times(1)).getApmValidationFile(Constants.PCF_APM_FILE_NAME_KEY);
+ }
+
+ @Test
+ void preloadMeasureConfigs_forCoverage() {
+ StorageService storage = mock(StorageService.class);
+ QrdaServiceImpl service = new QrdaServiceImpl(storage);
- return mockConverter;
+ service.preloadMeasureConfigs();
}
}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImplTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImplTest.java
index 0a0b8d51a..367a96208 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImplTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/StorageServiceImplTest.java
@@ -6,13 +6,18 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
@@ -144,7 +149,8 @@ void noBucket() {
@Test
void envVariablesPresent() {
S3Object s3ObjectMock = mock(S3Object.class);
- s3ObjectMock.setObjectContent(new ByteArrayInputStream("1234".getBytes()));
+ S3ObjectInputStream objectContent = new S3ObjectInputStream(new ByteArrayInputStream("1234".getBytes(StandardCharsets.UTF_8)), null);
+ when(s3ObjectMock.getObjectContent()).thenReturn(objectContent);
Mockito.when(amazonS3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectMock);
Mockito.when(environment.getProperty(Constants.BUCKET_NAME_ENV_VARIABLE)).thenReturn("meep");
underTest.getFileByLocationId("meep");
@@ -197,31 +203,86 @@ void test_getCpcPlusValidationFile_NPE() {
}
@Test
- void test_getCpcPlusValidationFile() {
- S3ObjectInputStream expected = new S3ObjectInputStream(null, null);
+ void test_getCpcPlusValidationFile() throws IOException {
+ byte[] expectedBytes = "Mock Contents".getBytes(StandardCharsets.UTF_8);
+ S3ObjectInputStream expectedStream = new S3ObjectInputStream(new ByteArrayInputStream(expectedBytes), null);
S3Object mockS3Obj = mock(S3Object.class);
- Mockito.when(mockS3Obj.getObjectContent()).thenReturn(expected);
+ Mockito.when(mockS3Obj.getObjectContent()).thenReturn(expectedStream);
Mockito.when(environment.getProperty(Constants.CPC_PLUS_BUCKET_NAME_VARIABLE)).thenReturn("Mock_Bucket");
Mockito.when(environment.getProperty(Constants.CPC_PLUS_FILENAME_VARIABLE)).thenReturn("Mock_Filename");
- Mockito.when(amazonS3Client.getObject( any(GetObjectRequest.class) )).thenReturn(mockS3Obj);
+ Mockito.when(amazonS3Client.getObject(any(GetObjectRequest.class))).thenReturn(mockS3Obj);
- InputStream actual = underTest.getCpcPlusValidationFile();
+ byte[] actualBytes;
+ try (InputStream actual = underTest.getCpcPlusValidationFile()) {
+ assertThat(actual).isNotNull();
+ actualBytes = toByteArray(actual);
+ }
- assertThat(actual).isEqualTo(expected);
+ assertThat(actualBytes).isEqualTo(expectedBytes);
+ verify(mockS3Obj, times(1)).close();
}
@Test
- void test_getApmValidationFile() {
- S3ObjectInputStream expected = new S3ObjectInputStream(null, null);
+ void managedInputStream_closeAddsSuppressedWhenSuperAndS3CloseFail() throws IOException {
+ byte[] bytes = "Mock Contents".getBytes(StandardCharsets.UTF_8);
+ S3ObjectInputStream expectedStream = new S3ObjectInputStream(new CloseFailingInputStream(bytes), null);
S3Object mockS3Obj = mock(S3Object.class);
- Mockito.when(mockS3Obj.getObjectContent()).thenReturn(expected);
+ Mockito.when(mockS3Obj.getObjectContent()).thenReturn(expectedStream);
+ doThrow(new IOException("S3 close failure")).when(mockS3Obj).close();
+
+ Mockito.when(environment.getProperty(Constants.CPC_PLUS_BUCKET_NAME_VARIABLE)).thenReturn("Mock_Bucket");
+ Mockito.when(environment.getProperty(Constants.CPC_PLUS_FILENAME_VARIABLE)).thenReturn("Mock_Filename");
+ Mockito.when(amazonS3Client.getObject(any(GetObjectRequest.class))).thenReturn(mockS3Obj);
+
+ InputStream managedStream = underTest.getCpcPlusValidationFile();
+ assertThat(managedStream).isNotNull();
+ IOException thrown = assertThrows(IOException.class, managedStream::close);
+
+ assertThat(thrown.getMessage()).contains("Input stream close failure");
+ assertThat(thrown.getSuppressed()).hasLength(1);
+ assertThat(thrown.getSuppressed()[0].getMessage()).contains("S3 close failure");
+ verify(mockS3Obj, times(1)).close();
+ }
+
+ @Test
+ void test_getApmValidationFile() throws IOException {
+ byte[] expectedBytes = "APM".getBytes(StandardCharsets.UTF_8);
+ S3ObjectInputStream expectedStream = new S3ObjectInputStream(new ByteArrayInputStream(expectedBytes), null);
+ S3Object mockS3Obj = mock(S3Object.class);
+ Mockito.when(mockS3Obj.getObjectContent()).thenReturn(expectedStream);
Mockito.when(environment.getProperty(Constants.BUCKET_NAME_ENV_VARIABLE)).thenReturn("Mock_Bucket");
- Mockito.when(amazonS3Client.getObject( any(GetObjectRequest.class) )).thenReturn(mockS3Obj);
+ Mockito.when(amazonS3Client.getObject(any(GetObjectRequest.class))).thenReturn(mockS3Obj);
+
+ byte[] actualBytes;
+ try (InputStream actual = underTest.getApmValidationFile(Constants.CPC_PLUS_APM_FILE_NAME_KEY)) {
+ assertThat(actual).isNotNull();
+ actualBytes = toByteArray(actual);
+ }
+
+ assertThat(actualBytes).isEqualTo(expectedBytes);
+ verify(mockS3Obj, times(1)).close();
+ }
+
+ private byte[] toByteArray(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] data = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(data)) != -1) {
+ buffer.write(data, 0, bytesRead);
+ }
+ return buffer.toByteArray();
+ }
- InputStream actual = underTest.getApmValidationFile(Constants.CPC_PLUS_APM_FILE_NAME_KEY);
+ private static final class CloseFailingInputStream extends FilterInputStream {
+ CloseFailingInputStream(byte[] data) {
+ super(new ByteArrayInputStream(data));
+ }
- assertThat(actual).isEqualTo(expected);
+ @Override
+ public void close() throws IOException {
+ throw new IOException("Input stream close failure");
+ }
}
}
diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/ValidationServiceImplTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/ValidationServiceImplTest.java
index 03819c427..7db413a33 100644
--- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/ValidationServiceImplTest.java
+++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/internal/ValidationServiceImplTest.java
@@ -217,14 +217,21 @@ void testCheckForValidationUrlVariableLoggingIfAbsent() {
Mockito.verify(objectUnderTest, Mockito.times(1)).apiLog(Constants.VALIDATION_URL_ENV_VARIABLE + " is unset");
}
-// @Test
-// void testInvalidSubmissionResponseJsonPath() throws IOException {
-// pathToSubmissionError = Path.of("src/test/resources/invalidSubmissionErrorFixture.json");
-// String errorJson = FileUtils.readFileToString(pathToSubmissionError.toFile(), StandardCharsets.UTF_8);
-// convertedErrors = service.convertQppValidationErrorsToQrda(errorJson, qppWrapper);
-//
-// convertedErrors.getErrors().stream().flatMap(error -> error.getDetails().stream())
-// .map(Detail::getLocation).map(Location::getPath)
-// .forEach(path -> assertThat(path, is(ValidationServiceImpl.UNABLE_PROVIDE_XPATH)));
-// }
+ @Test
+ void testNoHandlingErrorHandlerDoesNothing() throws IOException {
+ try {
+ Class> innerClass = Class.forName("gov.cms.qpp.conversion.api.services.internal.ValidationServiceImpl$NoHandlingErrorHandler");
+ java.lang.reflect.Constructor> constructor = innerClass.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ Object errorHandler = constructor.newInstance();
+
+ java.lang.reflect.Method method = innerClass.getDeclaredMethod("handleError", org.springframework.http.client.ClientHttpResponse.class);
+ method.setAccessible(true);
+
+ // This should not throw an exception
+ method.invoke(errorHandler, mock(org.springframework.http.client.ClientHttpResponse.class));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to test private NoHandlingErrorHandler", e);
+ }
+ }
}
diff --git a/test-commons/pom.xml b/test-commons/pom.xml
index a64aea6db..58463b598 100644
--- a/test-commons/pom.xml
+++ b/test-commons/pom.xml
@@ -6,7 +6,7 @@
gov.cms.qpp.conversion
qpp-conversion-tool-parent
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
../pom.xml
diff --git a/test-coverage/pom.xml b/test-coverage/pom.xml
index af090d14b..dcaee6ac6 100644
--- a/test-coverage/pom.xml
+++ b/test-coverage/pom.xml
@@ -6,7 +6,7 @@
gov.cms.qpp.conversion
qpp-conversion-tool-parent
- 2026.01.28.01-RELEASE
+ 2026.02.17.01-RELEASE
../pom.xml
@@ -53,6 +53,12 @@
org.jacoco
jacoco-maven-plugin
0.8.7
+
+
+ **/gov/cms/qpp/conversion/api/config/**
+ **/gov/cms/qpp/conversion/api/controllers/v1/**
+
+
report-aggregate