From 36bb743a45e57743aa14a65976aab6fe8bd37777 Mon Sep 17 00:00:00 2001 From: aschey-forpeople <162160982+aschey-forpeople@users.noreply.github.com> Date: Thu, 30 Jan 2025 08:22:56 -0800 Subject: [PATCH] BFD-3827: Support reading pagination info from POST bodies (#2533) --- .../server/war/commons/OffsetLinkBuilder.java | 46 ++----- .../war/commons/OpenAPIContentProvider.java | 18 ++- .../war/commons/PatientLinkBuilder.java | 75 ++++------- .../bfd/server/war/commons/StringUtils.java | 54 ++++++++ .../providers/R4CoverageResourceProvider.java | 7 + ...4ExplanationOfBenefitResourceProvider.java | 7 + .../providers/R4PatientResourceProvider.java | 29 +++- .../pac/AbstractR4ResourceProvider.java | 6 + .../providers/CoverageResourceProvider.java | 7 + .../ExplanationOfBenefitResourceProvider.java | 7 + .../providers/PatientResourceProvider.java | 25 +++- .../server/war/r4/providers/PatientE2E.java | 124 +++++++++++++++++- .../R4CoverageResourceProviderTest.java | 14 +- ...lanationOfBenefitResourceProviderTest.java | 17 +-- .../R4PatientResourceProviderIT.java | 2 +- .../R4PatientResourceProviderTest.java | 44 ++++--- .../server/war/r4/providers/pac/ClaimE2E.java | 58 ++++++++ .../r4/providers/pac/ClaimResponseE2E.java | 52 ++++++++ ...lanationOfBenefitResourceProviderTest.java | 15 ++- .../server/war/stu3/providers/PatientE2E.java | 37 ++++++ .../providers/PatientLinkBuilderTest.java | 56 ++++++-- .../PatientResourceProviderTest.java | 12 +- .../endpoint-responses/v1/metadata.json | 22 +++- .../endpoint-responses/v2/metadata.json | 22 +++- .../src/test/resources/openapi/posts.yaml | 35 +++++ 25 files changed, 633 insertions(+), 158 deletions(-) diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OffsetLinkBuilder.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OffsetLinkBuilder.java index 90f374cd45..4a5a65aace 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OffsetLinkBuilder.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OffsetLinkBuilder.java @@ -3,17 +3,13 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import gov.cms.bfd.server.war.stu3.providers.ExplanationOfBenefitResourceProvider; import gov.cms.bfd.sharedutils.exceptions.BadCodeMonkeyException; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.apache.http.client.utils.URIBuilder; import org.hl7.fhir.dstu3.model.Bundle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * PagingArguments encapsulates the arguments related to paging for the ExplanationOfBenefit, @@ -21,9 +17,6 @@ */ public final class OffsetLinkBuilder implements LinkBuilder { - private static final Logger LOGGER = - LoggerFactory.getLogger(ExplanationOfBenefitResourceProvider.class); - /** The page size for paging. */ private final Optional pageSize; @@ -45,6 +38,9 @@ public final class OffsetLinkBuilder implements LinkBuilder { */ private int numTotalResults = -1; + /** Start index request parameter. */ + private static final String PARAM_START_INDEX = "startIndex"; + /** * Instantiates a new Offset link builder. * @@ -52,8 +48,12 @@ public final class OffsetLinkBuilder implements LinkBuilder { * @param resource the resource */ public OffsetLinkBuilder(RequestDetails requestDetails, String resource) { - this.pageSize = parseIntegerParameters(requestDetails, Constants.PARAM_COUNT); - this.startIndex = parseIntegerParameters(requestDetails, "startIndex"); + this.pageSize = + StringUtils.parseIntegersFromRequest(requestDetails, Constants.PARAM_COUNT).stream() + .findFirst(); + this.startIndex = + StringUtils.parseIntegersFromRequest(requestDetails, PARAM_START_INDEX).stream() + .findFirst(); this.serverBase = requestDetails.getServerBaseForRequest(); this.resource = resource; this.requestDetails = requestDetails; @@ -76,27 +76,6 @@ private void validate() { } } - /** - * Returns the parsed parameter as an Integer. - * - * @param requestDetails the {@link RequestDetails} containing additional parameters for the URL - * in need of parsing out - * @param parameterToParse the parameter to parse from requestDetails - * @return the parsed parameter as an Integer, empty {@link Optional} if the parameter is not - * found - */ - private Optional parseIntegerParameters( - RequestDetails requestDetails, String parameterToParse) { - - if (requestDetails.getParameters().containsKey(parameterToParse)) { - - return Optional.of( - StringUtils.parseIntOrBadRequest( - requestDetails.getParameters().get(parameterToParse)[0], parameterToParse)); - } - return Optional.empty(); - } - /** {@inheritDoc} */ @Override public boolean isPagingRequested() { @@ -238,16 +217,15 @@ private String createPageLink(int startIndex) { Map params = new HashMap<>(requestDetails.getParameters()); // Add in paging related changes. - params.put("startIndex", new String[] {String.valueOf(startIndex)}); - params.put("_count", new String[] {String.valueOf(getPageSize())}); + params.put(PARAM_START_INDEX, new String[] {String.valueOf(startIndex)}); + params.put(Constants.PARAM_COUNT, new String[] {String.valueOf(getPageSize())}); try { // Setup URL base and resource. URIBuilder uri = new URIBuilder(serverBase + resource); - // Create query parameters by iterating thru all params entry sets. Handle multi values for + // Create query parameters by iterating through all params entry sets. Handle multi values for // the same parameter key. - ArrayList queryParams = new ArrayList(); for (Map.Entry paramSet : params.entrySet()) { for (String param : paramSet.getValue()) { uri.addParameter(paramSet.getKey(), param); diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OpenAPIContentProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OpenAPIContentProvider.java index 143a18bec6..63fe627d51 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OpenAPIContentProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/OpenAPIContentProvider.java @@ -128,12 +128,28 @@ public final class OpenAPIContentProvider { */ public static final String PATIENT_PARTD_CURSOR_VALUE = """ -Provide a pagination cursor or numeric _offset_ for processing Patient's Part D events information. +Provides a pagination cursor or numeric _offset_ for processing Patient's Part D events information. Examples: - `cursor=200` the first record is the 201st record - `cursor=1000` the first record is the 1001st record"""; + /*** + * Open API content for the _count parameter. + */ + public static final String COUNT_SHORT = "The number of records to return"; + + /*** + * Open API content for the _count parameter. + */ + public static final String COUNT_VALUE = + """ +Provides the number of records to be used for pagination. + +Examples: + - `_count=10`: return 10 values. +"""; + /** Open API content short description for /Patient's identifier parameter. */ public static final String BENEFICIARY_SP_RES_ID_SHORT = "Identifier resource for the covered party"; diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/PatientLinkBuilder.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/PatientLinkBuilder.java index a54524a6ed..b6b67306ea 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/PatientLinkBuilder.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/PatientLinkBuilder.java @@ -1,8 +1,10 @@ package gov.cms.bfd.server.war.commons; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.util.List; +import java.util.Optional; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Patient; @@ -20,14 +22,14 @@ public final class PatientLinkBuilder implements LinkBuilder { */ public static final int MAX_PAGE_SIZE = Integer.MAX_VALUE - 1; - /** The uri components. */ - private final UriComponents components; + /** The request details. */ + private final RequestDetails requestDetails; /** The count. */ - private final Integer count; + private final Optional count; /** The cursor value. */ - private final Long cursor; + private final Optional cursor; /** If there is another page for this link. */ private final boolean hasAnotherPage; @@ -38,12 +40,14 @@ public final class PatientLinkBuilder implements LinkBuilder { /** * Instantiates a new Patient link builder. * - * @param requestString the request string + * @param requestDetails the request details */ - public PatientLinkBuilder(String requestString) { - components = UriComponentsBuilder.fromUriString(requestString).build(); - count = extractCountParam(components); - cursor = extractCursorParam(components); + public PatientLinkBuilder(RequestDetails requestDetails) { + this.requestDetails = requestDetails; + count = + StringUtils.parseIntegersFromRequest(requestDetails, Constants.PARAM_COUNT).stream() + .findFirst(); + cursor = StringUtils.parseLongsFromRequest(requestDetails, PARAM_CURSOR).stream().findFirst(); hasAnotherPage = false; // Don't really know, so default to false validate(); } @@ -55,7 +59,7 @@ public PatientLinkBuilder(String requestString) { * @param hasAnotherPage if there is another page */ public PatientLinkBuilder(PatientLinkBuilder prev, boolean hasAnotherPage) { - components = prev.components; + requestDetails = prev.requestDetails; count = prev.count; cursor = prev.cursor; this.hasAnotherPage = hasAnotherPage; @@ -71,7 +75,7 @@ private void validate() { if (getPageSize() <= 0) { throw new InvalidRequestException("Value for pageSize cannot be zero or negative: %s"); } - if (!(getPageSize() <= MAX_PAGE_SIZE)) { + if (getPageSize() > MAX_PAGE_SIZE) { throw new InvalidRequestException("Page size must be less than " + MAX_PAGE_SIZE); } } @@ -79,19 +83,19 @@ private void validate() { /** {@inheritDoc} */ @Override public boolean isPagingRequested() { - return count != null; + return count.isPresent(); } /** {@inheritDoc} */ @Override public int getPageSize() { - return isPagingRequested() ? count : MAX_PAGE_SIZE; + return isPagingRequested() ? count.get() : MAX_PAGE_SIZE; } /** {@inheritDoc} */ @Override public boolean isFirstPage() { - return cursor == null || !isPagingRequested(); + return cursor.isEmpty() || !isPagingRequested(); } /** {@inheritDoc} */ @@ -104,12 +108,12 @@ public void addLinks(Bundle to) { to.addLink( new Bundle.BundleLinkComponent() .setRelation(Constants.LINK_SELF) - .setUrl(components.toUriString())); + .setUrl(requestDetails.getCompleteUrl())); to.addLink( new Bundle.BundleLinkComponent().setRelation(Constants.LINK_FIRST).setUrl(buildUrl(null))); if (hasAnotherPage) { - Patient lastPatient = (Patient) entries.get(entries.size() - 1).getResource(); + Patient lastPatient = (Patient) entries.getLast().getResource(); Long lastPatientId = StringUtils.parseLongOrBadRequest(lastPatient.getId(), PARAM_CURSOR); to.addLink( new Bundle.BundleLinkComponent() @@ -128,15 +132,15 @@ public void addLinks(org.hl7.fhir.r4.model.Bundle to) { to.addLink( new org.hl7.fhir.r4.model.Bundle.BundleLinkComponent() .setRelation(Constants.LINK_SELF) - .setUrl(components.toUriString())); + .setUrl(requestDetails.getCompleteUrl())); to.addLink( new org.hl7.fhir.r4.model.Bundle.BundleLinkComponent() .setRelation(Constants.LINK_FIRST) .setUrl(buildUrl(null))); - if (entries.size() == getPageSize() && entries.size() > 0) { + if (entries.size() == getPageSize() && !entries.isEmpty()) { org.hl7.fhir.r4.model.Patient lastPatient = - (org.hl7.fhir.r4.model.Patient) entries.get(entries.size() - 1).getResource(); + (org.hl7.fhir.r4.model.Patient) entries.getLast().getResource(); Long lastPatientId = Long.parseLong(lastPatient.getId()); to.addLink( new org.hl7.fhir.r4.model.Bundle.BundleLinkComponent() @@ -151,36 +155,7 @@ public void addLinks(org.hl7.fhir.r4.model.Bundle to) { * @return the cursor */ public Long getCursor() { - return cursor; - } - - /** - * Extracts the count from the component object. - * - * @param components the components - * @return the count, or {@code null} if the count text was {@code null} in the compntent object - * @throws InvalidRequestException (http 400 error) if the count could not be parsed into an - * integer - */ - private Integer extractCountParam(UriComponents components) { - String countText = components.getQueryParams().getFirst(Constants.PARAM_COUNT); - if (countText != null) { - return StringUtils.parseIntOrBadRequest(countText, Constants.PARAM_COUNT); - } - return null; - } - - /** - * Extracts the cursor from the component object. - * - * @param components the components - * @return the cursor, or {@code null} if the cursor text was {@code null} in the component object - */ - private Long extractCursorParam(UriComponents components) { - String cursorText = components.getQueryParams().getFirst(PARAM_CURSOR); - return cursorText != null && cursorText.length() > 0 - ? StringUtils.parseLongOrBadRequest(cursorText, PARAM_CURSOR) - : null; + return cursor.orElse(null); } /** @@ -190,6 +165,8 @@ private Long extractCursorParam(UriComponents components) { * @return the url string */ private String buildUrl(Long cursor) { + UriComponents components = + UriComponentsBuilder.fromUriString(requestDetails.getCompleteUrl()).build(); MultiValueMap params = components.getQueryParams(); if (cursor != null) { params = new LinkedMultiValueMap<>(params); diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/StringUtils.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/StringUtils.java index 2934fc8af3..a038ca45ba 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/StringUtils.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/commons/StringUtils.java @@ -1,7 +1,13 @@ package gov.cms.bfd.server.war.commons; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; /** Helper Utils class for Functions shared across. multiple classes. */ public class StringUtils { @@ -68,4 +74,52 @@ public static int parseIntOrBadRequest(String input, String fieldName) { String.format("Failed to parse value for %s as a number.", fieldName)); } } + + /** + * Returns the parsed parameter as an Integer. + * + * @param requestDetails the {@link RequestDetails} containing additional parameters for the + * request + * @param parameterToParse the parameter to parse from requestDetails + * @return the parsed parameter as an Integer, empty {@link Optional} if the parameter is not + * found + */ + public static List parseIntegersFromRequest( + RequestDetails requestDetails, String parameterToParse) { + return getParametersFromRequest(requestDetails, parameterToParse) + .map(p -> parseIntOrBadRequest(p, parameterToParse)) + .toList(); + } + + /** + * Returns the parsed parameter as a Long. + * + * @param requestDetails the {@link RequestDetails} containing additional parameters for the + * request + * @param parameterToParse the parameter to parse from requestDetails + * @return the parsed parameter as a Long, empty {@link Optional} if the parameter is not found + */ + public static List parseLongsFromRequest( + RequestDetails requestDetails, String parameterToParse) { + return getParametersFromRequest(requestDetails, parameterToParse) + .map(p -> parseLongOrBadRequest(p, parameterToParse)) + .toList(); + } + + /** + * Extracts the first parameter from the request if it's present and non-empty. + * + * @param requestDetails request details + * @param parameterToParse name of parameter to retrieve + * @return extracted value + */ + private static Stream getParametersFromRequest( + RequestDetails requestDetails, String parameterToParse) { + Map parameters = requestDetails.getParameters(); + if (parameters.containsKey(parameterToParse)) { + String[] paramValues = parameters.get(parameterToParse); + return Arrays.stream(paramValues).filter(p -> p != null && !p.isBlank()); + } + return Stream.of(); + } } diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProvider.java index e2f4283914..c4b3720541 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProvider.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -236,6 +237,7 @@ public Coverage read(@IdParam IdType coverageId) { * and find matches for * @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine * pagination + * @param count an {@link OptionalParam} for the count used in pagination * @param lastUpdated an {@link OptionalParam} to filter the results based on the passed date * range * @param requestDetails a {@link RequestDetails} containing the details of the request URL, used @@ -258,6 +260,11 @@ public Bundle searchByBeneficiary( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "_lastUpdated") @Description( shortDefinition = OpenAPIContentProvider.PATIENT_LAST_UPDATED_SHORT, diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProvider.java index f4cd8824ff..2760b60663 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProvider.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; @@ -268,6 +269,7 @@ metricRegistry, getClass().getSimpleName(), "query", "eob_by_id")) { * @param type a list of {@link ClaimType} to include in the result. Defaults to all types. * @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine * pagination + * @param count an {@link OptionalParam} for the count used in pagination * @param excludeSamhsa an {@link OptionalParam} that, if "true", will use {@link * R4EobSamhsaMatcher} to filter out all SAMHSA-related claims from the results * @param lastUpdated an {@link OptionalParam} that specifies a date range for the lastUpdated @@ -300,6 +302,11 @@ public Bundle findByPatient( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "excludeSAMHSA") @Description( shortDefinition = OpenAPIContentProvider.EOB_EXCLUDE_SAMSHA_SHORT, diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProvider.java index 143881b372..a8857853d7 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProvider.java @@ -12,6 +12,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; @@ -228,6 +229,7 @@ public Patient read(@IdParam IdType patientId, RequestDetails requestDetails) { * Patient#getId()} to try and find a matching {@link Patient} for * @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine * pagination + * @param count an {@link OptionalParam} used for paging * @param lastUpdated an {@link OptionalParam} to filter the results based on the passed date * range * @param requestDetails a {@link RequestDetails} containing the details of the request URL, used @@ -249,6 +251,11 @@ public Bundle searchByLogicalId( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "_lastUpdated") @Description( shortDefinition = OpenAPIContentProvider.PATIENT_LAST_UPDATED_VALUE, @@ -352,6 +359,7 @@ public Bundle searchByLogicalId( * @param coverageId the coverage id * @param referenceYear the reference year * @param cursor the cursor for paging + * @param count the count for paging * @param requestDetails the request details * @return the bundle representing the results */ @@ -359,7 +367,7 @@ public Bundle searchByLogicalId( @Trace @RetryOnFailoverOrConnectionException public Bundle searchByCoverageContract( - // This is very explicit as a place holder until this kind + // This is very explicit as a placeholder until this kind // of relational search is more common. @RequiredParam(name = "_has:Coverage.extension") @Description( @@ -376,6 +384,11 @@ public Bundle searchByCoverageContract( shortDefinition = OpenAPIContentProvider.PATIENT_PARTD_CURSOR_SHORT, value = OpenAPIContentProvider.PATIENT_PARTD_CURSOR_VALUE) String cursor, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, RequestDetails requestDetails) { String contractMonth = @@ -407,7 +420,7 @@ public Bundle searchByCoverageContract( * @return the bundle representing the results */ public Bundle searchByCoverageContractByFieldName( - // This is very explicit as a place holder until this kind + // This is very explicit as a placeholder until this kind // of relational search is more common. @RequiredParam(name = "_has:Coverage.extension") @Description( @@ -418,7 +431,7 @@ public Bundle searchByCoverageContractByFieldName( RequestDetails requestDetails) { checkCoverageId(coverageId); RequestHeaders requestHeader = RequestHeaders.getHeaderWrapper(requestDetails); - PatientLinkBuilder paging = new PatientLinkBuilder(requestDetails.getCompleteUrl()); + PatientLinkBuilder paging = new PatientLinkBuilder(requestDetails); CanonicalOperation operation = new CanonicalOperation(CanonicalOperation.Endpoint.V2_PATIENT); operation.setOption("by", "coverageContract"); @@ -674,6 +687,7 @@ private TypedQuery queryBeneficiariesByIds(List ids) { * Patient#getIdentifier()} to try and find a matching {@link Patient} for * @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine * pagination + * @param count an {@link OptionalParam} for the record count used for pagination * @param lastUpdated an {@link OptionalParam} to filter the results based on the passed date * range * @param requestDetails a {@link RequestDetails} containing the details of the request URL, used @@ -695,6 +709,11 @@ public Bundle searchByIdentifier( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "_lastUpdated") @Description( shortDefinition = OpenAPIContentProvider.PATIENT_LAST_UPDATED_SHORT, @@ -992,13 +1011,13 @@ private String partDFieldByMonth(CcwCodebookVariable month) { */ @Trace private Bundle searchByCoverageContractAndYearMonth( - // This is very explicit as a place holder until this kind + // This is very explicit as a placeholder until this kind // of relational search is more common. TokenParam coverageId, LocalDate yearMonth, RequestDetails requestDetails) { checkCoverageId(coverageId); RequestHeaders requestHeader = RequestHeaders.getHeaderWrapper(requestDetails); - PatientLinkBuilder paging = new PatientLinkBuilder(requestDetails.getCompleteUrl()); + PatientLinkBuilder paging = new PatientLinkBuilder(requestDetails); CanonicalOperation operation = new CanonicalOperation(CanonicalOperation.Endpoint.V2_PATIENT); operation.setOption("by", "coverageContractForYearMonth"); diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/pac/AbstractR4ResourceProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/pac/AbstractR4ResourceProvider.java index 53de067352..01458b096f 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/pac/AbstractR4ResourceProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/r4/providers/pac/AbstractR4ResourceProvider.java @@ -385,6 +385,7 @@ private void logMbiIdentifiersToMdc(Mbi mbi) { * @param mbi the patient identifier to search for * @param types a list of claim types to include * @param startIndex the offset used for result pagination + * @param count the count used for result pagination * @param hashed a boolean indicating whether the MBI is hashed * @param samhsa if {@code true}, exclude all SAMHSA-related resources * @param lastUpdated range which to include resources last updated within @@ -412,6 +413,11 @@ public Bundle findByPatient( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "isHashed") @Description( shortDefinition = OpenAPIContentProvider.PAC_IS_HASHED, diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/CoverageResourceProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/CoverageResourceProvider.java index 74d27bc3b3..29e031d738 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/CoverageResourceProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/CoverageResourceProvider.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -193,6 +194,7 @@ public Coverage read(@IdParam IdType coverageId) { * and find matches for * @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine * pagination + * @param count an {@link OptionalParam} for the count used in pagination * @param lastUpdated an {@link OptionalParam} to filter the results based on the passed date * range * @param requestDetails a {@link RequestDetails} containing the details of the request URL, used @@ -214,6 +216,11 @@ public Bundle searchByBeneficiary( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "_lastUpdated") @Description( shortDefinition = OpenAPIContentProvider.PATIENT_LAST_UPDATED_SHORT, diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProvider.java index 1c14f06879..20c7ffb0a9 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProvider.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -263,6 +264,7 @@ metricRegistry, getClass().getSimpleName(), "query", "eob_by_id")) { * @param type a list of {@link ClaimType} to include in the result. Defaults to all types. * @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine * pagination + * @param count an {@link OptionalParam} used for the count in pagination * @param excludeSamhsa an {@link OptionalParam} that, if "true", will use {@link * Stu3EobSamhsaMatcher} to filter out all SAMHSA-related claims from the results * @param lastUpdated an {@link OptionalParam} that specifies a date range for the lastUpdated @@ -294,6 +296,11 @@ public Bundle findByPatient( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "excludeSAMHSA") @Description( shortDefinition = OpenAPIContentProvider.EOB_EXCLUDE_SAMSHA_SHORT, diff --git a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProvider.java b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProvider.java index 019d5e4c59..521d945dc2 100644 --- a/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProvider.java +++ b/apps/bfd-server/bfd-server-war/src/main/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProvider.java @@ -12,6 +12,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; @@ -217,6 +218,7 @@ public Patient read(@IdParam IdType patientId, RequestDetails requestDetails) { * @param coverageId the coverage id * @param referenceYear the reference year * @param cursor the cursor for paging + * @param count the count for paging * @param requestDetails the request details * @return the bundle representing the results */ @@ -224,7 +226,7 @@ public Patient read(@IdParam IdType patientId, RequestDetails requestDetails) { @Trace @RetryOnFailoverOrConnectionException public Bundle searchByCoverageContract( - // This is very explicit as a place holder until this kind + // This is very explicit as a placeholder until this kind // of relational search is more common. @RequiredParam(name = "_has:Coverage.extension") @Description( @@ -241,6 +243,11 @@ public Bundle searchByCoverageContract( shortDefinition = OpenAPIContentProvider.PATIENT_PARTD_CURSOR_SHORT, value = OpenAPIContentProvider.PATIENT_PARTD_CURSOR_VALUE) String cursor, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, RequestDetails requestDetails) { // Figure out what month they're searching for. String contractMonth = @@ -274,6 +281,7 @@ public Bundle searchByCoverageContract( * Patient#getId()} to try and find a matching {@link Patient} for * @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine * pagination + * @param count an {@link OptionalParam} for the count used in pagination * @param lastUpdated an {@link OptionalParam} to filter the results based on the passed date * range * @param requestDetails a {@link RequestDetails} containing the details of the request URL, used @@ -295,6 +303,11 @@ public Bundle searchByLogicalId( shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT, value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE) String startIndex, + @OptionalParam(name = Constants.PARAM_COUNT) + @Description( + shortDefinition = OpenAPIContentProvider.COUNT_SHORT, + value = OpenAPIContentProvider.COUNT_VALUE) + String count, @OptionalParam(name = "_lastUpdated") @Description( shortDefinition = OpenAPIContentProvider.PATIENT_LAST_UPDATED_VALUE, @@ -402,7 +415,7 @@ public Bundle searchByLogicalId( */ @Trace private Bundle searchByCoverageContractAndYearMonth( - // This is very explicit as a place holder until this kind + // This is very explicit as a placeholder until this kind // of relational search is more common. TokenParam coverageId, LocalDate yearMonth, RequestDetails requestDetails) { checkCoverageId(coverageId); @@ -417,7 +430,7 @@ private Bundle searchByCoverageContractAndYearMonth( CommonHeaders.HEADER_NAME_INCLUDE_IDENTIFIERS)); } - PatientLinkBuilder paging = new PatientLinkBuilder(requestDetails.getCompleteUrl()); + PatientLinkBuilder paging = new PatientLinkBuilder(requestDetails); CanonicalOperation operation = new CanonicalOperation(CanonicalOperation.Endpoint.V1_PATIENT); operation.setOption("by", "coverageContractForYearMonth"); @@ -730,6 +743,7 @@ private List queryBeneficiariesByIdsWithBeneficiaryMonthlys(List formParams = + Map.of("_has:Coverage.extension", contractId, "_has:Coverage.rfrncyr", refYear); + String requestString = patientEndpoint + "_search?_count=1"; + + Response response = + given() + .spec(requestAuth) + .header("Content-Type", "application/x-www-form-urlencoded") + .formParams(formParams) + .expect() + .log() + .body() + .body("resourceType", equalTo("Bundle")) + // Should match the paging size + .body("entry.size()", equalTo(1)) + // Check pagination has the right number of links + .body("link.size()", equalTo(3)) + /* Patient (specifically search by contract) uses different paging + than all other resources, due to using bene id cursors. + There is no "last" page or "previous", only first/next/self + */ + .body("link.relation", hasItems("first", "next", "self")) + .statusCode(200) + .when() + .post(requestString); + + // Try to get the next page + String nextLink = testUtils.getPaginationLink(response, "next"); + + // However, there is no next page. V2 Patient contract pagination doesnt check this until its + // called + given() + .spec(requestAuth) + .header("Content-Type", "application/x-www-form-urlencoded") + .formParams(formParams) + .urlEncodingEnabled(false) + .expect() + .log() + .body() + .body("resourceType", equalTo("Bundle")) + // Check there were no additional results + .body("$", not(hasKey("entry"))) + // We should only have first and self link now + .body("link.size()", equalTo(2)) + .body("link.relation", hasItems("first", "self")) + .statusCode(200) + .when() + .post(nextLink); + } + + /** Tests the pagination links using a POST request with the pagination info in the POST body. */ + @Test + public void testPatientByPartDContractWhenPaginationExpectPagingLinksPostBody() { + ServerTestUtils.get() + .loadData( + Arrays.asList( + StaticRifResource.SAMPLE_A_BENES, StaticRifResource.SAMPLE_A_BENEFICIARY_HISTORY)); + String contractId = + CCWUtils.calculateVariableReferenceUrl(CcwCodebookVariable.PTDCNTRCT01) + "|S4607"; + String refYear = CCWUtils.calculateVariableReferenceUrl(CcwCodebookVariable.RFRNC_YR) + "|2018"; + Map formParams = + Map.of( + "_has:Coverage.extension", contractId, "_has:Coverage.rfrncyr", refYear, "_count", "1"); + String requestString = patientEndpoint + "_search"; + + Response response = + given() + .spec(requestAuth) + .header("Content-Type", "application/x-www-form-urlencoded") + .formParams(formParams) + .expect() + .log() + .body() + .body("resourceType", equalTo("Bundle")) + // Should match the paging size + .body("entry.size()", equalTo(1)) + // Check pagination has the right number of links + .body("link.size()", equalTo(3)) + /* Patient (specifically search by contract) uses different paging + than all other resources, due to using bene id cursors. + There is no "last" page or "previous", only first/next/self + */ + .body("link.relation", hasItems("first", "next", "self")) + .statusCode(200) + .when() + .post(requestString); + + // Try to get the next page + String nextLink = testUtils.getPaginationLink(response, "next"); + + // However, there is no next page. V2 Patient contract pagination doesnt check this until its + // called + given() + .spec(requestAuth) + .header("Content-Type", "application/x-www-form-urlencoded") + .formParams(formParams) + .urlEncodingEnabled(false) + .expect() + .log() + .body() + .body("resourceType", equalTo("Bundle")) + // Check there were no additional results + .body("$", not(hasKey("entry"))) + // We should only have first and self link now + .body("link.size()", equalTo(2)) + .body("link.relation", hasItems("first", "self")) + .statusCode(200) + .when() + .post(nextLink); + } + /** * Verifies that Patient searchByIdentifier returns a 200 when using HTTP POST with an unhashed * MBI. */ @Test public void testPatientUsingPostByIdentifierUsingUnhashedMbi() { - List loadedRecords = loadDataWithAdditionalBeneHistory(); + loadDataWithAdditionalBeneHistory(); // _search needed to distinguish the POST version of the endpoint (HAPI-FHIR) String requestString = patientEndpoint + "_search"; diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProviderTest.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProviderTest.java index 6fea8099a1..260aada921 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProviderTest.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4CoverageResourceProviderTest.java @@ -307,7 +307,7 @@ public void testCoverageByBeneficiaryWhereMissingBeneExpectEmptyBundle() { when(mockQuery.getSingleResult()).thenThrow(NoResultException.class); Bundle bundle = - coverageProvider.searchByBeneficiary(beneficiary, null, null, null, requestDetails); + coverageProvider.searchByBeneficiary(beneficiary, null, null, null, null, requestDetails); assertEquals(0, bundle.getTotal()); } @@ -315,7 +315,7 @@ public void testCoverageByBeneficiaryWhereMissingBeneExpectEmptyBundle() { /** Tests that the transformer is called with only the C4BB profile when C4DIC is not enabled. */ @Test public void testCoverageByBeneficiaryCount() { - coverageProvider.searchByBeneficiary(beneficiary, null, null, null, requestDetails); + coverageProvider.searchByBeneficiary(beneficiary, null, null, null, null, requestDetails); verify(coverageTransformer).transform(any(), eq(Profile.C4BB)); } @@ -326,7 +326,7 @@ public void testCoverageByBeneficiaryCount() { @Test public void testCoverageByBeneficiaryCountC4BBProfile() { coverageProvider.searchByBeneficiary( - beneficiary, null, null, ProfileConstants.C4BB_COVERAGE_URL, requestDetails); + beneficiary, null, null, null, ProfileConstants.C4BB_COVERAGE_URL, requestDetails); verify(coverageTransformer).transform(any(), eq(Profile.C4BB)); } @@ -343,7 +343,7 @@ public void testCoverageByBeneficiaryCountC4DICProfile() { coverageProvider.setEntityManager(entityManager); coverageProvider.searchByBeneficiary( - beneficiary, null, null, ProfileConstants.C4DIC_COVERAGE_URL, requestDetails); + beneficiary, null, null, null, ProfileConstants.C4DIC_COVERAGE_URL, requestDetails); verify(coverageTransformer).transform(any(), eq(Profile.C4DIC)); } @@ -356,7 +356,7 @@ public void testCoverageByBeneficiaryCountBothProfiles() { metricRegistry, loadedFilterManager, coverageTransformer, true); coverageProvider.setEntityManager(entityManager); - coverageProvider.searchByBeneficiary(beneficiary, null, null, null, requestDetails); + coverageProvider.searchByBeneficiary(beneficiary, null, null, null, null, requestDetails); verify(coverageTransformer).transform(any(), eq(Profile.C4BB)); } @@ -386,7 +386,7 @@ public void testCoverageByBeneficiaryWherePagingRequestedExpectPageData() { // Note: startIndex in the param is not used, must be passed from requestDetails Bundle bundle = - coverageProvider.searchByBeneficiary(beneficiary, null, null, null, requestDetails); + coverageProvider.searchByBeneficiary(beneficiary, null, null, null, null, requestDetails); /* * Check paging; Verify that only the first and last paging links exist, since there should @@ -406,7 +406,7 @@ public void testCoverageByBeneficiaryWhereNoPagingRequestedExpectNoPageData() { when(requestDetails.getHeader(any())).thenReturn(""); Bundle bundle = - coverageProvider.searchByBeneficiary(beneficiary, null, null, null, requestDetails); + coverageProvider.searchByBeneficiary(beneficiary, null, null, null, null, requestDetails); /* * Check that no paging was added when not requested diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProviderTest.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProviderTest.java index 790f1b44bc..66efbb5081 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProviderTest.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4ExplanationOfBenefitResourceProviderTest.java @@ -12,7 +12,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -58,7 +57,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; @@ -87,9 +85,6 @@ public class R4ExplanationOfBenefitResourceProviderTest { /** The class under test. */ R4ExplanationOfBenefitResourceProvider eobProvider; - /** ExecutorService for threading. */ - ExecutorService mockExecutorService = spy(Executors.newFixedThreadPool(2)); - /** The mocked request details. */ @Mock ServletRequestDetails requestDetails; @@ -443,7 +438,7 @@ void testFindByPatientWhenInvalidIdExpectException() { InvalidRequestException.class, () -> eobProvider.findByPatient( - patientParam, null, null, null, null, null, null, null, requestDetails)); + patientParam, null, null, null, null, null, null, null, null, requestDetails)); } /** @@ -456,7 +451,7 @@ void testFindByPatientWithPageSizeNotProvidedExpectNoPaging() { Bundle response = eobProvider.findByPatient( - patientParam, null, null, null, null, null, null, null, requestDetails); + patientParam, null, null, null, null, null, null, null, null, requestDetails); assertNotNull(response); assertNull(response.getLink(Constants.LINK_NEXT)); @@ -481,7 +476,7 @@ void testFindByPatientWithNegativeStartIndexExpectException() { InvalidRequestException.class, () -> eobProvider.findByPatient( - patientParam, null, null, null, null, null, null, null, requestDetails)); + patientParam, null, null, null, null, null, null, null, null, requestDetails)); } /** @@ -496,7 +491,7 @@ void testFindByPatientWhenNoClaimsFoundExpectEmptyBundle() { Bundle response = eobProvider.findByPatient( - patientParam, null, null, null, null, null, null, null, requestDetails); + patientParam, null, null, null, null, null, null, null, null, requestDetails); assertEquals(0, response.getTotal()); } @@ -512,7 +507,7 @@ void testFindByPatientSupportsWildcardClaimTypeV2() { Bundle response = eobProvider.findByPatient( - patientParam, listParam, null, null, null, null, null, null, requestDetails); + patientParam, listParam, null, null, null, null, null, null, null, requestDetails); assertNotNull(response); assertEquals(0, response.getTotal()); @@ -526,7 +521,7 @@ void testFindByPatientSupportsWildcardClaimTypeV2() { void testFindByPatientSupportsNullClaimTypeV2() { Bundle response = eobProvider.findByPatient( - patientParam, null, null, null, null, null, null, null, requestDetails); + patientParam, null, null, null, null, null, null, null, null, requestDetails); assertNotNull(response); assertEquals(0, response.getTotal()); diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderIT.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderIT.java index cd2dba1d07..bbfa4b3c63 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderIT.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderIT.java @@ -231,7 +231,7 @@ public void searchForExistingPatientByMbiHashHasHistoricMbis() { testBene.getBeneficiaryHistories().add(beneHistoryEntry); Bundle searchResults = - patientProvider.searchByIdentifier(identifier, null, null, requestDetails); + patientProvider.searchByIdentifier(identifier, null, null, null, requestDetails); assertNotNull(searchResults); assertEquals(1, searchResults.getTotal()); diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderTest.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderTest.java index 6c767c911e..9b1769a26a 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderTest.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/R4PatientResourceProviderTest.java @@ -350,7 +350,7 @@ public void testReadWhenVersionedIdExpectException() { */ @Test public void testSearchByCoverageContractWhenValidContractExpectMetrics() { - patientProvider.searchByCoverageContract(contractId, refYear, null, requestDetails); + patientProvider.searchByCoverageContract(contractId, refYear, null, null, requestDetails); // Three queries are made with metrics here String expectedTimerName = @@ -377,7 +377,7 @@ public void testSearchByCoverageContractWhenValidContractExpectMetrics() { @Test public void testSearchByCoverageContractWhenContractExistsExpectTransformerCalled() { Bundle response = - patientProvider.searchByCoverageContract(contractId, refYear, null, requestDetails); + patientProvider.searchByCoverageContract(contractId, refYear, null, null, requestDetails); assertEquals(1, response.getTotal()); verify(beneficiaryTransformerV2, times(1)).transform(eq(testBene), any()); @@ -398,7 +398,7 @@ public void testSearchByCoverageContractWhenContractExistsExpectTransformerCalle @Test public void testSearchByCoverageContractWhenNoPagingRequestedExpectNoPageData() { Bundle response = - patientProvider.searchByCoverageContract(contractId, refYear, null, requestDetails); + patientProvider.searchByCoverageContract(contractId, refYear, null, null, requestDetails); /* * Check that no paging was added @@ -419,9 +419,11 @@ public void testSearchByCoverageContractWhenPagingRequestedExpectPageData() { // Apparently the contract endpoint gets the count from the request url instead of how the other // endpoints do when(requestDetails.getCompleteUrl()).thenReturn("https://test?_count=1"); + when(requestDetails.getParameters()).thenReturn(Map.of("_count", new String[] {"1"})); // Note: cursor in the param is not used, must be passed from requestDetails + Bundle response = - patientProvider.searchByCoverageContract(contractId, refYear, null, requestDetails); + patientProvider.searchByCoverageContract(contractId, refYear, null, null, requestDetails); /* * Check paging; Paging on contract also apparently returns differently @@ -444,7 +446,7 @@ public void testSearchByCoverageContractWhenNoPatientsExpectEmptyBundle() { when(mockQuery.getResultList()).thenReturn(new ArrayList()); Bundle response = - patientProvider.searchByCoverageContract(contractId, refYear, null, requestDetails); + patientProvider.searchByCoverageContract(contractId, refYear, null, null, requestDetails); assertEquals(0, response.getTotal()); } @@ -462,7 +464,7 @@ public void testSearchByCoverageContractWhenNonNumericContractIdExpectException( InvalidRequestException.class, () -> patientProvider.searchByCoverageContract( - contractId, refYear, null, requestDetails)); + contractId, refYear, null, null, requestDetails)); assertEquals( "Failed to parse value for Contract Year as a number.", exception.getLocalizedMessage()); } @@ -480,7 +482,7 @@ public void testSearchByCoverageContractWhenWrongLengthContractIdExpectException InvalidRequestException.class, () -> patientProvider.searchByCoverageContract( - contractId, refYear, null, requestDetails)); + contractId, refYear, null, null, requestDetails)); assertEquals( "Coverage id is not expected length; value 123 is not expected length 5", exception.getLocalizedMessage()); @@ -498,7 +500,8 @@ public void testSearchByLogicalIdWhenBeneExistsExpectTransformerCalled() { when(testPatient.getMeta()).thenReturn(mockMeta); when(loadedFilterManager.getTransactionTime()).thenReturn(Instant.now()); - Bundle response = patientProvider.searchByLogicalId(logicalId, null, null, requestDetails); + Bundle response = + patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails); assertEquals(testPatient, response.getEntry().get(0).getResource()); verify(beneficiaryTransformerV2, times(1)).transform(eq(testBene), any(), eq(true)); @@ -513,7 +516,8 @@ public void testSearchByLogicalIdWhenBeneDoesntExistExpectEmptyBundle() { when(loadedFilterManager.getTransactionTime()).thenReturn(Instant.now()); when(mockQuery.getSingleResult()).thenThrow(NoResultException.class); - Bundle response = patientProvider.searchByLogicalId(logicalId, null, null, requestDetails); + Bundle response = + patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails); assertEquals(0, response.getTotal()); } @@ -536,7 +540,8 @@ public void testSearchByLogicalIdWhenPagingRequestedExpectPageData() { params.put(Constants.PARAM_COUNT, new String[] {"1"}); when(requestDetails.getParameters()).thenReturn(params); // Note: startIndex in the param is not used, must be passed from requestDetails - Bundle response = patientProvider.searchByLogicalId(logicalId, null, null, requestDetails); + Bundle response = + patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails); /* * Check paging; Verify that only the first and last paging links exist, since there should @@ -558,7 +563,8 @@ public void testSearchByLogicalIdWhenNoPagingRequestedExpectNoPageData() { // Set no paging params Map params = new HashMap<>(); when(requestDetails.getParameters()).thenReturn(params); - Bundle response = patientProvider.searchByLogicalId(logicalId, null, null, requestDetails); + Bundle response = + patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails); /* * Check that no paging was added @@ -581,7 +587,7 @@ public void testSearchByLogicalIdWhenBlankIdExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByLogicalId(logicalId, null, null, requestDetails)); + () -> patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails)); assertEquals("Missing required id value", exception.getLocalizedMessage()); } @@ -597,7 +603,7 @@ public void testSearchByLogicalIdWhenNonEmptySystemExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByLogicalId(logicalId, null, null, requestDetails)); + () -> patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails)); assertEquals( "System is unsupported here and should not be set (system)", exception.getLocalizedMessage()); @@ -622,7 +628,7 @@ public void testSearchByIdentifierIdWhenMbiHashExpectPatientAndMetrics() { when(mockQueryFunction.getResultList()).thenReturn(rawValues); Bundle bundle = - patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, requestDetails); + patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, null, requestDetails); assertEquals(1, bundle.getTotal()); @@ -647,7 +653,7 @@ public void testSearchByIdentifierIdWhenNoPagingExpectNoPageData() { when(requestDetails.getHeader(any())).thenReturn(""); Bundle bundle = - patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, requestDetails); + patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, null, requestDetails); /* * Check that no paging was added when not requested @@ -672,7 +678,7 @@ public void testSearchByIdentifierIdWhenPagingRequestedExpectPageData() { when(requestDetails.getParameters()).thenReturn(params); // Note: startIndex in the param is not used, must be passed from requestDetails Bundle bundle = - patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, requestDetails); + patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, null, requestDetails); /* * Check paging; Verify that only the first and last paging links exist, since there should @@ -694,7 +700,7 @@ public void testSearchByIdentifierIdWhenBeneDoesntExistExpectEmptyBundle() { when(mockQueryFunction.getSingleResult()).thenThrow(NoResultException.class); Bundle bundle = - patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, requestDetails); + patientProvider.searchByIdentifier(mbiHashIdentifier, null, null, null, requestDetails); assertEquals(0, bundle.getTotal()); } @@ -712,7 +718,7 @@ public void testSearchByIdentifierIdWhenEmptyHashExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByIdentifier(identifier, null, null, requestDetails)); + () -> patientProvider.searchByIdentifier(identifier, null, null, null, requestDetails)); assertEquals("lookup value cannot be null/empty", exception.getLocalizedMessage()); } @@ -728,7 +734,7 @@ public void testSearchByIdentifierIdWhenBadSystemExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByIdentifier(identifier, null, null, requestDetails)); + () -> patientProvider.searchByIdentifier(identifier, null, null, null, requestDetails)); assertEquals("Unsupported identifier system: bad-system", exception.getLocalizedMessage()); } } diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimE2E.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimE2E.java index a65b2a892a..d5ae372b36 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimE2E.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimE2E.java @@ -220,6 +220,28 @@ public void shouldGetCorrectClaimResourcesByMbiHash() { verifyResponseMatchesFor(requestString, false, "claimSearch", MBI_IGNORE_PATTERNS); } + /** Tests the search response when using a POST request. */ + @Test + public void shouldGetCorrectClaimResourcesByMbiPost() { + // Sending multiple entries with the same key doesn't appear to work in the POST body + String requestString = claimEndpoint + "_search"; + + // NOTE: Sending multiple values only works with List as the map value, not String[] + // because Rest Assured specifically looks for types that extend Collection + verifyPostResponseMatchesFor( + requestString, + Map.of( + "mbi", + List.of(RDATestUtils.MBI), + "isHashed", + List.of("false"), + "service-date", + List.of("gt1970-07-18", "lt1970-07-25")), + false, + "claimSearch", + MBI_IGNORE_PATTERNS); + } + /** * Tests to see if the correct response is given when a search is done for {@link Claim}s using * given mbi and service-date range with tax numbers included. In this test case the query finds @@ -470,6 +492,42 @@ private void verifyResponseMatchesFor( AssertUtils.assertJsonEquals(expected, response, ignorePatterns); } + /** + * Verifies the Claim response for the given requestString returns a 200 and the json response + * matches the expected response file. + * + * @param requestString the request string to search with + * @param formParams form POST params + * @param includeTaxNumbers the value to use for IncludeTaxNumbers header + * @param expectedResponseFileName the name of the response file to compare against + * @param ignorePatterns the ignore patterns to use when comparing the result file to the response + */ + private void verifyPostResponseMatchesFor( + String requestString, + Map formParams, + boolean includeTaxNumbers, + String expectedResponseFileName, + Set ignorePatterns) { + + String response = + given() + .spec(requestAuth) + .formParams(formParams) + .header(CommonHeaders.HEADER_NAME_INCLUDE_TAX_NUMBERS, includeTaxNumbers) + .expect() + .statusCode(200) + .when() + .post(requestString) + .then() + .extract() + .response() + .asString(); + + String expected = rdaTestUtils.expectedResponseFor(expectedResponseFileName); + + AssertUtils.assertJsonEquals(expected, response, ignorePatterns); + } + /** * Verifies the Claim response for the given requestString returns a 400 and the json response of * FHIR OperationOutcome. diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimResponseE2E.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimResponseE2E.java index ce8264946e..a10e872dfc 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimResponseE2E.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/r4/providers/pac/ClaimResponseE2E.java @@ -11,6 +11,7 @@ import gov.cms.bfd.server.war.utils.AssertUtils; import gov.cms.bfd.server.war.utils.RDATestUtils; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -143,6 +144,24 @@ void shouldGetCorrectClaimResponseResourcesByMbiHash() { verifyResponseMatchesFor(requestString, "claimResponseSearch", MBI_IGNORE_PATTERNS); } + /** Tests the search endpoint using a POST request. */ + @Test + void shouldGetCorrectClaimResponseResourcesByMbiPost() { + String requestString = claimResponseEndpoint + "_search"; + + verifyPostResponseMatchesFor( + requestString, + Map.of( + "mbi", + List.of(RDATestUtils.MBI), + "isHashed", + "false", + "service-date", + List.of("gt1970-07-18", "lt1970-07-25")), + "claimResponseSearch", + MBI_IGNORE_PATTERNS); + } + /** * Tests to see if a valid response is given when a search is done for {@link ClaimResponse}s * using given mbi and excludeSAMHSA=true, since this does an extra check for samhsa data. @@ -380,4 +399,37 @@ private void verifyResponseMatchesFor( AssertUtils.assertJsonEquals(expected, response, ignorePatterns); } + + /** + * Verifies the ClaimResponse response for the given requestString returns a 200 and the json + * response matches the expected response file. + * + * @param requestString the request string to search with + * @param formParams form parameters + * @param expectedResponseFileName the name of the response file to compare against + * @param ignorePatterns the ignore patterns to use when comparing the result file to the response + */ + private void verifyPostResponseMatchesFor( + String requestString, + Map formParams, + String expectedResponseFileName, + Set ignorePatterns) { + + String response = + given() + .spec(requestAuth) + .formParams(formParams) + .expect() + .statusCode(200) + .when() + .post(requestString) + .then() + .extract() + .response() + .asString(); + + String expected = rdaTestUtils.expectedResponseFor(expectedResponseFileName); + + AssertUtils.assertJsonEquals(expected, response, ignorePatterns); + } } diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProviderTest.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProviderTest.java index f261a925ec..f37e1251b0 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProviderTest.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/ExplanationOfBenefitResourceProviderTest.java @@ -441,7 +441,7 @@ void testFindByPatientWhenInvalidIdExpectException() { InvalidRequestException.class, () -> eobProvider.findByPatient( - patientParam, null, null, null, null, null, null, requestDetails)); + patientParam, null, null, null, null, null, null, null, requestDetails)); } /** @@ -453,7 +453,8 @@ void testFindByPatientWithPageSizeNotProvidedExpectNoPaging() { when(mockQuery.getResultList()).thenReturn(List.of(0)); Bundle response = - eobProvider.findByPatient(patientParam, null, null, null, null, null, null, requestDetails); + eobProvider.findByPatient( + patientParam, null, null, null, null, null, null, null, requestDetails); assertNotNull(response); assertNull(response.getLink(Constants.LINK_NEXT)); @@ -478,7 +479,7 @@ void testFindByPatientWithNegativeStartIndexExpectException() { InvalidRequestException.class, () -> eobProvider.findByPatient( - patientParam, null, null, null, null, null, null, requestDetails)); + patientParam, null, null, null, null, null, null, null, requestDetails)); } /** @@ -492,7 +493,8 @@ void testFindByPatientWhenNoClaimsFoundExpectEmptyBundle() { when(mockQuery.getResultList()).thenReturn(List.of(0)); Bundle response = - eobProvider.findByPatient(patientParam, null, null, null, null, null, null, requestDetails); + eobProvider.findByPatient( + patientParam, null, null, null, null, null, null, null, requestDetails); assertEquals(0, response.getTotal()); } @@ -508,7 +510,7 @@ void testFindByPatientSupportsWildcardClaimType() { Bundle response = eobProvider.findByPatient( - patientParam, listParam, null, null, null, null, null, requestDetails); + patientParam, listParam, null, null, null, null, null, null, requestDetails); assertNotNull(response); assertEquals(0, response.getTotal()); @@ -521,7 +523,8 @@ void testFindByPatientSupportsWildcardClaimType() { @Test void testFindByPatientSupportsNullClaimType() { Bundle response = - eobProvider.findByPatient(patientParam, null, null, null, null, null, null, requestDetails); + eobProvider.findByPatient( + patientParam, null, null, null, null, null, null, null, requestDetails); assertNotNull(response); assertEquals(0, response.getTotal()); diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientE2E.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientE2E.java index 30e17434dc..5c937a43b9 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientE2E.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientE2E.java @@ -772,6 +772,43 @@ private void verifyReadExsistingPatientWithHeaders( .get(requestString); } + /** Tests the pagination links using a POST request with the pagination info in the POST body. */ + @Test + public void testPatientByPartDContractWhenPaginationExpectPagingLinksPost() { + ServerTestUtils.get() + .loadData( + Arrays.asList( + StaticRifResource.SAMPLE_A_BENES, StaticRifResource.SAMPLE_A_BENEFICIARY_HISTORY)); + String contractId = + CCWUtils.calculateVariableReferenceUrl(CcwCodebookVariable.PTDCNTRCT01) + "|S4607"; + String refYear = CCWUtils.calculateVariableReferenceUrl(CcwCodebookVariable.RFRNC_YR) + "|2018"; + Map formParams = + Map.of("_has:Coverage.extension", contractId, "_has:Coverage.rfrncyr", refYear); + String requestString = patientEndpoint + "_search?_count=1"; + + given() + .spec(requestAuth) + .header(PatientResourceProvider.HEADER_NAME_INCLUDE_IDENTIFIERS, "mbi") + .header("Content-Type", "application/x-www-form-urlencoded") + .formParams(formParams) + .expect() + .log() + .body() + .body("resourceType", equalTo("Bundle")) + // Should match the paging size + .body("entry.size()", equalTo(1)) + // Check pagination has the right number of links + .body("link.size()", equalTo(2)) + /* Patient (specifically search by contract) uses different paging + than all other resources, due to using bene id cursors. + There is no "last" page or "previous", only first/next/self + */ + .body("link.relation", hasItems("first", "self")) + .statusCode(200) + .when() + .post(requestString); + } + /** * Gets the hashed HICN based on the passed unhashed HICN value using the loaded data to look for * the corresponding mbi hash. Will pull from the bene history data instead of beneficiary if diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientLinkBuilderTest.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientLinkBuilderTest.java index 80ca33ed37..81f1b4ebac 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientLinkBuilderTest.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientLinkBuilderTest.java @@ -6,26 +6,39 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import gov.cms.bfd.server.war.commons.PatientLinkBuilder; import java.util.Collections; +import java.util.HashMap; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Coverage; import org.hl7.fhir.dstu3.model.ExplanationOfBenefit; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Reference; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** Unit test for the {@link PatientLinkBuilder}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class PatientLinkBuilderTest { /** Contract url for use in pagination testing. */ - public static String TEST_CONTRACT_URL = + public static final String TEST_CONTRACT_URL = "https://localhost:443/v1/fhir/Patient?_has:Coverage.extension=https://bluebutton.cms.gov/resources/variables/ptdcntrct02|S0000"; + /** Request details. */ + @Mock private RequestDetails requestDetails; + /** * Validate that for the base testing contract url no paging was requested, no paging was * returned, we start on the 'first' page, and the page size is set to the maximum value (since @@ -33,7 +46,7 @@ public class PatientLinkBuilderTest { */ @Test public void noCountTest() { - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL); assertFalse(paging.isPagingRequested()); assertTrue(paging.isFirstPage()); @@ -54,7 +67,7 @@ public void noCountTest() { @Test public void missingCountTest() { // Missing _count - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL + "&cursor=999"); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL + "&cursor=999"); assertFalse(paging.isPagingRequested()); assertTrue(paging.isFirstPage()); @@ -72,7 +85,7 @@ public void missingCountTest() { */ @Test public void emptyPageTest() { - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=10"); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL + "&_count=10"); assertTrue(paging.isPagingRequested()); assertTrue(paging.isFirstPage()); @@ -91,7 +104,7 @@ public void emptyPageTest() { */ @Test public void emptyCursorTest() { - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=10&cursor="); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL + "&_count=10&cursor="); assertTrue(paging.isPagingRequested()); assertTrue(paging.isFirstPage()); @@ -105,7 +118,7 @@ public void emptyCursorTest() { */ @Test public void onePageTest() { - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=10"); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL + "&_count=10"); assertTrue(paging.isPagingRequested()); assertTrue(paging.isFirstPage()); @@ -129,7 +142,7 @@ public void onePageTest() { */ @Test public void testMdcLogsInAddResourcesToBundle() { - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=10"); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL + "&_count=10"); assertTrue(paging.isPagingRequested()); assertTrue(paging.isFirstPage()); @@ -183,7 +196,7 @@ public void testMdcLogsInAddResourcesToBundle() { @Test public void fullPageTest() { // test a page with a page size of 1 and 1 patient in the result - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=1"); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL + "&_count=1"); assertTrue(paging.isPagingRequested()); assertTrue(paging.isFirstPage()); @@ -209,7 +222,7 @@ public void fullPageTest() { @Test public void fullPageTestWithPaging() { // test a page with a page size of 1 and 1 patient in the result - PatientLinkBuilder paging = new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=1"); + PatientLinkBuilder paging = configureRequestDetails(TEST_CONTRACT_URL + "&_count=1"); paging = new PatientLinkBuilder(paging, true); assertTrue(paging.isPagingRequested()); @@ -240,7 +253,7 @@ public void testNonNumberPageSizeExpectException() { assertThrows( InvalidRequestException.class, () -> { - new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=abc"); + configureRequestDetails(TEST_CONTRACT_URL + "&_count=abc"); }, "Invalid argument in request URL: _count must be a number."); } @@ -251,7 +264,7 @@ public void testNegativePageSizeExpectException() { assertThrows( InvalidRequestException.class, () -> { - new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=-1"); + configureRequestDetails(TEST_CONTRACT_URL + "&_count=-1"); }, "Value for pageSize cannot be zero or negative: -1"); } @@ -262,7 +275,7 @@ public void testZeroPageSizeExpectException() { assertThrows( InvalidRequestException.class, () -> { - new PatientLinkBuilder(TEST_CONTRACT_URL + "&_count=0"); + configureRequestDetails(TEST_CONTRACT_URL + "&_count=0"); }, "Value for pageSize cannot be zero or negative: 0"); } @@ -273,9 +286,26 @@ public void testOverlyLargePageSizeException() { assertThrows( InvalidRequestException.class, () -> { - new PatientLinkBuilder( + configureRequestDetails( TEST_CONTRACT_URL + "&_count=" + (PatientLinkBuilder.MAX_PAGE_SIZE + 1)); }, "Page size must be less than " + PatientLinkBuilder.MAX_PAGE_SIZE); } + + /** + * Returns a new {@link PatientLinkBuilder} configured from the supplied URI. + * + * @param uri URI + * @return link builder + */ + private PatientLinkBuilder configureRequestDetails(String uri) { + UriComponents components = UriComponentsBuilder.fromUriString(uri).build(); + when(requestDetails.getCompleteUrl()).thenReturn(components.toUriString()); + HashMap params = new HashMap<>(); + for (var param : components.getQueryParams().keySet()) { + params.put(param, components.getQueryParams().get(param).toArray(new String[0])); + } + when(requestDetails.getParameters()).thenReturn(params); + return new PatientLinkBuilder(requestDetails); + } } diff --git a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProviderTest.java b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProviderTest.java index 2ca7fcd88c..e1a2c52c88 100644 --- a/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProviderTest.java +++ b/apps/bfd-server/bfd-server-war/src/test/java/gov/cms/bfd/server/war/stu3/providers/PatientResourceProviderTest.java @@ -261,7 +261,7 @@ public void testSearchByCoverageContractWhereNonNumericContractIdExpectException InvalidRequestException.class, () -> patientProvider.searchByCoverageContract( - contractId, refYear, null, requestDetails)); + contractId, refYear, null, null, requestDetails)); assertEquals( "Failed to parse value for Contract Year as a number.", exception.getLocalizedMessage()); } @@ -281,7 +281,7 @@ public void testSearchByCoverageContractWhereWrongLengthContractIdExpectExceptio InvalidRequestException.class, () -> patientProvider.searchByCoverageContract( - contractId, refYear, null, requestDetails)); + contractId, refYear, null, null, requestDetails)); assertEquals( "Coverage id is not expected length; value 123 is not expected length 5", exception.getLocalizedMessage()); @@ -299,7 +299,7 @@ public void testSearchByLogicalIdWhereBlankIdExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByLogicalId(logicalId, null, null, requestDetails)); + () -> patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails)); assertEquals("Missing required id value", exception.getLocalizedMessage()); } @@ -315,7 +315,7 @@ public void testSearchByLogicalIdWhereNonEmptySystemExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByLogicalId(logicalId, null, null, requestDetails)); + () -> patientProvider.searchByLogicalId(logicalId, null, null, null, requestDetails)); assertEquals( "System is unsupported here and should not be set (system)", exception.getLocalizedMessage()); @@ -334,7 +334,7 @@ public void testSearchByIdentifierIdWhereEmptyHashExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByIdentifier(identifier, null, null, requestDetails)); + () -> patientProvider.searchByIdentifier(identifier, null, null, null, requestDetails)); assertEquals("lookup value cannot be null/empty", exception.getLocalizedMessage()); } @@ -350,7 +350,7 @@ public void testSearchByIdentifierIdWhereBadSystemExpectException() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, - () -> patientProvider.searchByIdentifier(identifier, null, null, requestDetails)); + () -> patientProvider.searchByIdentifier(identifier, null, null, null, requestDetails)); assertEquals("Unsupported identifier system: bad-system", exception.getLocalizedMessage()); } } diff --git a/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v1/metadata.json b/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v1/metadata.json index ec5a39658a..7380568c16 100644 --- a/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v1/metadata.json +++ b/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v1/metadata.json @@ -31,6 +31,10 @@ "name" : "beneficiary", "type" : "reference", "documentation" : "**NOTE: TO MAKE A REQUEST TO THIS ENDPOINT IT IS REQUIRED TO CHOOSE ONE OUT OF THE FOLLOWING THREE PARAMETERS AT A GIVEN TIME (_id, identifier, _has:Coverage.extension)**\n\nFetch _Patient_ data using a FHIR _IdType_ identifier; an IdType\nrepresents the logical identity for a resource, or as much of that\nidentity that is known. In FHIR, every resource must have a _logical ID_ which is\ndefined by the [FHIR specification](https://www.hl7.org/fhir/r4/datatypes.html#id) as:\n\n`Any combination of upper or lower case ASCII letters ('A'..'Z', and 'a'..'z', numerals ('0'..'9'),\n'-' and '.', with a length limit of 64 characters. (This might be an integer, an un-prefixed OID, UUID\nor any other identifier pattern that meets these constraints.)`\n\nThis class contains that logical ID, and can optionally also contain a relative or absolute URL\nrepresenting the resource identity; the following are all valid values for IdType, and all might\nrepresent the same resource:\n - `_id=567834`\n - `_id=1234`" + }, { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" }, { "name" : "_lastUpdated", "type" : "date", @@ -54,6 +58,10 @@ "name" : "patient", "type" : "reference", "documentation" : "**NOTE: TO MAKE A REQUEST TO THIS ENDPOINT IT IS REQUIRED TO CHOOSE ONE OUT OF THE FOLLOWING THREE PARAMETERS AT A GIVEN TIME (_id, identifier, _has:Coverage.extension)**\n\nFetch _Patient_ data using a FHIR _IdType_ identifier; an IdType\nrepresents the logical identity for a resource, or as much of that\nidentity that is known. In FHIR, every resource must have a _logical ID_ which is\ndefined by the [FHIR specification](https://www.hl7.org/fhir/r4/datatypes.html#id) as:\n\n`Any combination of upper or lower case ASCII letters ('A'..'Z', and 'a'..'z', numerals ('0'..'9'),\n'-' and '.', with a length limit of 64 characters. (This might be an integer, an un-prefixed OID, UUID\nor any other identifier pattern that meets these constraints.)`\n\nThis class contains that logical ID, and can optionally also contain a relative or absolute URL\nrepresenting the resource identity; the following are all valid values for IdType, and all might\nrepresent the same resource:\n - `_id=567834`\n - `_id=1234`" + }, { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" }, { "name" : "_lastUpdated", "type" : "date", @@ -98,6 +106,18 @@ "code" : "search-type" } ], "searchParam" : [ { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { "name" : "_has:Coverage", "type" : "token", "documentation" : "**NOTE: TO MAKE A REQUEST TO THIS ENDPOINT IT IS REQUIRED TO CHOOSE ONE OUT OF THE FOLLOWING THREE PARAMETERS AT A GIVEN TIME (_id, identifier, _has:Coverage.extension)**\n\nWhen searching for a Patient's Part D events information, this resource identifies\nthe Part D contract value that will be used when determining eligibility.\n\nExample:\n - `_has:Coverage.extension=`\n - `_has:Coverage.extension=ABCD`" @@ -120,7 +140,7 @@ }, { "name" : "cursor", "type" : "string", - "documentation" : "Provide a pagination cursor or numeric _offset_ for processing Patient's Part D events information.\n\nExamples:\n - `cursor=200` the first record is the 201st record\n - `cursor=1000` the first record is the 1001st record" + "documentation" : "Provides a pagination cursor or numeric _offset_ for processing Patient's Part D events information.\n\nExamples:\n - `cursor=200` the first record is the 201st record\n - `cursor=1000` the first record is the 1001st record" }, { "name" : "identifier", "type" : "token", diff --git a/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v2/metadata.json b/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v2/metadata.json index 12c41ad87a..db3412d5f7 100644 --- a/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v2/metadata.json +++ b/apps/bfd-server/bfd-server-war/src/test/resources/endpoint-responses/v2/metadata.json @@ -33,6 +33,10 @@ "searchInclude" : [ "*", "Claim:mbi" ], "searchRevInclude" : [ "Claim:mbi", "ClaimResponse:mbi", "Coverage:beneficiary", "ExplanationOfBenefit:patient" ], "searchParam" : [ { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { "name" : "_lastUpdated", "type" : "date", "documentation" : "Only satisfy the Search if the Beneficiary's `last_updated` Date falls within a specified _DateRange_.\nA _DateRange_ can be defined by providing less than `lt` and/or greater than `gt` values.\nThis parameter can be included in a request one or more times.\n\nInexact timestamps are accepted, but not recommended, since the input will implicitly be converted to use the server's timezone.\n\nExamples:\n - `_lastUpdated=gt2023-01-02T00:00+00:00&_lastUpdated=lt2023-05-01T00:00+00:00` defines a range between two provided dates\n - `_lastUpdated=gt2023-01-02T00:00+00:00` defines a range between the provided date and today\n - `_lastUpdated=lt2023-05-01T00:00+00:00` defines a range from the earliest available records until the provided date" @@ -76,6 +80,10 @@ "searchInclude" : [ "*", "ClaimResponse:mbi" ], "searchRevInclude" : [ "Claim:mbi", "ClaimResponse:mbi", "Coverage:beneficiary", "ExplanationOfBenefit:patient" ], "searchParam" : [ { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { "name" : "_lastUpdated", "type" : "date", "documentation" : "Only satisfy the Search if the Beneficiary's `last_updated` Date falls within a specified _DateRange_.\nA _DateRange_ can be defined by providing less than `lt` and/or greater than `gt` values.\nThis parameter can be included in a request one or more times.\n\nInexact timestamps are accepted, but not recommended, since the input will implicitly be converted to use the server's timezone.\n\nExamples:\n - `_lastUpdated=gt2023-01-02T00:00+00:00&_lastUpdated=lt2023-05-01T00:00+00:00` defines a range between two provided dates\n - `_lastUpdated=gt2023-01-02T00:00+00:00` defines a range between the provided date and today\n - `_lastUpdated=lt2023-05-01T00:00+00:00` defines a range from the earliest available records until the provided date" @@ -119,6 +127,10 @@ "searchInclude" : [ "*", "Coverage:beneficiary" ], "searchRevInclude" : [ "Claim:mbi", "ClaimResponse:mbi", "Coverage:beneficiary", "ExplanationOfBenefit:patient" ], "searchParam" : [ { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { "name" : "_lastUpdated", "type" : "date", "documentation" : "Only satisfy the Search if the Beneficiary's `last_updated` Date falls within a specified _DateRange_.\nA _DateRange_ can be defined by providing less than `lt` and/or greater than `gt` values.\nThis parameter can be included in a request one or more times.\n\nInexact timestamps are accepted, but not recommended, since the input will implicitly be converted to use the server's timezone.\n\nExamples:\n - `_lastUpdated=gt2023-01-02T00:00+00:00&_lastUpdated=lt2023-05-01T00:00+00:00` defines a range between two provided dates\n - `_lastUpdated=gt2023-01-02T00:00+00:00` defines a range between the provided date and today\n - `_lastUpdated=lt2023-05-01T00:00+00:00` defines a range from the earliest available records until the provided date" @@ -146,6 +158,10 @@ "searchInclude" : [ "*", "ExplanationOfBenefit:patient" ], "searchRevInclude" : [ "Claim:mbi", "ClaimResponse:mbi", "Coverage:beneficiary", "ExplanationOfBenefit:patient" ], "searchParam" : [ { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { "name" : "_lastUpdated", "type" : "date", "documentation" : "Only satisfy the Search if the Beneficiary's `last_updated` Date falls within a specified _DateRange_.\nA _DateRange_ can be defined by providing less than `lt` and/or greater than `gt` values.\nThis parameter can be included in a request one or more times.\n\nInexact timestamps are accepted, but not recommended, since the input will implicitly be converted to use the server's timezone.\n\nExamples:\n - `_lastUpdated=gt2023-01-02T00:00+00:00&_lastUpdated=lt2023-05-01T00:00+00:00` defines a range between two provided dates\n - `_lastUpdated=gt2023-01-02T00:00+00:00` defines a range between the provided date and today\n - `_lastUpdated=lt2023-05-01T00:00+00:00` defines a range from the earliest available records until the provided date" @@ -193,6 +209,10 @@ "searchInclude" : [ "*" ], "searchRevInclude" : [ "Claim:mbi", "ClaimResponse:mbi", "Coverage:beneficiary", "ExplanationOfBenefit:patient" ], "searchParam" : [ { + "name" : "_count", + "type" : "string", + "documentation" : "Provides the number of records to be used for pagination.\n\nExamples:\n - `_count=10`: return 10 values.\n" + }, { "name" : "_has:Coverage.extension", "type" : "token", "documentation" : "**NOTE: TO MAKE A REQUEST TO THIS ENDPOINT IT IS REQUIRED TO CHOOSE ONE OUT OF THE FOLLOWING THREE PARAMETERS AT A GIVEN TIME (_id, identifier, _has:Coverage.extension)**\n\nWhen searching for a Patient's Part D events information, this resource identifies\nthe Part D contract value that will be used when determining eligibility.\n\nExample:\n - `_has:Coverage.extension=`\n - `_has:Coverage.extension=ABCD`" @@ -211,7 +231,7 @@ }, { "name" : "cursor", "type" : "string", - "documentation" : "Provide a pagination cursor or numeric _offset_ for processing Patient's Part D events information.\n\nExamples:\n - `cursor=200` the first record is the 201st record\n - `cursor=1000` the first record is the 1001st record" + "documentation" : "Provides a pagination cursor or numeric _offset_ for processing Patient's Part D events information.\n\nExamples:\n - `cursor=200` the first record is the 201st record\n - `cursor=1000` the first record is the 1001st record" }, { "name" : "identifier", "type" : "token", diff --git a/apps/bfd-server/bfd-server-war/src/test/resources/openapi/posts.yaml b/apps/bfd-server/bfd-server-war/src/test/resources/openapi/posts.yaml index 74a4d5c5b3..296633173a 100644 --- a/apps/bfd-server/bfd-server-war/src/test/resources/openapi/posts.yaml +++ b/apps/bfd-server/bfd-server-war/src/test/resources/openapi/posts.yaml @@ -88,6 +88,13 @@ paths: - `startIndex=100` format: int32 example: 100 + '_count': + description: |- + Provides the number of records to be used for pagination. + + Examples: + - `_count=10`: return 10 values. + format: int32 type: type: string description: |- @@ -184,6 +191,13 @@ paths: - `startIndex=100` format: int32 example: 100 + '_count': + description: |- + Provides the number of records to be used for pagination. + + Examples: + - `_count=10`: return 10 values. + format: int32 identifier: type: string description: |- @@ -300,6 +314,13 @@ paths: - `startIndex=100` format: int32 example: 100 + '_count': + description: |- + Provides the number of records to be used for pagination. + + Examples: + - `_count=10`: return 10 values. + format: int32 identifier: type: string description: |- @@ -453,6 +474,13 @@ paths: Example: - `startIndex=100` example: 100 + '_count': + description: |- + Provides the number of records to be used for pagination. + + Examples: + - `_count=10`: return 10 values. + format: int32 type: type: string description: |- @@ -571,6 +599,13 @@ paths: Example: - `startIndex=100` example: 100 + '_count': + description: |- + Provides the number of records to be used for pagination. + + Examples: + - `_count=10`: return 10 values. + format: int32 type: type: string description: |-