diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/EndpointResourceValidationR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/EndpointResourceValidationR4Test.java new file mode 100644 index 00000000000..7ba6a435509 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/EndpointResourceValidationR4Test.java @@ -0,0 +1,171 @@ +package ca.uhn.fhir.jpa.validation; + +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.validation.ResultSeverityEnum; +import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; +import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.r4.model.Meta; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nonnull; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +public class EndpointResourceValidationR4Test extends BaseResourceProviderR4Test { + private static final String myResourceType = "Patient"; + private final String myProfile = "http://example.org/fhir/StructureDefinition/TestPatient"; + private PrePopulatedValidationSupport myPrePopulatedValidationSupport; + + @BeforeEach + public void before() { + CachingValidationSupport myValidationSupport = createCachingValidationSupport(); + FhirInstanceValidator fhirInstanceValidator = new FhirInstanceValidator(myValidationSupport); + RequestValidatingInterceptor interceptor = new RequestValidatingInterceptor(); + interceptor.addValidatorModule(fhirInstanceValidator); + interceptor.setFailOnSeverity(ResultSeverityEnum.ERROR); + interceptor.setAddResponseHeaderOnSeverity(ResultSeverityEnum.INFORMATION); + myServer.registerInterceptor(interceptor); + } + + @Nonnull + private CachingValidationSupport createCachingValidationSupport() { + myPrePopulatedValidationSupport = new PrePopulatedValidationSupport(myFhirContext); + ValidationSupportChain chain = new ValidationSupportChain( + new DefaultProfileValidationSupport(myFhirContext), + new SnapshotGeneratingValidationSupport(myFhirContext), + new CommonCodeSystemsTerminologyService(myFhirContext), + new InMemoryTerminologyServerValidationSupport(myFhirContext), + myPrePopulatedValidationSupport); + return new CachingValidationSupport(chain, true); + } + + @Test + public void testCreatePatientRequest_withProfileNotRegistered_unknownProfile() { + createProfile(myProfile, "1", "Patient.identifier"); + + final Patient patient = new Patient(); + patient.setMeta(new Meta().addProfile(myProfile)); + + try { + myClient.create().resource(patient).execute(); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: Profile reference '" + myProfile + "' has not been checked because it is unknown", e.getMessage()); + } + } + + @Test + public void testCreatePatientRequest_withProfileNoVersion_throwsExceptionWithLatestVersion() { + createAndRegisterProfile("1", "Patient.identifier"); + createAndRegisterProfile("2", "Patient.name"); + + final Patient patient = new Patient(); + patient.setMeta(new Meta().addProfile(myProfile)); + + try { + myClient.create().resource(patient).execute(); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: Patient.name: minimum required = 1, but only found 0 (from " + myProfile + "|2)", e.getMessage()); + } + } + + @Test + public void testCreatePatientRequest_withProfileWithVersion_throwsExceptionWithSpecifiedVersion() { + createAndRegisterProfile("1", "Patient.identifier"); + createAndRegisterProfile("2", "Patient.name"); + + final Patient patient = new Patient(); + patient.setMeta(new Meta().addProfile(myProfile + "|1")); + + try { + myClient.create().resource(patient).execute(); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: Patient.identifier: minimum required = 1, but only found 0 (from " + myProfile + "|1)", e.getMessage()); + } + } + + @Test + public void testCreatePatientRequest_withMultipleProfiles_throwsExceptionWithFirstDeclaredProfile() { + final String sdIdentifier = myProfile + "-identifier"; + final String sdName = myProfile + "-name"; + + createAndRegisterProfile(sdIdentifier, "1", "Patient.identifier"); + createAndRegisterProfile(sdName, "1", "Patient.name"); + + final Patient patient = new Patient(); + patient.setMeta(new Meta().addProfile(sdIdentifier).addProfile(sdName)); + + try { + myClient.create().resource(patient).execute(); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: Patient.identifier: minimum required = 1, but only found 0 (from " + sdIdentifier + "|1)", e.getMessage()); + } + } + + @Test + public void testCreatePatientRequest_withMultipleVersions_throwsExceptionWithFirstDeclaredProfile() { + createAndRegisterProfile(myProfile, "1", "Patient.identifier"); + createAndRegisterProfile(myProfile, "2", "Patient.name"); + createAndRegisterProfile(myProfile, "3", "Patient.birthDate"); + + final Patient patient = new Patient(); + patient.setMeta(new Meta() + .addProfile(myProfile + "|2") + .addProfile(myProfile + "|1") + .addProfile(myProfile + "|3")); + + try { + myClient.create().resource(patient).execute(); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: Patient.name: minimum required = 1, but only found 0 (from " + myProfile + "|2)", e.getMessage()); + } + } + + private void createAndRegisterProfile(String theVersion, String thePath) { + createAndRegisterProfile(myProfile, theVersion, thePath); + } + + private void createAndRegisterProfile(String theUrl, String theVersion, String thePath) { + StructureDefinition sd = createProfile(theUrl, theVersion, thePath); + myPrePopulatedValidationSupport.addStructureDefinition(sd); + } + + private StructureDefinition createProfile(String theUrl, String theVersion, String thePath) { + final String baseProfile = "http://hl7.org/fhir/StructureDefinition/Patient"; + final String profileId = "TestProfile"; + + StructureDefinition sd = new StructureDefinition() + .setUrl(theUrl).setVersion(theVersion) + .setBaseDefinition(baseProfile) + .setType(myResourceType) + .setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setId(profileId); + sd.getDifferential().addElement() + .setPath(thePath) + .setMin(1) + .setId(thePath); + + DaoMethodOutcome outcome = myStructureDefinitionDao.update(sd, new SystemRequestDetails()); + assertNotNull(outcome.getResource()); + return (StructureDefinition) outcome.getResource(); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/OnDemandResourceValidationR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/OnDemandResourceValidationR4Test.java new file mode 100644 index 00000000000..d3c15c08371 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/OnDemandResourceValidationR4Test.java @@ -0,0 +1,180 @@ +package ca.uhn.fhir.jpa.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.SingleValidationMessage; +import ca.uhn.fhir.validation.ValidationResult; +import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; +import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class OnDemandResourceValidationR4Test extends BaseResourceProviderR4Test { + private static final FhirContext ourCtx = FhirContext.forR4(); + private static final String myResourceType = "Patient"; + private final String myProfile = "http://example.org/fhir/StructureDefinition/TestPatient"; + + @Test + public void testCreateStructureDefinition_createMultipleWithWithSameUrl_isStoredSuccessfully() { + createProfile("1", "Patient.identifier"); + createProfile("2", "Patient.name"); + createProfile("3", "Patient.birthdate"); + } + + private static FhirValidator myFhirValidator; + private static PrePopulatedValidationSupport ourValidationSupport; + + @BeforeEach + public void before() { + myFhirValidator = ourCtx.newValidator(); + myFhirValidator.setValidateAgainstStandardSchema(false); + myFhirValidator.setValidateAgainstStandardSchematron(false); + + ourValidationSupport = new PrePopulatedValidationSupport(ourCtx); + + ValidationSupportChain chain = new ValidationSupportChain( + new DefaultProfileValidationSupport(ourCtx), + new SnapshotGeneratingValidationSupport(ourCtx), + new CommonCodeSystemsTerminologyService(ourCtx), + new InMemoryTerminologyServerValidationSupport(ourCtx), + ourValidationSupport); + CachingValidationSupport myValidationSupport = new CachingValidationSupport(chain, true); + FhirInstanceValidator myInstanceVal = new FhirInstanceValidator(myValidationSupport); + myFhirValidator.registerValidatorModule(myInstanceVal); + } + + @Test + public void testValidatePatient_withProfileNotRegistered_unknownProfile() { + createProfile(myProfile, "1", "Patient.identifier"); + + IIdType idType = createPatient(withProfile(myProfile)); + Patient patient = myPatientDao.read(idType, mySrd); + + ValidationResult validationResult = myFhirValidator.validateWithResult(patient); + assertFalse(validationResult.isSuccessful()); + assertEquals(1, validationResult.getMessages().size()); + + SingleValidationMessage message = validationResult.getMessages().iterator().next(); + assertEquals("Profile reference '" + myProfile + "' has not been checked because it is unknown", message.getMessage()); + } + + @Test + public void testValidatePatient_withProfileNoVersion_shouldUsesLatestVersion() { + createAndRegisterProfile(myProfile, "1", "Patient.identifier"); + createAndRegisterProfile(myProfile, "2", "Patient.name"); + + IIdType idType = createPatient(withProfile(myProfile)); + Patient patient = myPatientDao.read(idType, mySrd); + + ValidationResult validationResult = myFhirValidator.validateWithResult(patient); + assertFalse(validationResult.isSuccessful()); + assertEquals(1, validationResult.getMessages().size()); + + SingleValidationMessage message = validationResult.getMessages().iterator().next(); + assertEquals("Patient.name: minimum required = 1, but only found 0 (from " + myProfile + "|2)", message.getMessage()); + } + + @Test + public void testValidatePatient_withProfileWithVersion_shouldUseSpecifiedVersion() { + + createAndRegisterProfile(myProfile, "1", "Patient.identifier"); + createAndRegisterProfile(myProfile, "2", "Patient.name"); + + IIdType idType = createPatient(withProfile(myProfile + "|1")); + Patient patient = myPatientDao.read(idType, mySrd); + + ValidationResult validationResult = myFhirValidator.validateWithResult(patient); + assertFalse(validationResult.isSuccessful()); + assertEquals(1, validationResult.getMessages().size()); + + SingleValidationMessage message = validationResult.getMessages().iterator().next(); + assertEquals("Patient.identifier: minimum required = 1, but only found 0 (from " + myProfile + "|1)", message.getMessage()); + } + + @Test + public void testValidatePatient_withMultipleProfiles_validatesAgainstAllProfiles() { + final String sdIdentifier = myProfile + "-identifier"; + final String sdName = myProfile + "-name"; + + createAndRegisterProfile(sdIdentifier, "1", "Patient.identifier"); + createAndRegisterProfile(sdName, "1", "Patient.name"); + + IIdType idType = createPatient(withProfile(sdIdentifier), withProfile(sdName)); + Patient patient = myPatientDao.read(idType, mySrd); + + ValidationResult validationResult = myFhirValidator.validateWithResult(patient); + assertFalse(validationResult.isSuccessful()); + assertEquals(2, validationResult.getMessages().size()); + + Iterator messageIterator = validationResult.getMessages().iterator(); + assertEquals("Patient.identifier: minimum required = 1, but only found 0 (from " + myProfile + "-identifier|1)", messageIterator.next().getMessage()); + assertEquals("Patient.name: minimum required = 1, but only found 0 (from " + myProfile + "-name|1)", messageIterator.next().getMessage()); + } + + @Test + public void testValidatePatient_withMultipleVersions_validatesAgainstFirstVersion() { + createAndRegisterProfile(myProfile, "1", "Patient.identifier"); + createAndRegisterProfile(myProfile, "2", "Patient.name"); + createAndRegisterProfile(myProfile, "3", "Patient.birthDate"); + + IIdType idType = createPatient( + withProfile(myProfile + "|2"), + withProfile(myProfile + "|1"), + withProfile(myProfile + "|3")); + Patient patient = myPatientDao.read(idType, mySrd); + + ValidationResult validationResult = myFhirValidator.validateWithResult(patient); + assertFalse(validationResult.isSuccessful()); + assertEquals(1, validationResult.getMessages().size()); + + SingleValidationMessage message = validationResult.getMessages().iterator().next(); + assertEquals("Patient.identifier: minimum required = 1, but only found 0 (from " + myProfile + "|1)", message.getMessage()); + } + + private void createAndRegisterProfile(String theUrl, String theVersion, String thePath) { + StructureDefinition sd = createProfile(theUrl, theVersion, thePath); + ourValidationSupport.addStructureDefinition(sd); + } + + private void createProfile(String theVersion, String theRequiredPath) { + createProfile(myProfile, theVersion, theRequiredPath); + } + + private StructureDefinition createProfile(String theUrl, String theVersion, String theRequiredPath) { + final String myResourceTypeDefinition = "http://hl7.org/fhir/StructureDefinition/Patient"; + final String myProfileId = "TestProfile"; + + StructureDefinition sd = new StructureDefinition() + .setUrl(theUrl).setVersion(theVersion) + .setBaseDefinition(myResourceTypeDefinition) + .setType(myResourceType) + .setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setId(myProfileId); + sd.getDifferential().addElement() + .setPath(theRequiredPath) + .setMin(1) + .setId(theRequiredPath); + + DaoMethodOutcome outcome = myStructureDefinitionDao.update(sd, mySrd); + assertNotNull(outcome.getResource()); + return (StructureDefinition) outcome.getResource(); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/RepositoryValidationOnCreateResourceR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/RepositoryValidationOnCreateResourceR4Test.java new file mode 100644 index 00000000000..e601f16a8f4 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/RepositoryValidationOnCreateResourceR4Test.java @@ -0,0 +1,153 @@ +package ca.uhn.fhir.jpa.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.interceptor.validation.IRepositoryValidatingRule; +import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor; +import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +public class RepositoryValidationOnCreateResourceR4Test extends BaseResourceProviderR4Test { + private static final String myResourceType = "Patient"; + private final String myProfile = "http://example.org/fhir/StructureDefinition/TestPatient"; + + private static final FhirContext ourCtx = FhirContext.forR4(); + private final RepositoryValidatingInterceptor myRepositoryValidatingInterceptor = new RepositoryValidatingInterceptor(ourCtx, Collections.emptyList()); + + @BeforeEach + public void before() { + myInterceptorRegistry.registerInterceptor(myRepositoryValidatingInterceptor); + } + + @AfterEach + public void after() { + myInterceptorRegistry.unregisterInterceptor(myRepositoryValidatingInterceptor); + } + + @Test + public void testCreatePatient_withValidationRuleNoVersion_validatesAgainstLatestVersion() { + createProfile("1", "Patient.identifier"); + createProfile("2", "Patient.name"); + + setupRepositoryValidationRules(myProfile); + + try { + createPatient(withProfile(myProfile)); + fail(); + } catch (PreconditionFailedException e) { + assertEquals(Msg.code(574) + + "Patient.name: minimum required = 1, but only found 0 (from " + myProfile + "|2)", e.getMessage()); + } + } + + @Test + public void testCreatePatient_withValidationRuleWithVersion_unknownProfile() { + createProfile("1", "Patient.identifier"); + createProfile("2", "Patient.name"); + + setupRepositoryValidationRules(myProfile + "|1"); + + try { + createPatient(withProfile(myProfile + "|1")); + fail(); + } catch (PreconditionFailedException e) { + assertEquals(Msg.code(574) + "Profile reference '" + myProfile + + "|1' has not been checked because it is unknown, and the validator is set to not fetch unknown profiles", e.getMessage()); + } + } + + @Test + public void testCreatePatient_withProfileWithVersion_doesNotConform() { + setupRepositoryValidationRules(myProfile); + + try { + createPatient(withProfile(myProfile + "|1")); + fail(); + } catch (PreconditionFailedException e) { + assertEquals(Msg.code(575) + "Resource of type \"" + myResourceType + + "\" does not declare conformance to profile from: [" + myProfile + "]", e.getMessage()); + } + } + + @Test + public void testCreatePatient_withValidationRuleWithVersion_doesNotConform() { + setupRepositoryValidationRules(myProfile + "|1"); + try { + createPatient(withProfile(myProfile)); + fail(); + } catch (PreconditionFailedException e) { + assertEquals(Msg.code(575) + "Resource of type \"" + myResourceType + + "\" does not declare conformance to profile from: [" + myProfile + "|1]", e.getMessage()); + } + } + + @Test + public void testCreatePatient_withMultipleProfiles_validatesAgainstLastProfile() { + final String sdIdentifier = myProfile + "-identifier"; + final String sdName = myProfile + "-name"; + + createProfile(sdIdentifier, "1", "Patient.identifier"); + createProfile(sdName, "1", "Patient.name"); + + setupRepositoryValidationRules(sdIdentifier, sdName); + try { + createPatient(withProfile(sdIdentifier), withProfile(sdName)); + fail(); + } catch (PreconditionFailedException e) { + assertEquals(Msg.code(574) + + "Patient.name: minimum required = 1, but only found 0 (from " + sdName + "|1)", e.getMessage()); + } + } + + private void setupRepositoryValidationRules(String... theProfiles) { + List rules = myAppCtx + .getBean(RepositoryValidatingRuleBuilder.REPOSITORY_VALIDATING_RULE_BUILDER, RepositoryValidatingRuleBuilder.class) + .forResourcesOfType(myResourceType) + .requireAtLeastOneProfileOf(theProfiles) + .and() + .requireValidationToDeclaredProfiles() + .withBestPracticeWarningLevel(BestPracticeWarningLevel.Ignore) + .rejectOnSeverity("error") + .build(); + + myRepositoryValidatingInterceptor.setRules(rules); + } + + private void createProfile(String theVersion, String theRequiredPath) { + createProfile(myProfile, theVersion, theRequiredPath); + } + + private void createProfile(String theUrl, String theVersion, String thePath) { + final String baseProfile = "http://hl7.org/fhir/StructureDefinition/Patient"; + final String profileId = "TestProfile"; + + StructureDefinition sd = new StructureDefinition() + .setUrl(theUrl).setVersion(theVersion) + .setBaseDefinition(baseProfile) + .setType(myResourceType) + .setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setId(profileId); + sd.getDifferential().addElement() + .setPath(thePath) + .setMin(1) + .setId(thePath); + + DaoMethodOutcome outcome = myStructureDefinitionDao.update(sd, new SystemRequestDetails()); + assertNotNull(outcome.getResource()); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ResourceValidationR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ResourceValidationR4Test.java deleted file mode 100644 index 6671c1bf0fb..00000000000 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ResourceValidationR4Test.java +++ /dev/null @@ -1,333 +0,0 @@ -package ca.uhn.fhir.jpa.validation; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; -import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; -import ca.uhn.fhir.jpa.interceptor.validation.IRepositoryValidatingRule; -import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor; -import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; -import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.gclient.TokenClientParam; -import ca.uhn.fhir.rest.param.UriParam; -import ca.uhn.fhir.rest.param.UriParamQualifierEnum; -import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; -import ca.uhn.fhir.validation.FhirValidator; -import ca.uhn.fhir.validation.SingleValidationMessage; -import ca.uhn.fhir.validation.ValidationResult; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; -import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; -import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; -import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; -import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; -import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; -import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.StructureDefinition; -import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; - -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -public class ResourceValidationR4Test extends BaseResourceProviderR4Test { - private static final FhirContext ourCtx = FhirContext.forR4(); - private static FhirValidator myFhirValidator; - private static PrePopulatedValidationSupport ourValidationSupport; - private static final String myResourceType = "Patient"; - private static final String myResourceTypeDefinition = "http://hl7.org/fhir/StructureDefinition/Patient"; - private static final String PATIENT_STRUCTURE_DEFINITION_URL = "http://example.org/fhir/StructureDefinition/TestPatient"; - - private final RepositoryValidatingInterceptor myRepositoryValidatingInterceptor = new RepositoryValidatingInterceptor(ourCtx, Collections.emptyList()); - @Autowired - private ApplicationContext myApplicationContext; - - private final String profile = PATIENT_STRUCTURE_DEFINITION_URL; - - @BeforeAll - public static void setup() { - myFhirValidator = ourCtx.newValidator(); - myFhirValidator.setValidateAgainstStandardSchema(false); - myFhirValidator.setValidateAgainstStandardSchematron(false); - - ourValidationSupport = new PrePopulatedValidationSupport(ourCtx); - - ValidationSupportChain chain = new ValidationSupportChain( - new DefaultProfileValidationSupport(ourCtx), - new SnapshotGeneratingValidationSupport(ourCtx), - new CommonCodeSystemsTerminologyService(ourCtx), - new InMemoryTerminologyServerValidationSupport(ourCtx), - ourValidationSupport); - CachingValidationSupport myValidationSupport = new CachingValidationSupport(chain, true); - FhirInstanceValidator myInstanceVal = new FhirInstanceValidator(myValidationSupport); - myFhirValidator.registerValidatorModule(myInstanceVal); - } - - @BeforeEach - public void before() { - myInterceptorRegistry.registerInterceptor(myRepositoryValidatingInterceptor); - } - - @AfterEach - public void after() { - myInterceptorRegistry.unregisterInterceptor(myRepositoryValidatingInterceptor); - } - - @Test - public void testCreateStructureDefinition_createMultipleWithWithSameUrl_isStoredSuccessfully() { - createPatientStructureDefinitionWithMandatoryField(PATIENT_STRUCTURE_DEFINITION_URL, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(PATIENT_STRUCTURE_DEFINITION_URL, "2", "Patient.name"); - createPatientStructureDefinitionWithMandatoryField(PATIENT_STRUCTURE_DEFINITION_URL, "3", "Patient.birthdate"); - } - - - @Nested - public class OnDemandResourceValidationTests { - @Test - public void testValidatePatient_withProfileIncludingVersion_shouldUseSpecifiedVersion() { - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(profile, "2", "Patient.name"); - - IIdType idType = createPatient(withProfile(profile + "|1")); - Patient patient = myPatientDao.read(idType, mySrd); - - ValidationResult validationResult = myFhirValidator.validateWithResult(patient); - assertFalse(validationResult.isSuccessful()); - assertEquals(1, validationResult.getMessages().size()); - - SingleValidationMessage message = validationResult.getMessages().iterator().next(); - assertEquals("Patient.identifier: minimum required = 1, but only found 0 (from " + profile + "|1)", message.getMessage()); - } - - @Test - public void testValidatePatient_withProfileNoVersion_shouldUsesLatestVersion() { - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(profile, "2", "Patient.name"); - - IIdType idType = createPatient(withProfile(profile)); - Patient patient = myPatientDao.read(idType, mySrd); - - ValidationResult validationResult = myFhirValidator.validateWithResult(patient); - assertFalse(validationResult.isSuccessful()); - assertEquals(1, validationResult.getMessages().size()); - - SingleValidationMessage message = validationResult.getMessages().iterator().next(); - assertEquals("Patient.name: minimum required = 1, but only found 0 (from " + profile + "|2)", message.getMessage()); - } - - @Test - public void testValidatePatient_withMultipleProfilesDifferentUrls_validatesAgainstAllProfiles() { - final String sdIdentifier = PATIENT_STRUCTURE_DEFINITION_URL + "-identifier"; - final String sdName = PATIENT_STRUCTURE_DEFINITION_URL + "-name"; - - createPatientStructureDefinitionWithMandatoryField(sdIdentifier, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(sdName, "1", "Patient.name"); - - IIdType idType = createPatient(withProfile(sdIdentifier), withProfile(sdName)); - Patient patient = myPatientDao.read(idType, mySrd); - - ValidationResult validationResult = myFhirValidator.validateWithResult(patient); - assertFalse(validationResult.isSuccessful()); - assertEquals(2, validationResult.getMessages().size()); - - Iterator messageIterator = validationResult.getMessages().iterator(); - assertEquals("Patient.identifier: minimum required = 1, but only found 0 (from " + profile + "-identifier|1)", messageIterator.next().getMessage()); - assertEquals("Patient.name: minimum required = 1, but only found 0 (from " + profile + "-name|1)", messageIterator.next().getMessage()); - } - - @Test - public void testValidatePatient_withMultipleProfilesSameUrlDifferentVersions_validatesAgainstFirstVersion() { - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(profile, "2", "Patient.name"); - createPatientStructureDefinitionWithMandatoryField(profile, "3", "Patient.birthDate"); - - IIdType idType = createPatient( - withProfile(profile + "|2"), - withProfile(profile + "|1"), - withProfile(profile + "|3")); - Patient patient = myPatientDao.read(idType, mySrd); - - ValidationResult validationResult = myFhirValidator.validateWithResult(patient); - assertFalse(validationResult.isSuccessful()); - assertEquals(1, validationResult.getMessages().size()); - - SingleValidationMessage message = validationResult.getMessages().iterator().next(); - assertEquals("Patient.identifier: minimum required = 1, but only found 0 (from " + profile + "|1)", message.getMessage()); - } - } - - @Nested - public class RepositoryValidationOnCreateTests { - @Test - public void testCreatePatient_withProfileNoVersionAndRepositoryValidationRuleNoVersion_validatesAgainstFirstVersion() { - setupRepositoryValidationRules(profile); - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(PATIENT_STRUCTURE_DEFINITION_URL, "2", "Patient.name"); - - try { - createPatient(withProfile(profile)); - fail(); - } catch (PreconditionFailedException e) { - ourLog.info(e.getMessage()); - assertEquals(Msg.code(574) - + "Patient.identifier: minimum required = 1, but only found 0 (from " + profile + "|1)", e.getMessage()); - } - } - - @Test - public void testCreatePatient_withProfileWithVersionAndRepositoryValidationRuleWithVersion_unknownProfile() { - setupRepositoryValidationRules(profile + "|1"); - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(profile, "2", "Patient.name"); - - try { - createPatient(withProfile(profile + "|1")); - fail(); - } catch (PreconditionFailedException e) { - assertEquals(Msg.code(574) + "Profile reference '" + profile - + "|1' has not been checked because it is unknown, and the validator is set to not fetch unknown profiles", e.getMessage()); - } - } - - @Test - public void testCreatePatient_withProfileWithVersionAndRepositoryValidationRuleNoVersion_doesNotConform() { - setupRepositoryValidationRules(profile); - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(profile, "2", "Patient.name"); - - try { - createPatient(withProfile(profile + "|1")); - fail(); - } catch (PreconditionFailedException e) { - assertEquals(Msg.code(575) + "Resource of type \"" + myResourceType - + "\" does not declare conformance to profile from: [" + profile + "]", e.getMessage()); - } - } - - @Test - public void testCreatePatient_withPatientProfileWithoutVersionAndRepositoryValidationRuleWithVersion_doesNotConform() { - setupRepositoryValidationRules(profile + "|1"); - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatientStructureDefinitionWithMandatoryField(profile, "2", "Patient.name"); - - try { - createPatient(withProfile(profile)); - fail(); - } catch (PreconditionFailedException e) { - assertEquals(Msg.code(575) + "Resource of type \"" + myResourceType - + "\" does not declare conformance to profile from: [" + profile + "|1]", e.getMessage()); - } - } - - private void setupRepositoryValidationRules(String... theProfiles) { - List rules = myApplicationContext - .getBean(RepositoryValidatingRuleBuilder.REPOSITORY_VALIDATING_RULE_BUILDER, RepositoryValidatingRuleBuilder.class) - .forResourcesOfType(myResourceType) - .requireAtLeastOneProfileOf(theProfiles) - .and() - .requireValidationToDeclaredProfiles() - .withBestPracticeWarningLevel(BestPracticeWarningLevel.Ignore) - .rejectOnSeverity("error") - .build(); - - myRepositoryValidatingInterceptor.setRules(rules); - } - } - - @Nested - public class SearchWithProfileTests { - @Test - public void testSearchPatient_withProfileBelowCriteria_returnsAllMatches() { - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - IIdType id1 = createPatient(withProfile(profile)); - IIdType id2 = createPatient(withProfile(profile + "|1")); - - SearchParameterMap params = new SearchParameterMap(); - params.add("_profile", new UriParam(profile).setQualifier(UriParamQualifierEnum.BELOW)); - IBundleProvider bundleProvider = myPatientDao.search(params, mySrd); - assertFalse(bundleProvider.isEmpty()); - assertThat(toUnqualifiedVersionlessIdValues(bundleProvider), - containsInAnyOrder(id1.toUnqualifiedVersionless().getValue(), id2.toUnqualifiedVersionless().getValue())); - } - - @Test - public void testSearchPatient_withProfileExactCriteriaWithoutVersionAndPatientProfileWithoutVersion_returnsExactMatch() { - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - - IIdType id1 = createPatient(withProfile(profile)); - IIdType id2 = createPatient(withProfile(profile)); - - Bundle outcome = myClient.search().forResource(myResourceType) - .where(new TokenClientParam("_profile").exactly().code(profile)) - .returnBundle(Bundle.class).execute(); - assertThat(toUnqualifiedVersionlessIdValues(outcome), - containsInAnyOrder(id1.toUnqualifiedVersionless().getValue(), id2.toUnqualifiedVersionless().getValue())); - } - - @Test - public void testSearchPatient_withProfileExactCriteriaWithVersionAndPatientProfileWithVersion_returnsExactMatch() { - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - IIdType id1 = createPatient(withProfile(profile + "|1")); - - Bundle outcome = myClient.search().forResource(myResourceType) - .where(new TokenClientParam("_profile").exactly().code(profile + "|1")) - .returnBundle(Bundle.class).execute(); - assertThat(toUnqualifiedVersionlessIdValues(outcome), - containsInAnyOrder(id1.toUnqualifiedVersionless().getValue())); - } - - @Test - public void testSearchPatient_withProfileExactCriteriaWithoutVersionAndPatientProfileWithVersion_returnsNoMatch() { - final String profile = PATIENT_STRUCTURE_DEFINITION_URL; - - createPatientStructureDefinitionWithMandatoryField(profile, "1", "Patient.identifier"); - createPatient(withProfile(profile + "|1")); - - Bundle outcome = myClient.search().forResource(myResourceType) - .where(new TokenClientParam("_profile").exactly().code(profile)) - .returnBundle(Bundle.class).execute(); - assertTrue(outcome.getEntryFirstRep().isEmpty()); - } - } - - private void createPatientStructureDefinitionWithMandatoryField(String theUrl, String theVersion, String thePath) { - StructureDefinition sd = new StructureDefinition() - .setUrl(theUrl).setVersion(theVersion) - .setBaseDefinition(myResourceTypeDefinition) - .setType(myResourceType) - .setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); - sd.getDifferential().addElement() - .setPath(thePath) - .setMin(1) - .setId(thePath); - - DaoMethodOutcome outcome = myStructureDefinitionDao.create(sd, mySrd); - assertTrue(outcome.getCreated()); - ourValidationSupport.addStructureDefinition(sd); - } -} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/SearchWithResourceProfileR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/SearchWithResourceProfileR4Test.java new file mode 100644 index 00000000000..c817919cbd9 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/SearchWithResourceProfileR4Test.java @@ -0,0 +1,96 @@ +package ca.uhn.fhir.jpa.validation; + +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.gclient.TokenClientParam; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.param.UriParamQualifierEnum; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SearchWithResourceProfileR4Test extends BaseResourceProviderR4Test { + private static final String myResourceType = "Patient"; + private final String myProfile = "http://example.org/fhir/StructureDefinition/TestPatient"; + + @Test + public void testSearchPatient_withProfileBelowCriteria_returnsAllMatches() { + createProfile(); + IIdType id1 = createPatient(withProfile(myProfile)); + IIdType id2 = createPatient(withProfile(myProfile + "|1")); + + SearchParameterMap params = new SearchParameterMap(); + params.add("_profile", new UriParam(myProfile).setQualifier(UriParamQualifierEnum.BELOW)); + IBundleProvider bundleProvider = myPatientDao.search(params, mySrd); + assertFalse(bundleProvider.isEmpty()); + assertThat(toUnqualifiedVersionlessIdValues(bundleProvider), + containsInAnyOrder(id1.toUnqualifiedVersionless().getValue(), id2.toUnqualifiedVersionless().getValue())); + } + + @Test + public void testSearchPatient_withProfileExactCriteriaWithoutVersionAndPatientProfileWithoutVersion_returnsExactMatch() { + createProfile(); + IIdType id1 = createPatient(withProfile(myProfile)); + IIdType id2 = createPatient(withProfile(myProfile)); + + Bundle outcome = myClient.search().forResource(myResourceType) + .where(new TokenClientParam("_profile").exactly().code(myProfile)) + .returnBundle(Bundle.class).execute(); + assertThat(toUnqualifiedVersionlessIdValues(outcome), + containsInAnyOrder(id1.toUnqualifiedVersionless().getValue(), id2.toUnqualifiedVersionless().getValue())); + } + + @Test + public void testSearchPatient_withProfileExactCriteriaWithVersionAndPatientProfileWithVersion_returnsExactMatch() { + createProfile(); + IIdType id1 = createPatient(withProfile(myProfile + "|1")); + + Bundle outcome = myClient.search().forResource(myResourceType) + .where(new TokenClientParam("_profile").exactly().code(myProfile + "|1")) + .returnBundle(Bundle.class).execute(); + assertThat(toUnqualifiedVersionlessIdValues(outcome), + containsInAnyOrder(id1.toUnqualifiedVersionless().getValue())); + } + + @Test + public void testSearchPatient_withProfileExactCriteria_returnsNoMatch() { + createProfile(); + createPatient(withProfile(myProfile + "|1")); + + Bundle outcome = myClient.search().forResource(myResourceType) + .where(new TokenClientParam("_profile").exactly().code(myProfile)) + .returnBundle(Bundle.class).execute(); + assertTrue(outcome.getEntryFirstRep().isEmpty()); + } + + private void createProfile() { + final String baseProfile = "http://hl7.org/fhir/StructureDefinition/Patient"; + final String profileId = "TestProfile"; + final String version = "1"; + final String rulePath = "Patient.identifier"; + + StructureDefinition sd = new StructureDefinition() + .setUrl(myProfile).setVersion(version) + .setBaseDefinition(baseProfile) + .setType(myResourceType) + .setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setId(profileId); + sd.getDifferential().addElement() + .setPath(rulePath) + .setMin(1) + .setId(rulePath); + + DaoMethodOutcome outcome = myStructureDefinitionDao.update(sd, new SystemRequestDetails()); + assertNotNull(outcome.getResource()); + } +}