package ca.uhn.fhir.validation; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.instance.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.instance.hapi.validation.IValidationSupport; import org.hl7.fhir.instance.hapi.validation.IValidationSupport.CodeValidationResult; import org.hl7.fhir.instance.model.CodeType; import org.hl7.fhir.instance.model.Observation; import org.hl7.fhir.instance.model.Observation.ObservationStatus; import org.hl7.fhir.instance.model.Patient; import org.hl7.fhir.instance.model.StringType; import org.hl7.fhir.instance.model.StructureDefinition; import org.hl7.fhir.instance.model.ValueSet; import org.hl7.fhir.instance.model.ValueSet.ConceptDefinitionComponent; import org.hl7.fhir.instance.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.instance.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.validation.InstanceValidator; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.web.bind.annotation.GetMapping; import ca.uhn.fhir.context.FhirContext; public class FhirInstanceValidatorTest { private static FhirContext ourCtx = FhirContext.forDstu2Hl7Org(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidatorTest.class); private static DefaultProfileValidationSupport ourDefaultValidationSupport = new DefaultProfileValidationSupport() { @Override public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { if (theUri.equals("http://fhir.hl7.org.nz/dstu2/StructureDefinition/ohAllergyIntolerance")) { String contents; try { contents = IOUtils.toString(getClass().getResourceAsStream("/allergyintolerance-sd-david.json"), "UTF-8"); } catch (IOException e) { throw new Error(e); } StructureDefinition sd = ourCtx.newJsonParser().parseResource(StructureDefinition.class, contents); return (T) sd; } T retVal = super.fetchResource(theContext, theClass, theUri); if (retVal == null) { ourLog.warn("Can not fetch: {}", theUri); } return retVal; } }; private FhirInstanceValidator myInstanceVal; private IValidationSupport myMockSupport; private FhirValidator myVal; private ArrayList<String> myValidConcepts; private Map<String, ValueSetExpansionComponent> mySupportedCodeSystemsForExpansion; private void addValidConcept(String theSystem, String theCode) { myValidConcepts.add(theSystem + "___" + theCode); } @AfterClass public static void afterClass() { ourDefaultValidationSupport.flush(); ourDefaultValidationSupport = null; } @SuppressWarnings("unchecked") @Before public void before() { myVal = ourCtx.newValidator(); myVal.setValidateAgainstStandardSchema(false); myVal.setValidateAgainstStandardSchematron(false); myInstanceVal = new FhirInstanceValidator(ourDefaultValidationSupport); myVal.registerValidatorModule(myInstanceVal); mySupportedCodeSystemsForExpansion = new HashMap<String, ValueSet.ValueSetExpansionComponent>(); myValidConcepts = new ArrayList<String>(); myMockSupport = mock(IValidationSupport.class); when(myMockSupport.expandValueSet(any(FhirContext.class), any(ConceptSetComponent.class))).thenAnswer(new Answer<ValueSetExpansionComponent>() { @Override public ValueSetExpansionComponent answer(InvocationOnMock theInvocation) throws Throwable { ConceptSetComponent arg = (ConceptSetComponent)theInvocation.getArguments()[0]; ValueSetExpansionComponent retVal = mySupportedCodeSystemsForExpansion.get(arg.getSystem()); if (retVal == null) { retVal = ourDefaultValidationSupport.expandValueSet(any(FhirContext.class), arg); } ourLog.info("expandValueSet({}) : {}", new Object[] { theInvocation.getArguments()[0], retVal }); return retVal; } }); when(myMockSupport.isCodeSystemSupported(any(FhirContext.class), any(String.class))).thenAnswer(new Answer<Boolean>() { @Override public Boolean answer(InvocationOnMock theInvocation) throws Throwable { boolean retVal = mySupportedCodeSystemsForExpansion.containsKey(theInvocation.getArguments()[0]); ourLog.info("isCodeSystemSupported({}) : {}", new Object[] { theInvocation.getArguments()[0], retVal }); return retVal; } }); when(myMockSupport.fetchResource(any(FhirContext.class), any(Class.class), any(String.class))) .thenAnswer(new Answer<IBaseResource>() { @Override public IBaseResource answer(InvocationOnMock theInvocation) throws Throwable { FhirContext fhirContext = (FhirContext) theInvocation.getArguments()[0]; Class<IBaseResource> type = (Class<IBaseResource>) theInvocation.getArguments()[1]; String uri = (String) theInvocation.getArguments()[2]; IBaseResource retVal = ourDefaultValidationSupport.fetchResource(fhirContext, type, uri); ourLog.info("fetchResource({}, {}) : {}", new Object[] { theInvocation.getArguments()[1], theInvocation.getArguments()[2], retVal }); return retVal; } }); when(myMockSupport.validateCode(any(FhirContext.class), any(String.class), any(String.class), any(String.class))) .thenAnswer(new Answer<CodeValidationResult>() { @Override public CodeValidationResult answer(InvocationOnMock theInvocation) throws Throwable { FhirContext ctx = (FhirContext) theInvocation.getArguments()[0]; String system = (String) theInvocation.getArguments()[1]; String code = (String) theInvocation.getArguments()[2]; CodeValidationResult retVal; if (myValidConcepts.contains(system + "___" + code)) { retVal = new CodeValidationResult(new ConceptDefinitionComponent(new CodeType(code))); } else { retVal = ourDefaultValidationSupport.validateCode(ctx, system, code, (String) theInvocation.getArguments()[2]); } ourLog.info("validateCode({}, {}, {}) : {}", new Object[] { system, code, (String) theInvocation.getArguments()[2], retVal }); return retVal; } }); when(myMockSupport.fetchCodeSystem(any(FhirContext.class), any(String.class))).thenAnswer(new Answer<ValueSet>() { @Override public ValueSet answer(InvocationOnMock theInvocation) throws Throwable { ValueSet retVal = ourDefaultValidationSupport.fetchCodeSystem((FhirContext) theInvocation.getArguments()[0],(String) theInvocation.getArguments()[1]); ourLog.info("fetchCodeSystem({}) : {}", new Object[] { (String) theInvocation.getArguments()[1], retVal }); return retVal; } }); } private List<SingleValidationMessage> logResultsAndReturnNonInformationalOnes(ValidationResult theOutput) { List<SingleValidationMessage> retVal = new ArrayList<SingleValidationMessage>(); int index = 0; for (SingleValidationMessage next : theOutput.getMessages()) { ourLog.info("Result {}: {} - {} - {}", new Object[] { index, next.getSeverity(), next.getLocationString(), next.getMessage() }); index++; if (next.getSeverity() != ResultSeverityEnum.INFORMATION) { retVal.add(next); } } return retVal; } private List<SingleValidationMessage> logResultsAndReturnAll(ValidationResult theOutput) { List<SingleValidationMessage> retVal = new ArrayList<SingleValidationMessage>(); int index = 0; for (SingleValidationMessage next : theOutput.getMessages()) { ourLog.info("Result {}: {} - {} - {}", new Object[] { index, next.getSeverity(), next.getLocationString(), next.getMessage() }); index++; retVal.add(next); } return retVal; } @Rule public TestRule watcher = new TestWatcher() { protected void starting(Description description) { ourLog.info("Starting test: " + description.getMethodName()); } }; @Test public void testValidateRawJsonResource() { // @formatter:off String input = "{" + "\"resourceType\":\"Patient\"," + "\"id\":\"123\"" + "}"; // @formatter:on ValidationResult output = myVal.validateWithResult(input); assertEquals(output.toString(), 0, output.getMessages().size()); } /** * Received by email from David Hay */ @Test public void testValidateAllergyIntoleranceFromDavid() throws Exception { String input = IOUtils.toString(getClass().getResourceAsStream("/allergyintolerance-david.json"), "UTF-8"); // Just make sure this doesn't crash ValidationResult output = myVal.validateWithResult(input); ourLog.info(output.toString()); } @Test public void testValidateRawJsonResourceBadAttributes() { // @formatter:off String input = "{" + "\"resourceType\":\"Patient\"," + "\"id\":\"123\"," + "\"foo\":\"123\"" + "}"; // @formatter:on ValidationResult output = myVal.validateWithResult(input); assertEquals(output.toString(), 1, output.getMessages().size()); ourLog.info(output.getMessages().get(0).getLocationString()); ourLog.info(output.getMessages().get(0).getMessage()); assertEquals("/foo", output.getMessages().get(0).getLocationString()); assertEquals("Element is unknown or does not match any slice", output.getMessages().get(0).getMessage()); } @Test public void testValidateRawXmlResource() { // @formatter:off String input = "<Patient xmlns=\"http://hl7.org/fhir\">" + "<id value=\"123\"/>" + "</Patient>"; // @formatter:on ValidationResult output = myVal.validateWithResult(input); assertEquals(output.toString(), 0, output.getMessages().size()); } @Test public void testValidateRawXmlResourceBadAttributes() { // @formatter:off String input = "<Patient xmlns=\"http://hl7.org/fhir\">" + "<id value=\"123\"/>" + "<foo value=\"222\"/>" + "</Patient>"; // @formatter:on ValidationResult output = myVal.validateWithResult(input); assertEquals(output.toString(), 1, output.getMessages().size()); ourLog.info(output.getMessages().get(0).getLocationString()); ourLog.info(output.getMessages().get(0).getMessage()); assertEquals("/f:Patient/f:foo", output.getMessages().get(0).getLocationString()); assertEquals("Element is unknown or does not match any slice", output.getMessages().get(0).getMessage()); } @Test public void testValidateResourceFailingInvariant() { Observation input = new Observation(); // Has a value, but not a status (which is required) input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); input.setValue(new StringType("AAA")); ValidationResult output = myVal.validateWithResult(input); assertThat(output.getMessages().size(), greaterThan(0)); assertEquals("Element '/f:Observation.status': minimum required = 1, but only found 0", output.getMessages().get(0).getMessage()); } /** * See #216 */ @Test public void testValidateRawXmlInvalidChoiceName() throws Exception { String input = IOUtils .toString(FhirInstanceValidator.class.getResourceAsStream("/medicationstatement_invalidelement.xml")); ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> res = logResultsAndReturnAll(output); ourLog.info(res.toString()); for (SingleValidationMessage nextMessage : res) { if (nextMessage.getSeverity() == ResultSeverityEnum.ERROR) { fail(nextMessage.toString()); } } // TODO: we should really not have any errors at all here, but for // now we aren't validating snomed codes correctly // assertEquals(output.toString(), 0, res.size()); } @Test public void testValidateResourceContainingProfileDeclarationDoesntResolve() { addValidConcept("http://loinc.org", "12345"); Observation input = new Observation(); input.getMeta().addProfile("http://foo/myprofile"); input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); input.setStatus(ObservationStatus.FINAL); myInstanceVal.setValidationSupport(myMockSupport); ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(errors.toString(), 1, errors.size()); assertEquals("StructureDefinition reference \"http://foo/myprofile\" could not be resolved", errors.get(0).getMessage()); } @Test public void testValidateResourceContainingProfileDeclaration() { addValidConcept("http://loinc.org", "12345"); Observation input = new Observation(); input.getMeta().addProfile("http://hl7.org/fhir/StructureDefinition/devicemetricobservation"); input.addIdentifier().setSystem("http://acme").setValue("12345"); input.getEncounter().setReference("http://foo.com/Encounter/9"); input.setStatus(ObservationStatus.FINAL); input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); myInstanceVal.setValidationSupport(myMockSupport); ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); assertThat(errors.toString(), containsString("Element '/f:Observation.subject': minimum required = 1, but only found 0")); assertThat(errors.toString(), containsString("Element encounter @ /f:Observation: max allowed = 0, but found 1")); assertThat(errors.toString(), containsString("Element '/f:Observation.device': minimum required = 1, but only found 0")); assertThat(errors.toString(), containsString("")); } @Test public void testValidateResourceWithDefaultValueset() { Observation input = new Observation(); input.setStatus(ObservationStatus.FINAL); input.getCode().setText("No code here!"); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(input)); ValidationResult output = myVal.validateWithResult(input); assertEquals(output.getMessages().size(), 0); } @Test public void testValidateResourceWithDefaultValuesetBadCode() { String input = "<Observation xmlns=\"http://hl7.org/fhir\">\n" + " <status value=\"notvalidcode\"/>\n" + " <code>\n" + " <text value=\"No code here!\"/>\n" + " </code>\n" + "</Observation>"; ValidationResult output = myVal.validateWithResult(input); assertEquals( "Coded value notvalidcode is not in value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status)", output.getMessages().get(0).getMessage()); } @Test public void testValidateJsonNumericId() { String input="{\"resourceType\": \"Patient\",\n" + " \"id\": 123,\n" + " \"meta\": {\n" + " \"versionId\": \"29\",\n" + " \"lastUpdated\": \"2015-12-22T19:53:11.000Z\"\n" + " },\n" + " \"communication\": {\n" + " \"language\": {\n" + " \"coding\": [\n" + " {\n" + " \"system\": \"urn:ietf:bcp:47\",\n" + " \"code\": \"hi\",\n" + " \"display\": \"Hindi\",\n" + " \"userSelected\": false\n" + " }],\n" + " \"text\": \"Hindi\"\n" + " },\n" + " \"preferred\": true\n" + " }\n" + "}"; ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> all = logResultsAndReturnNonInformationalOnes(output); assertEquals(0, all.size()); } @Test public void testValidateCodeableConceptContainingOnlyGoodCode() { Patient p = new Patient(); p.getMaritalStatus().addCoding().setSystem("http://hl7.org/fhir/v3/MaritalStatus").setCode("M"); ValidationResult output = myVal.validateWithResult(p); List<SingleValidationMessage> all = logResultsAndReturnNonInformationalOnes(output); assertEquals(0, all.size()); } @Test public void testValidateCodeableConceptContainingOnlyBadCode() { Patient p = new Patient(); p.getMaritalStatus().addCoding().setSystem("http://hl7.org/fhir/v3/MaritalStatus").setCode("FOO"); ValidationResult output = myVal.validateWithResult(p); List<SingleValidationMessage> all = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, all.size()); assertEquals("Unknown Code (http://hl7.org/fhir/v3/MaritalStatus#FOO)", output.getMessages().get(0).getMessage()); } @Test public void testValidateResourceWithValuesetExpansion() { Patient patient = new Patient(); patient.addIdentifier().setSystem("http://example.com/").setValue("12345").getType().addCoding().setSystem("http://example.com/foo/bar").setCode("bar"); ValidationResult output = myVal.validateWithResult(patient); List<SingleValidationMessage> all = logResultsAndReturnAll(output); assertEquals(2, all.size()); assertEquals("/f:Patient/f:identifier/f:type", all.get(0).getLocationString()); assertEquals("None of the codes are in the expected value set http://hl7.org/fhir/ValueSet/identifier-type (http://hl7.org/fhir/ValueSet/identifier-type)", all.get(0).getMessage()); assertEquals(ResultSeverityEnum.WARNING, all.get(0).getSeverity()); patient = new Patient(); patient.addIdentifier().setSystem("http://system").setValue("12345").getType().addCoding().setSystem("http://hl7.org/fhir/v2/0203").setCode("MR"); output = myVal.validateWithResult(patient); all = logResultsAndReturnNonInformationalOnes(output); assertEquals(0, all.size()); } @Test public void testValidateResourceWithExampleBindingCodeValidationFailing() { Observation input = new Observation(); myInstanceVal.setValidationSupport(myMockSupport); input.setStatus(ObservationStatus.FINAL); input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(errors.toString(), 1, errors.size()); assertEquals("Unable to validate code \"12345\" in code system \"http://loinc.org\"", errors.get(0).getMessage()); } @Test public void testValidateResourceWithExampleBindingCodeValidationPassingLoinc() { Observation input = new Observation(); myInstanceVal.setValidationSupport(myMockSupport); addValidConcept("http://loinc.org", "12345"); input.setStatus(ObservationStatus.FINAL); input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(errors.toString(), 0, errors.size()); } @Test public void testValidateResourceWithExampleBindingCodeValidationPassingLoincWithExpansion() { Observation input = new Observation(); ValueSetExpansionComponent expansionComponent = new ValueSetExpansionComponent(); expansionComponent.addContains().setSystem("http://loinc.org").setCode("12345").setDisplay("Some display code"); mySupportedCodeSystemsForExpansion.put("http://loinc.org", expansionComponent); myInstanceVal.setValidationSupport(myMockSupport); addValidConcept("http://loinc.org", "12345"); input.setStatus(ObservationStatus.FINAL); input.getCode().addCoding().setSystem("http://loinc.org").setCode("1234"); ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(errors.toString(), 1, errors.size()); assertEquals("Unable to validate code \"1234\" in code system \"http://loinc.org\"", errors.get(0).getMessage()); } @Test public void testValidateResourceWithExampleBindingCodeValidationPassingNonLoinc() { Observation input = new Observation(); myInstanceVal.setValidationSupport(myMockSupport); addValidConcept("http://acme.org", "12345"); input.setStatus(ObservationStatus.FINAL); input.getCode().addCoding().setSystem("http://acme.org").setCode("12345"); ValidationResult output = myVal.validateWithResult(input); List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(errors.toString(), 0, errors.size()); } }