From 87fa2ae6a8ca0dde143cce07a58987f5252dd578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=E2=89=A1ZRS?= <12814349+LZRS@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:31:00 +0300 Subject: [PATCH] Validate QuestionnaireItem.Group with repeats as repeating group (#2755) * Validate QuestionnaireItem.Group with repeats as repeating group Instead of just as group * Add tests for repeating groups in QuestionnaireResponseValidatorTest * Update licence year to fix spotless --- .../QuestionnaireResponseValidator.kt | 23 +-- .../QuestionnaireResponseValidatorTest.kt | 139 +++++++++++++++++- 2 files changed, 152 insertions(+), 10 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt index 616549caf7..e0a74edcad 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 Google LLC + * Copyright 2022-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,10 +151,12 @@ object QuestionnaireResponseValidator { questionnaireResponseItemValidator: QuestionnaireResponseItemValidator, linkIdToValidationResultMap: MutableMap>, ): Map> { - when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) { - Questionnaire.QuestionnaireItemType.DISPLAY, - Questionnaire.QuestionnaireItemType.NULL, -> Unit - Questionnaire.QuestionnaireItemType.GROUP -> + checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" } + when { + questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY || + questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit + questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats -> // Nested items under group // http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item validateQuestionnaireResponseItems( @@ -262,10 +264,13 @@ object QuestionnaireResponseValidator { questionnaireItem: Questionnaire.QuestionnaireItemComponent, questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent, ) { - when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) { - Questionnaire.QuestionnaireItemType.DISPLAY, - Questionnaire.QuestionnaireItemType.NULL, -> Unit - Questionnaire.QuestionnaireItemType.GROUP -> + checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" } + + when { + questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY || + questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit + questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats -> // Nested items under group // http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item checkQuestionnaireResponseItems(questionnaireItem.item, questionnaireResponseItem.item) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt index 6b3dfacf26..36fd666d41 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 Google LLC + * Copyright 2022-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import com.google.android.fhir.datacapture.extensions.EXTENSION_HIDDEN_URL +import com.google.android.fhir.datacapture.extensions.packRepeatedGroups import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import kotlinx.coroutines.test.runTest @@ -596,6 +597,79 @@ class QuestionnaireResponseValidatorTest { ) } + @Test + fun `validation fails for required item in a questionnaire repeating group item with answer value`() { + val questionnaire1 = + Questionnaire().apply { + url = "questionnaire-1" + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("group-1"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.GROUP, + ), + ) + .apply { + repeats = true + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("question-0"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.INTEGER, + ), + ) + .apply { required = true }, + ) + }, + ) + } + + val questionnaireResponse1 = + QuestionnaireResponse() + .apply { + questionnaire = "questionnaire-1" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")) + .apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = IntegerType(1) + }, + ) + }, + ) + }, + ) + + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")), + ) + }, + ) + } + .apply { packRepeatedGroups(questionnaire1) } + + runTest { + val result = + QuestionnaireResponseValidator.validateQuestionnaireResponse( + questionnaire1, + questionnaireResponse1, + context, + ) + + assertThat(result.keys).containsExactly("question-0", "group-1") + assertThat(result["question-0"]!!.first()).isInstanceOf(Invalid::class.java) + assertThat((result["question-0"]!!.first() as Invalid).getSingleStringValidationMessage()) + .isEqualTo("Missing answer for required field.") + } + } + @Test fun `check passes if questionnaire response matches questionnaire`() { QuestionnaireResponseValidator.checkQuestionnaireResponse( @@ -1653,6 +1727,69 @@ class QuestionnaireResponseValidatorTest { ) } + @Test + fun `check fails for wrong answer type to a nested question in repeating group`() { + assertException_checkQuestionnaireResponse_throwsIllegalArgumentException( + Questionnaire().apply { + url = "questionnaire-1" + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("group-1"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.GROUP, + ), + ) + .apply { + repeats = true + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("question-0"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.INTEGER, + ), + ), + ) + }, + ) + }, + QuestionnaireResponse().apply { + questionnaire = "questionnaire-1" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")) + .apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = IntegerType(1) + }, + ) + }, + ) + }, + ) + + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")) + .apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DecimalType(2.0) + }, + ) + }, + ) + }, + ) + }, + "Mismatching question type INTEGER and answer type decimal for question-0", + ) + } + private fun assertException_checkQuestionnaireResponse_throwsIllegalArgumentException( questionnaire: Questionnaire, questionnaireResponse: QuestionnaireResponse,