package ca.uhn.fhir.validation; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import org.apache.commons.io.IOUtils; import org.hl7.fhir.dstu3.context.IWorkerContext; import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.dstu3.hapi.validation.HapiWorkerContext; import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport.CodeValidationResult; import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode; import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent; import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.IntegerType; import org.hl7.fhir.dstu3.model.Questionnaire; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; import org.hl7.fhir.dstu3.model.QuestionnaireResponse; import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseStatus; import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.ValueSet; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.util.TestUtil; public class QuestionnaireResponseValidatorDstu3Test { private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); private static FhirContext ourCtx = FhirContext.forDstu3(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorDstu3Test.class); private FhirInstanceValidator myInstanceVal; private FhirValidator myVal; private IValidationSupport myValSupport; private IWorkerContext myWorkerCtx; @Before public void before() { myValSupport = mock(IValidationSupport.class); // new DefaultProfileValidationSupport(); myWorkerCtx = new HapiWorkerContext(ourCtx, myValSupport); myVal = ourCtx.newValidator(); myVal.setValidateAgainstStandardSchema(false); myVal.setValidateAgainstStandardSchematron(false); ValidationSupportChain validationSupport = new ValidationSupportChain(myValSupport, myDefaultValidationSupport); myInstanceVal = new FhirInstanceValidator(validationSupport); myVal.registerValidatorModule(myInstanceVal); } private ValidationResult stripBindingHasNoSourceMessage(ValidationResult theErrors) { List<SingleValidationMessage> messages = new ArrayList<SingleValidationMessage>(theErrors.getMessages()); for (int i = 0; i < messages.size(); i++) { if (messages.get(i).getMessage().contains("has no source, so can't")) { messages.remove(i); i--; } } return new ValidationResult(ourCtx, messages); } @Test public void testAnswerWithWrongType() { Questionnaire q = new Questionnaire(); q.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("Answer value must be of type boolean")); } @Test public void testCodedAnswer() { String questionnaireRef = "http://example.com/Questionnaire/q1"; Questionnaire q = new Questionnaire(); q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.CHOICE).setOptions(new Reference("http://somevalueset")); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq("http://example.com/Questionnaire/q1"))).thenReturn(q); CodeSystem codeSystem = new CodeSystem(); codeSystem.setContent(CodeSystemContentMode.COMPLETE); codeSystem.setUrl("http://codesystems.com/system"); codeSystem.addConcept().setCode("code0"); when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system"))).thenReturn(codeSystem); CodeSystem codeSystem2 = new CodeSystem(); codeSystem2.setContent(CodeSystemContentMode.COMPLETE); codeSystem2.setUrl("http://codesystems.com/system2"); codeSystem2.addConcept().setCode("code2"); when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system2"))).thenReturn(codeSystem2); ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); when(myValSupport.fetchResource(any(FhirContext.class), eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); QuestionnaireResponse qa; ValidationResult errors; // Good code qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code0")); errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); assertEquals(errors.toString(), 0, errors.getMessages().size()); // Bad code qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code1")); errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("The value provided (http://codesystems.com/system::code1) is not in the options value set in the questionnaire")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system2").setCode("code3")); errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("The value provided (http://codesystems.com/system2::code3) is not in the options value set in the questionnaire")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); } @Test public void testGroupWithNoLinkIdInQuestionnaireResponse() { Questionnaire q = new Questionnaire(); QuestionnaireItemComponent qGroup = q.addItem().setType(QuestionnaireItemType.GROUP); qGroup.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); QuestionnaireResponseItemComponent qaGroup = qa.addItem(); qaGroup.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("minimum required = 1, but only found 0 - QuestionnaireResponse.item")); } @Test public void testItemWithNoType() { Questionnaire q = new Questionnaire(); QuestionnaireItemComponent qGroup = q.addItem(); qGroup.setLinkId("link0"); qGroup.addItem().setLinkId("link1").setType(QuestionnaireItemType.STRING); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); QuestionnaireResponseItemComponent qaGroup = qa.addItem().setLinkId("link0"); qaGroup.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("Definition for item link0 does not contain a type")); assertEquals(1, errors.getMessages().size()); } @Test public void testMissingRequiredQuestion() { Questionnaire q = new Questionnaire(); q.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.STRING); q.addItem().setLinkId("link1").setRequired(true).setType(QuestionnaireItemType.STRING); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); String reference = qa.getQuestionnaire().getReference(); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q); ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("No response found for required item link0")); } @Test public void testOpenchoiceAnswer() { String questionnaireRef = "http://example.com/Questionnaire/q1"; Questionnaire q = new Questionnaire(); QuestionnaireItemComponent item = q.addItem(); item.setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.OPENCHOICE).setOptions(new Reference("http://somevalueset")); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(questionnaireRef))).thenReturn(q); CodeSystem codeSystem = new CodeSystem(); codeSystem.setContent(CodeSystemContentMode.COMPLETE); codeSystem.setUrl("http://codesystems.com/system"); codeSystem.addConcept().setCode("code0"); when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system"))).thenReturn(codeSystem); CodeSystem codeSystem2 = new CodeSystem(); codeSystem2.setContent(CodeSystemContentMode.COMPLETE); codeSystem2.setUrl("http://codesystems.com/system2"); codeSystem2.addConcept().setCode("code2"); when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system2"))).thenReturn(codeSystem2); ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); when(myValSupport.fetchResource(any(FhirContext.class), eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(FhirContext.class), eq("http://codesystems.com/system"), eq("code0"), any(String.class))).thenReturn(new CodeValidationResult(new ConceptDefinitionComponent(new CodeType("code0")))); QuestionnaireResponse qa; ValidationResult errors; // Good code qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code0")); errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); assertEquals(errors.toString(), 0, errors.getMessages().size()); // Bad code qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code1")); errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("The value provided (http://codesystems.com/system::code1) is not in the options value set in the questionnaire")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); // Partial code qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem(null).setCode("code1")); errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("The value provided (null::code1) is not in the options value set in the questionnaire")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("").setCode("code1")); errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("The value provided (null::code1) is not in the options value set in the questionnaire")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://system").setCode(null)); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("The value provided (http://system::null) is not in the options value set in the questionnaire")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); // Wrong type qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new IntegerType(123)); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("Cannot validate integer answer option because no option list is provided")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); // String answer qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setDisplay("Hello")); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.getMessages(), empty()); // Missing String answer qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference(questionnaireRef); qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setDisplay("")); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("No response answer found for required item link0")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item")); } @Test public void testUnexpectedAnswer() { Questionnaire q = new Questionnaire(); q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); } @Test public void testUnexpectedGroup() { Questionnaire q = new Questionnaire(); q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.addItem().setLinkId("link1").addItem().setLinkId("link2"); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); } // @Test public void validateHealthConnexExample() throws Exception { String input = IOUtils.toString(QuestionnaireResponseValidatorDstu3Test.class.getResourceAsStream("/questionnaireanswers-0f431c50ddbe4fff8e0dd6b7323625fc.xml")); QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input); ValidationResult errors = myVal.validateWithResult(qa); assertEquals(errors.toString(), 0, errors.getMessages().size()); /* * Now change a coded value */ //@formatter:off input = input.replaceAll( "<answer>\n" + " <valueCoding>\n" + " <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + " <code value=\"2\"/>\n" + " <display value=\"Once/twice\"/>\n" + " </valueCoding>\n" + " </answer>", "<answer>\n" + " <valueCoding>\n" + " <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + " <code value=\"GGG\"/>\n" + " <display value=\"Once/twice\"/>\n" + " </valueCoding>\n" + " </answer>"); assertThat(input, containsString("GGG")); //@formatter:on qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input); errors = myVal.validateWithResult(qa); assertEquals(errors.toString(), 10, errors.getMessages().size()); } @AfterClass public static void afterClassClearContext() { myDefaultValidationSupport.flush(); myDefaultValidationSupport = null; TestUtil.clearAllStaticFieldsForUnitTest(); } }