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 java.util.ArrayList; import java.util.List; import org.apache.commons.io.IOUtils; import org.hl7.fhir.instance.model.Coding; import org.hl7.fhir.instance.model.DataElement; import org.hl7.fhir.instance.model.IntegerType; import org.hl7.fhir.instance.model.Questionnaire; import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat; import org.hl7.fhir.instance.model.Questionnaire.GroupComponent; import org.hl7.fhir.instance.model.QuestionnaireResponse; import org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionnaireResponseStatus; import org.hl7.fhir.instance.model.Reference; import org.hl7.fhir.instance.model.StringType; import org.hl7.fhir.instance.model.ValueSet; import org.hl7.fhir.instance.utils.WorkerContext; import org.hl7.fhir.instance.validation.QuestionnaireResponseValidator; import org.hl7.fhir.instance.validation.ValidationMessage; 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 QuestionnaireResponseValidatorTest { private static FhirContext ourCtx = FhirContext.forDstu2Hl7Org(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorTest.class); private QuestionnaireResponseValidator myVal; private WorkerContext myWorkerCtx; @Before public void before() { myWorkerCtx = new WorkerContext(); myVal = new QuestionnaireResponseValidator(myWorkerCtx); } @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); } @Test public void testAnswerWithWrongType() { Questionnaire q = new Questionnaire(); q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q); List<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]")); } @Test public void testCodedAnswer() { String questionnaireRef = "http://example.com/Questionnaire/q1"; Questionnaire q = new Questionnaire(); q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.CHOICE).setOptions(new Reference("http://somevalueset")); myWorkerCtx.getQuestionnaires().put(questionnaireRef, q); ValueSet options = new ValueSet(); options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("urn:system2").addConcept().setCode("code2"); myWorkerCtx.getValueSets().put("http://somevalueset", options); QuestionnaireResponse qa; List<ValidationMessage> errors; // Good code qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); assertEquals(errors.toString(), 0, errors.size()); qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code2")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); assertEquals(errors.toString(), 0, errors.size()); // Bad code qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset]")); qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code3")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Question with linkId[link0] has answer with system[urn:system2] and code[code3] but this is not a valid answer for ValueSet[http://somevalueset]")); } @Test public void testOpenchoiceAnswer() { String questionnaireRef = "http://example.com/Questionnaire/q1"; Questionnaire q = new Questionnaire(); q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.OPENCHOICE).setOptions(new Reference("http://somevalueset")); myWorkerCtx.getQuestionnaires().put(questionnaireRef, q); ValueSet options = new ValueSet(); options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("urn:system2").addConcept().setCode("code2"); myWorkerCtx.getValueSets().put("http://somevalueset", options); QuestionnaireResponse qa; List<ValidationMessage> errors; // Good code qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); assertEquals(errors.toString(), 0, errors.size()); qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code2")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); assertEquals(errors.toString(), 0, errors.size()); // Bad code qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset]")); // Partial code qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem(null).setCode("code1")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a system")); qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("").setCode("code1")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a system")); qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("system").setCode(null)); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a code")); qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("system").setCode(null)); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a code")); // Wrong type qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new IntegerType(123)); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] found of type [IntegerType] but this is invalid for question of type [open-choice]")); // String answer qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("Hello")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors, empty()); // Missing String answer qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference(questionnaireRef); qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("")); errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]")); assertThat(errors.toString(), containsString("Answer to question with linkId[link0] is required but answer does not have a value")); } @Test public void testExtensionDereference() throws Exception { Questionnaire q = ourCtx.newJsonParser().parseResource(Questionnaire.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-q.json"))); QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-qr.xml"))); DataElement de = ourCtx.newJsonParser().parseResource(DataElement.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-de.json"))); myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q); myWorkerCtx.getDataElements().put("DataElement/4771", de); List<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertEquals(errors.toString(), errors.size(), 0); } @Test public void testGroupWithNoLinkIdInQuestionnaireResponse() { Questionnaire q = new Questionnaire(); GroupComponent qGroup = q.getGroup().addGroup(); qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup(); qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q); List<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]")); } @Test public void testMissingRequiredQuestion() { Questionnaire q = new Questionnaire(); q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.STRING); q.getGroup().addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.STRING); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q); List<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("Missing answer to required question with linkId[link0]")); } @Test public void testMultipleGroupsWithNoLinkIdInQuestionnaire() { Questionnaire q = new Questionnaire(); GroupComponent qGroup = q.getGroup().addGroup(); qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN); qGroup = q.getGroup().addGroup(); qGroup.addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup(); qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q); List<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString( "ValidationMessage[level=FATAL,type=BUSINESSRULE,location=//QuestionnaireResponse/group[0],message=Questionnaire in invalid, unable to validate QuestionnaireResponse: Multiple groups found at this position with blank/missing linkId]")); assertEquals(1, errors.size()); } @Test public void testUnexpectedAnswer() { Questionnaire q = new Questionnaire(); q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q); List<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]")); assertThat(errors.toString(), containsString("message=Found answer with linkId[link1] but this ID is not allowed at this position")); } @Test public void testUnexpectedGroup() { Questionnaire q = new Questionnaire(); q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN); QuestionnaireResponse qa = new QuestionnaireResponse(); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getGroup().addGroup().setLinkId("link1"); myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q); List<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); ourLog.info(errors.toString()); assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/group[0]")); assertThat(errors.toString(), containsString("Group with linkId[link1] found at this position, but this group does not exist at this position in Questionnaire")); } // @Test public void validateHealthConnexExample() throws Exception { String input = IOUtils.toString(QuestionnaireResponseValidatorTest.class.getResourceAsStream("/questionnaireanswers-0f431c50ddbe4fff8e0dd6b7323625fc.xml")); QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input); ArrayList<ValidationMessage> errors = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); assertEquals(errors.toString(), 0, errors.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 = new ArrayList<ValidationMessage>(); myVal.validate(errors, qa); assertEquals(errors.toString(), 10, errors.size()); } }