wpi merge operation provider

This commit is contained in:
Emre Dincturk 2024-11-26 09:28:10 -05:00
parent 054f7e6678
commit 7979f037e9
7 changed files with 524 additions and 18 deletions

View File

@ -19,8 +19,11 @@
*/
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient;
import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters;
import ca.uhn.fhir.jpa.dao.merge.MergeOperationParameters;
import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.primitive.IdDt;
@ -39,12 +42,25 @@ import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.CanonicalIdentifier;
import ca.uhn.fhir.util.IdentifierUtil;
import ca.uhn.fhir.util.ParametersUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -240,6 +256,90 @@ public abstract class BaseJpaResourceProviderPatient<T extends IBaseResource> ex
}
}
/**
* /Patient/$merge
*/
@Operation(
name = ProviderConstants.OPERATION_MERGE,
canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge")
public void patientMerge(
HttpServletRequest theServletRequest,
HttpServletResponse theServletResponse,
ServletRequestDetails theRequestDetails,
@OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER)
List<Identifier> theSourcePatientIdentifier,
@OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER)
List<Identifier> theTargetPatientIdentifier,
@OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1)
IBaseReference theSourcePatient,
@OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1)
IBaseReference theTargetPatient,
@OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1)
IPrimitiveType<Boolean> thePreview,
@OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1)
IBaseResource theResultPatient)
throws IOException {
startRequest(theServletRequest);
try {
MergeOperationParameters mergeOperationParameters = createMergeOperationParameters(
theSourcePatientIdentifier,
theTargetPatientIdentifier,
theSourcePatient,
theTargetPatient,
thePreview,
theResultPatient);
IFhirResourceDaoPatient<?> dao = (IFhirResourceDaoPatient<?>) getDao();
ResourceMergeService resourceMergeService = new ResourceMergeService(dao);
FhirContext fhirContext = dao.getContext();
ResourceMergeService.MergeOutcome mergeOutcome =
resourceMergeService.merge(mergeOperationParameters, theRequestDetails);
IBaseParameters retVal = ParametersUtil.newInstance(fhirContext);
ParametersUtil.addParameterToParameters(fhirContext, retVal, "outcome", mergeOutcome.getOperationOutcome());
theServletResponse.setStatus(mergeOutcome.getHttpStatusCode());
// we are writing the response to directly, otherwise the response status we set above is ignored.
fhirContext
.newJsonParser()
.setPrettyPrint(true)
.encodeResourceToWriter(retVal, theServletResponse.getWriter());
theServletResponse.getWriter().close();
} finally {
endRequest(theServletRequest);
}
}
private MergeOperationParameters createMergeOperationParameters(
List<Identifier> theSourcePatientIdentifier,
List<Identifier> theTargetPatientIdentifier,
IBaseReference theSourcePatient,
IBaseReference theTargetPatient,
IPrimitiveType<Boolean> thePreview,
IBaseResource theResultPatient) {
MergeOperationParameters mergeOperationParameters = new MergeOperationParameters();
if (theSourcePatientIdentifier != null) {
List<CanonicalIdentifier> sourceResourceIdentifiers = theSourcePatientIdentifier.stream()
.map(IdentifierUtil::identifierDtFromIdentifier)
.collect(Collectors.toList());
mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers);
}
if (theTargetPatientIdentifier != null) {
List<CanonicalIdentifier> targetResourceIdentifiers = theTargetPatientIdentifier.stream()
.map(IdentifierUtil::identifierDtFromIdentifier)
.collect(Collectors.toList());
mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers);
}
mergeOperationParameters.setSourceResource(theSourcePatient);
mergeOperationParameters.setTargetResource(theTargetPatient);
mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue());
mergeOperationParameters.setResultPatient((Patient) theResultPatient);
return mergeOperationParameters;
}
/**
* Given a list of string types, return only the ID portions of any parameters passed in.
*/

View File

@ -22,7 +22,6 @@ package ca.uhn.fhir.mdm.util;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.CanonicalIdentifier;
import org.hl7.fhir.instance.model.api.IBase;
@ -31,23 +30,7 @@ public final class IdentifierUtil {
private IdentifierUtil() {}
public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) {
CanonicalIdentifier retval = new CanonicalIdentifier();
// TODO add other fields like "use" etc
if (theIdentifier instanceof org.hl7.fhir.dstu3.model.Identifier) {
org.hl7.fhir.dstu3.model.Identifier ident = (org.hl7.fhir.dstu3.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else if (theIdentifier instanceof org.hl7.fhir.r4.model.Identifier) {
org.hl7.fhir.r4.model.Identifier ident = (org.hl7.fhir.r4.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else if (theIdentifier instanceof org.hl7.fhir.r5.model.Identifier) {
org.hl7.fhir.r5.model.Identifier ident = (org.hl7.fhir.r5.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else {
throw new InternalErrorException(Msg.code(1486) + "Expected 'Identifier' type but was '"
+ theIdentifier.getClass().getName() + "'");
}
return retval;
return ca.uhn.fhir.util.IdentifierUtil.identifierDtFromIdentifier(theIdentifier);
}
/**

View File

@ -252,6 +252,7 @@ public class ProviderConstants {
* Patient $merge operation parameters
*/
public static final String OPERATION_MERGE_SOURCE_PATIENT = "source-patient";
public static final String OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER = "source-patient-identifier";
public static final String OPERATION_MERGE_TARGET_PATIENT = "target-patient";
public static final String OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier";

View File

@ -0,0 +1,94 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.dao.merge;
import ca.uhn.fhir.util.CanonicalIdentifier;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.r4.model.Patient;
import java.util.List;
public class MergeOperationParameters {
private List<CanonicalIdentifier> mySourceResourceIdentifiers;
private List<CanonicalIdentifier> myTargetResourceIdentifiers;
private IBaseReference mySourceResource;
private IBaseReference myTargetResource;
private boolean myPreview;
// TODO: this can be changed to a generic resource to support other resources
private Patient myResultResource;
public List<CanonicalIdentifier> getSourceIdentifiers() {
return mySourceResourceIdentifiers;
}
public boolean hasAtLeastOneSourceIdentifier() {
return mySourceResourceIdentifiers != null && !mySourceResourceIdentifiers.isEmpty();
}
public void setSourceResourceIdentifiers(List<CanonicalIdentifier> theSourceIdentifiers) {
this.mySourceResourceIdentifiers = theSourceIdentifiers;
}
public List<CanonicalIdentifier> getTargetIdentifiers() {
return myTargetResourceIdentifiers;
}
public boolean hasAtLeastOneTargetIdentifier() {
return myTargetResourceIdentifiers != null && !myTargetResourceIdentifiers.isEmpty();
}
public void setTargetResourceIdentifiers(List<CanonicalIdentifier> theTargetIdentifiers) {
this.myTargetResourceIdentifiers = theTargetIdentifiers;
}
public boolean isPreview() {
return myPreview;
}
public void setPreview(boolean thePreview) {
this.myPreview = thePreview;
}
public Patient getResultPatient() {
return myResultResource;
}
public void setResultPatient(Patient theResultPatient) {
this.myResultResource = theResultPatient;
}
public IBaseReference getSourceResource() {
return mySourceResource;
}
public void setSourceResource(IBaseReference theSourceResource) {
this.mySourceResource = theSourceResource;
}
public IBaseReference getTargetResource() {
return myTargetResource;
}
public void setTargetResource(IBaseReference theTargetResource) {
this.myTargetResource = theTargetResource;
}
}

View File

@ -0,0 +1,130 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.dao.merge;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import java.util.ArrayList;
import java.util.List;
import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST;
public class ResourceMergeService {
IFhirResourceDaoPatient<?> myDao;
FhirContext myFhirContext;
public ResourceMergeService(IFhirResourceDaoPatient<?> thePatientDao) {
myDao = thePatientDao;
myFhirContext = myDao.getContext();
}
/**
* Implemention of the $merge operation for resources
* @param theMergeOperationParameters the merge operation parameters
* @param theRequestDetails the request details
* @return the merge outcome containing OperationOutcome and HTTP status code
*/
public MergeOutcome merge(MergeOperationParameters theMergeOperationParameters, RequestDetails theRequestDetails) {
MergeOutcome mergeOutcome = new MergeOutcome();
IBaseOperationOutcome outcome = OperationOutcomeUtil.newInstance(myFhirContext);
mergeOutcome.setOperationOutcome(outcome);
if (!validateMergeOperationParameters(theMergeOperationParameters, outcome)) {
mergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST);
return mergeOutcome;
}
return mergeOutcome;
}
/**
* Validates the merge operation parameters and adds validation errors to the outcome
* @param theMergeOperationParameters the merge operation parameters
* @param theOutcome the outcome to add validation errors to
* @return true if the parameters are valid, false otherwise
*/
private boolean validateMergeOperationParameters(
MergeOperationParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) {
List<String> errorMessages = new ArrayList<>();
if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier()
&& theMergeOperationParameters.getSourceResource() == null) {
errorMessages.add("There are no source resource parameters provided, include either a source-patient, "
+ "source-patient-identifier parameter.");
}
// Spec has conflicting information about this case
if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier()
&& theMergeOperationParameters.getSourceResource() != null) {
errorMessages.add(
"Source patient must be provided either by source-patient-identifier or by source-resource, not both.");
}
if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier()
&& theMergeOperationParameters.getTargetResource() == null) {
errorMessages.add("There are no target resource parameters provided, include either a target-patient, "
+ "target-patient-identifier parameter.");
}
// Spec has conflicting information about this case
if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier()
&& theMergeOperationParameters.getTargetResource() != null) {
errorMessages.add("Target patient must be provided either by target-patient-identifier or by "
+ "target-resource, not both.");
}
if (!errorMessages.isEmpty()) {
for (String validationError : errorMessages) {
OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", validationError, null, null);
}
// there are validation errors
return false;
}
// no validation errors
return true;
}
public static class MergeOutcome {
private IBaseOperationOutcome myOperationOutcome;
private int myHttpStatusCode;
public IBaseOperationOutcome getOperationOutcome() {
return myOperationOutcome;
}
public void setOperationOutcome(IBaseOperationOutcome theOperationOutcome) {
this.myOperationOutcome = theOperationOutcome;
}
public int getHttpStatusCode() {
return myHttpStatusCode;
}
public void setHttpStatusCode(int theHttpStatusCode) {
this.myHttpStatusCode = theHttpStatusCode;
}
}
}

View File

@ -0,0 +1,49 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.util;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IBase;
public final class IdentifierUtil {
private IdentifierUtil() {}
public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) {
CanonicalIdentifier retval = new CanonicalIdentifier();
// TODO add other fields like "use" etc
if (theIdentifier instanceof org.hl7.fhir.dstu3.model.Identifier) {
org.hl7.fhir.dstu3.model.Identifier ident = (org.hl7.fhir.dstu3.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else if (theIdentifier instanceof org.hl7.fhir.r4.model.Identifier) {
org.hl7.fhir.r4.model.Identifier ident = (org.hl7.fhir.r4.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else if (theIdentifier instanceof org.hl7.fhir.r5.model.Identifier) {
org.hl7.fhir.r5.model.Identifier ident = (org.hl7.fhir.r5.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else {
throw new InternalErrorException(Msg.code(1486) + "Expected 'Identifier' type but was '"
+ theIdentifier.getClass().getName() + "'");
}
return retval;
}
}

View File

@ -0,0 +1,149 @@
package ca.uhn.fhir.jpa.dao.merge;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.CanonicalIdentifier;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class ResourceMergeServiceTest {
@Mock
private IFhirResourceDaoPatient<?> myDaoMock;
@Mock
RequestDetails myRequestDetailsMock;
private ResourceMergeService myResourceMergeService;
private final FhirContext myFhirContext = FhirContext.forR4Cached();
@BeforeEach
void setup() {
when(myDaoMock.getContext()).thenReturn(myFhirContext);
myResourceMergeService = new ResourceMergeService(myDaoMock);
}
@Test
void testValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorInOutcomeWith400Status() {
// Given
MergeOperationParameters mergeOperationParameters = new MergeOperationParameters();
mergeOperationParameters.setTargetResource(new Reference("Patient/123"));
// When
ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock);
// Then
OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome();
assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400);
assertThat(operationOutcome.getIssue()).hasSize(1);
assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR);
assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("There are no source resource parameters provided, include either a source-patient, " +
"source-patient-identifier parameter.");
verifyNoMoreInteractions(myDaoMock);
}
@Test
void testValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorInOutcomeWith400Status() {
// Given
MergeOperationParameters mergeOperationParameters = new MergeOperationParameters();
mergeOperationParameters.setSourceResource(new Reference("Patient/123"));
// When
ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock);
// Then
OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome();
assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400);
assertThat(operationOutcome.getIssue()).hasSize(1);
assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR);
assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("There are no target resource " +
"parameters provided, include either a target-patient, target-patient-identifier parameter.");
verifyNoMoreInteractions(myDaoMock);
}
@Test
void testValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsInOutcomeWith400Status() {
// Given
MergeOperationParameters mergeOperationParameters = new MergeOperationParameters();
// When
ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock);
// Then
OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome();
assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400);
assertThat(operationOutcome.getIssue()).hasSize(2);
assertThat(operationOutcome.getIssue().get(0).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR);
assertThat(operationOutcome.getIssue().get(0).getDiagnostics()).contains("There are no source resource " +
"parameters provided, include either a source-patient, source-patient-identifier parameter.");
assertThat(operationOutcome.getIssue().get(1).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR);
assertThat(operationOutcome.getIssue().get(1).getDiagnostics()).contains("There are no target resource " +
"parameters provided, include either a target-patient, target-patient-identifier parameter.");
verifyNoMoreInteractions(myDaoMock);
}
@Test
void testValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() {
// Given
MergeOperationParameters mergeOperationParameters = new MergeOperationParameters();
mergeOperationParameters.setSourceResource(new Reference("Patient/123"));
mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val")));
mergeOperationParameters.setTargetResource(new Reference("Patient/345"));
// When
ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock);
// Then
OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome();
assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400);
assertThat(operationOutcome.getIssue()).hasSize(1);
assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR);
assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("Source patient must be provided " +
"either by source-patient-identifier or by source-resource, not both.");
verifyNoMoreInteractions(myDaoMock);
}
@Test
void testValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() {
// Given
MergeOperationParameters mergeOperationParameters = new MergeOperationParameters();
mergeOperationParameters.setTargetResource(new Reference("Patient/123"));
mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val")));
mergeOperationParameters.setSourceResource(new Reference("Patient/345"));
// When
ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock);
// Then
OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome();
assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400);
assertThat(operationOutcome.getIssue()).hasSize(1);
assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR);
assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("Target patient must be provided " +
"either by target-patient-identifier or by target-resource, not both.");
verifyNoMoreInteractions(myDaoMock);
}
}