diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index f3d9659fb7..0696f56df0 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -156,6 +156,10 @@ dependencies { exclude(group = "ca.uhn.hapi.fhir") } + implementation(libs.hapi.fhir.validation) { exclude(module = "commons-logging") } + implementation(libs.hapi.fhir.validation.resources.r4) + implementation(libs.hapi.fhir.validation.resources.r5) + // Shared dependencies api(libs.bundles.datastore.kt) api(libs.glide) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt new file mode 100644 index 0000000000..8f224f6db5 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.di + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport +import ca.uhn.fhir.context.support.IValidationSupport +import ca.uhn.fhir.validation.FhirValidator +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +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.UnknownCodeSystemWarningValidationSupport +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator + +@Module +@InstallIn(SingletonComponent::class) +class FhirValidatorModule { + + @Provides + @Singleton + fun provideFhirValidator(): FhirValidator { + val fhirContext = FhirContext.forR4() + + val validationSupportChain = + ValidationSupportChain( + DefaultProfileValidationSupport(fhirContext), + InMemoryTerminologyServerValidationSupport(fhirContext), + CommonCodeSystemsTerminologyService(fhirContext), + UnknownCodeSystemWarningValidationSupport(fhirContext).apply { + setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING) + }, + ) + val instanceValidator = FhirInstanceValidator(validationSupportChain) + instanceValidator.isAssumeValidRestReferences = true + instanceValidator.invalidateCaches() + return fhirContext.newValidator().apply { registerValidatorModule(instanceValidator) } + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt index 371ab57e67..620e600e60 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt @@ -19,12 +19,14 @@ package org.smartregister.fhircore.engine.task import androidx.annotation.VisibleForTesting import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.util.TerserUtil +import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.FhirEngine import com.google.android.fhir.get import com.google.android.fhir.logicalId import com.google.android.fhir.search.search import java.util.Date import javax.inject.Inject +import javax.inject.Provider import javax.inject.Singleton import org.hl7.fhir.r4.model.ActivityDefinition import org.hl7.fhir.r4.model.Base @@ -56,7 +58,9 @@ import org.smartregister.fhircore.engine.configuration.event.EventType import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.extension.addResourceParameter import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.checkResourceValid import org.smartregister.fhircore.engine.util.extension.encodeResourceToString +import org.smartregister.fhircore.engine.util.extension.errorMessages import org.smartregister.fhircore.engine.util.extension.extractFhirpathDuration import org.smartregister.fhircore.engine.util.extension.extractFhirpathPeriod import org.smartregister.fhircore.engine.util.extension.extractId @@ -75,6 +79,7 @@ constructor( val transformSupportServices: TransformSupportServices, val defaultRepository: DefaultRepository, val fhirResourceUtil: FhirResourceUtil, + val fhirValidatorProvider: Provider, val workflowCarePlanGenerator: WorkflowCarePlanGenerator, ) { private val structureMapUtilities by lazy { @@ -193,7 +198,22 @@ constructor( val carePlanTasks = output.contained.filterIsInstance() - if (carePlanModified) saveCarePlan(output) + if (carePlanModified) { + fhirValidatorProvider + .get() + .checkResourceValid(output) + .filterNot { it.errorMessages.isBlank() } + .takeIf { it.isNotEmpty() } + ?.let { + val errors = buildString { + it.forEach { validationResult -> appendLine(validationResult.errorMessages) } + } + + throw IllegalStateException(errors) + } + + saveCarePlan(output) + } if (carePlanTasks.isNotEmpty()) { fhirResourceUtil.updateUpcomingTasksToDue( @@ -215,7 +235,9 @@ constructor( carePlan.contained.clear() // Save CarePlan only if it has activity, otherwise just save contained/dependent resources - if (output.hasActivity()) defaultRepository.create(true, carePlan) + if (output.hasActivity()) { + defaultRepository.create(true, carePlan) + } dependents.forEach { defaultRepository.create(true, it) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtension.kt new file mode 100644 index 0000000000..9512f89f17 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtension.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.util.extension + +import ca.uhn.fhir.validation.FhirValidator +import ca.uhn.fhir.validation.ResultSeverityEnum +import ca.uhn.fhir.validation.ValidationResult +import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.withContext +import org.hl7.fhir.r4.model.Resource +import org.smartregister.fhircore.engine.BuildConfig + +suspend fun FhirValidator.checkResourceValid( + vararg resource: Resource, + isDebug: Boolean = BuildConfig.BUILD_TYPE.contains("debug", ignoreCase = true), +): List { + if (!isDebug) return emptyList() + + return withContext(coroutineContext) { + resource.map { this@checkResourceValid.validateWithResult(it) } + } +} + +val ValidationResult.errorMessages + get() = buildString { + for (validationMsg in + messages.filter { it.severity.ordinal >= ResultSeverityEnum.WARNING.ordinal }) { + appendLine("${validationMsg.message} - ${validationMsg.locationString}") + } + } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt index 3f3e8a1543..a644109cab 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt @@ -21,6 +21,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.parser.IParser import ca.uhn.fhir.rest.gclient.ReferenceClientParam +import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.FhirEngine import com.google.android.fhir.SearchResult import com.google.android.fhir.get @@ -45,6 +46,7 @@ import java.util.Calendar import java.util.Date import java.util.UUID import javax.inject.Inject +import javax.inject.Provider import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -129,6 +131,8 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { @Inject lateinit var workflowCarePlanGenerator: WorkflowCarePlanGenerator + @Inject lateinit var fhirValidatorProvider: Provider + @Inject lateinit var fhirPathEngine: FHIRPathEngine @Inject lateinit var fhirEngine: FhirEngine @@ -169,6 +173,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { fhirPathEngine = fhirPathEngine, defaultRepository = defaultRepository, fhirResourceUtil = fhirResourceUtil, + fhirValidatorProvider = fhirValidatorProvider, workflowCarePlanGenerator = workflowCarePlanGenerator, ) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtensionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtensionTest.kt new file mode 100644 index 0000000000..c3b51f34c5 --- /dev/null +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtensionTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.util.extension + +import ca.uhn.fhir.validation.FhirValidator +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.spyk +import io.mockk.verify +import javax.inject.Inject +import kotlinx.coroutines.test.runTest +import org.hl7.fhir.instance.model.api.IBaseResource +import org.hl7.fhir.r4.model.CarePlan +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Reference +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.smartregister.fhircore.engine.robolectric.RobolectricTest + +@HiltAndroidTest +class FhirValidatorExtensionTest : RobolectricTest() { + + @get:Rule var hiltRule = HiltAndroidRule(this) + + @Inject lateinit var validator: FhirValidator + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun testCheckResourceValidRunsNoValidationWhenBuildTypeIsNotDebug() = runTest { + val basicResource = CarePlan() + val fhirValidatorSpy = spyk(validator) + val results = fhirValidatorSpy.checkResourceValid(basicResource, isDebug = false) + Assert.assertTrue(results.isEmpty()) + verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any()) } + verify(exactly = 0) { fhirValidatorSpy.validateWithResult(any()) } + } + + @Test + fun testCheckResourceValidValidatesResourceStructureWhenCarePlanResourceInvalid() = runTest { + val basicCarePlan = CarePlan() + val results = validator.checkResourceValid(basicCarePlan) + Assert.assertFalse(results.isEmpty()) + Assert.assertTrue( + results.any { + it.errorMessages.contains( + "CarePlan.status: minimum required = 1, but only found 0", + ignoreCase = true, + ) + }, + ) + Assert.assertTrue( + results.any { + it.errorMessages.contains( + "CarePlan.intent: minimum required = 1, but only found 0", + ignoreCase = true, + ) + }, + ) + } + + @Test + fun testCheckResourceValidValidatesReferenceType() = runTest { + val carePlan = + CarePlan().apply { + status = CarePlan.CarePlanStatus.ACTIVE + intent = CarePlan.CarePlanIntent.PLAN + subject = Reference("Task/unknown") + } + val results = validator.checkResourceValid(carePlan) + Assert.assertFalse(results.isEmpty()) + Assert.assertEquals(1, results.size) + Assert.assertTrue( + results + .first() + .errorMessages + .contains( + "The type 'Task' implied by the reference URL Task/unknown is not a valid Target for this element (must be one of [Group, Patient]) - CarePlan.subject", + ignoreCase = true, + ), + ) + } + + @Test + fun testCheckResourceValidValidatesReferenceWithNoType() = runTest { + val carePlan = + CarePlan().apply { + status = CarePlan.CarePlanStatus.ACTIVE + intent = CarePlan.CarePlanIntent.PLAN + subject = Reference("unknown") + } + val results = validator.checkResourceValid(carePlan) + Assert.assertFalse(results.isEmpty()) + Assert.assertEquals(1, results.size) + Assert.assertTrue( + results + .first() + .errorMessages + .contains( + "The syntax of the reference 'unknown' looks incorrect, and it should be checked - CarePlan.subject", + ignoreCase = true, + ), + ) + } + + @Test + fun testCheckResourceValidValidatesResourceCorrectly() = runTest { + val patient = Patient() + val carePlan = + CarePlan().apply { + status = CarePlan.CarePlanStatus.ACTIVE + intent = CarePlan.CarePlanIntent.PLAN + subject = Reference(patient) + } + val results = validator.checkResourceValid(carePlan) + Assert.assertFalse(results.isEmpty()) + Assert.assertEquals(1, results.size) + Assert.assertTrue(results.first().errorMessages.isBlank()) + } +} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 79949fc6c0..a2bf1d0ffd 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -111,7 +111,7 @@ dagger-hilt = "2.45" jetbrains-dokka = "1.8.20" navigation-safeargs = "2.4.2" diffplug-spotless = "6.19.0" - +hapi-fhir = "6.0.1" [libraries] accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist-flowlayout" } @@ -222,6 +222,9 @@ work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work-testing" } workflow = { group = "org.smartregister", name = "workflow", version.ref = "workflow" } xercesImpl = { group = "xerces", name = "xercesImpl", version.ref = "xercesImpl" } +hapi-fhir-validation = {group = "ca.uhn.hapi.fhir", name="hapi-fhir-validation", version.ref = "hapi-fhir"} +hapi-fhir-validation-resources-r4 = {group = "ca.uhn.hapi.fhir", name="hapi-fhir-validation-resources-r4", version.ref = "hapi-fhir"} +hapi-fhir-validation-resources-r5 = {group = "ca.uhn.hapi.fhir", name="hapi-fhir-validation-resources-r5", version.ref = "hapi-fhir"} [plugins] org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index ad061afbb0..dfb2647afd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.datacapture.mapping.ResourceMapper import com.google.android.fhir.datacapture.mapping.StructureMapExtractionContext import com.google.android.fhir.datacapture.validation.NotValidated @@ -35,6 +36,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import java.util.Date import java.util.UUID import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -67,7 +69,9 @@ import org.smartregister.fhircore.engine.util.extension.DEFAULT_PLACEHOLDER_PREF import org.smartregister.fhircore.engine.util.extension.appendOrganizationInfo import org.smartregister.fhircore.engine.util.extension.appendPractitionerInfo import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.checkResourceValid import org.smartregister.fhircore.engine.util.extension.cqfLibraryUrls +import org.smartregister.fhircore.engine.util.extension.errorMessages import org.smartregister.fhircore.engine.util.extension.extractByStructureMap import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid @@ -80,6 +84,7 @@ import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.engine.util.extension.updateLastUpdated import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.engine.util.helper.TransformSupportServices +import org.smartregister.fhircore.quest.BuildConfig import org.smartregister.fhircore.quest.R import timber.log.Timber @@ -94,6 +99,7 @@ constructor( val transformSupportServices: TransformSupportServices, val sharedPreferencesHelper: SharedPreferencesHelper, val fhirOperator: FhirOperator, + val fhirValidatorProvider: Provider, val fhirPathDataExtractor: FhirPathDataExtractor, ) : ViewModel() { @@ -207,6 +213,21 @@ constructor( context = context, ) + val validationResults = + bundle.entry + .map { it.resource } + .flatMap { fhirValidatorProvider.get().checkResourceValid(it) } + val validationFailures = validationResults.filterNot { it.errorMessages.isBlank() } + if (validationFailures.isNotEmpty()) { + val errorMessages = buildString { + validationFailures.map { it.errorMessages }.forEach(this::appendLine) + } + Timber.e(errorMessages) + context.showToast(context.getString(R.string.extracted_resources_validation_fail)) + setProgressState(QuestionnaireProgressState.ExtractionInProgress(false)) + return@launch + } + saveExtractedResources( bundle = bundle, questionnaire = questionnaire, @@ -232,6 +253,7 @@ constructor( val newBundle = bundle.copyBundle(currentQuestionnaireResponse) generateCarePlan( + context = context, subject = subject, bundle = newBundle, questionnaireConfig = questionnaireConfig, @@ -624,18 +646,28 @@ constructor( subject: Resource, bundle: Bundle, questionnaireConfig: QuestionnaireConfig, + context: Context, ) { - questionnaireConfig.planDefinitions?.forEach { planId -> - kotlin - .runCatching { - fhirCarePlanGenerator.generateOrUpdateCarePlan( - planDefinitionId = planId, - subject = subject, - data = bundle, - generateCarePlanWithWorkflowApi = questionnaireConfig.generateCarePlanWithWorkflowApi, - ) - } - .onFailure { Timber.e(it) } + val errorMessages = buildString { + questionnaireConfig.planDefinitions?.forEach { planId -> + kotlin + .runCatching { + fhirCarePlanGenerator.generateOrUpdateCarePlan( + planDefinitionId = planId, + subject = subject, + data = bundle, + generateCarePlanWithWorkflowApi = questionnaireConfig.generateCarePlanWithWorkflowApi, + ) + } + .onFailure { appendLine(it) } + } + } + + if (errorMessages.isNotBlank()) { + Timber.e(errorMessages) + if (BuildConfig.BUILD_TYPE.contains("debug", ignoreCase = true)) { + context.showToast(context.getString(R.string.error_occurred_generating_careplan)) + } } } diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index 921621d752..7c0998d95f 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -101,6 +101,8 @@ Questionnaire not found. Sync all questionnaires to fix No visits Questionnaire response invalid + Validation on extracted resources failed. Please check the logs + An error occurred generating CarePlan. Please check the logs https://smartregister.org/app-version Application Version Missing subject type on questionnaire. Provide Questionnaire.subjectType to resolve. diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index da667a46b7..015f3fcab3 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -18,6 +18,7 @@ package org.smartregister.fhircore.quest.ui.questionnaire import android.app.Application import androidx.test.core.app.ApplicationProvider +import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.mapping.ResourceMapper import com.google.android.fhir.db.ResourceNotFoundException @@ -40,6 +41,7 @@ import io.mockk.verify import java.util.Date import java.util.UUID import javax.inject.Inject +import javax.inject.Provider import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking @@ -62,6 +64,7 @@ import org.hl7.fhir.r4.model.Parameters import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.StringType @@ -93,6 +96,7 @@ import org.smartregister.fhircore.engine.util.extension.isToday import org.smartregister.fhircore.engine.util.extension.valueToString import org.smartregister.fhircore.engine.util.extension.yesterday import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor +import org.smartregister.fhircore.quest.BuildConfig import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireViewModel.Companion.CONTAINED_LIST_TITLE @@ -108,6 +112,8 @@ class QuestionnaireViewModelTest : RobolectricTest() { @Inject lateinit var sharedPreferencesHelper: SharedPreferencesHelper + @Inject lateinit var fhirValidatorProvider: Provider + @Inject lateinit var configService: ConfigService @Inject lateinit var resourceDataRulesExecutor: ResourceDataRulesExecutor @@ -182,6 +188,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { fhirCarePlanGenerator = fhirCarePlanGenerator, resourceDataRulesExecutor = resourceDataRulesExecutor, fhirPathDataExtractor = fhirPathDataExtractor, + fhirValidatorProvider = fhirValidatorProvider, fhirOperator = fhirOperator, ), ) @@ -203,9 +210,113 @@ class QuestionnaireViewModelTest : RobolectricTest() { } } + @Test + fun testHandleQuestionnaireSubmissionDoesNotSaveExtractedResourcesContainingInvalidWhenInDebug() { + mockkObject(ResourceMapper) + mockkObject(Timber) + val questionnaire = + extractionQuestionnaire().apply { extension = samplePatientRegisterQuestionnaire.extension } + val questionnaireResponse = extractionQuestionnaireResponse() + val actionParameters = emptyList() + val onSuccessfulSubmission = + spyk({ idsTypes: List, _: QuestionnaireResponse -> Timber.i(idsTypes.toString()) }) + coEvery { + ResourceMapper.extract( + questionnaire = questionnaire, + questionnaireResponse = questionnaireResponse, + structureMapExtractionContext = any(), + ) + } returns + Bundle().apply { + addEntry( + Bundle.BundleEntryComponent().apply { + resource = + patient.apply { + addLink().apply { + other = Reference("Group/1234") + type = Patient.LinkType.REFER + } + } + }, + ) + } + + questionnaireViewModel.handleQuestionnaireSubmission( + questionnaire = questionnaire, + currentQuestionnaireResponse = questionnaireResponse, + actionParameters = actionParameters, + context = context, + questionnaireConfig = questionnaireConfig, + onSuccessfulSubmission = onSuccessfulSubmission, + ) + + // Verify QuestionnaireResponse was validated + verify { + questionnaireViewModel.validateQuestionnaireResponse( + questionnaire, + questionnaireResponse, + context, + ) + } + // Verify perform extraction was invoked + coVerify { + questionnaireViewModel.performExtraction( + extractByStructureMap = true, + questionnaire = questionnaire, + questionnaireResponse = questionnaireResponse, + context = context, + ) + } + + if (BuildConfig.BUILD_TYPE.contains("debug", ignoreCase = true)) { + val errorMessageSlot = slot() + verify { Timber.e(capture(errorMessageSlot)) } + Assert.assertTrue( + errorMessageSlot.captured.contains( + "The type 'Group' implied by the reference URL Group/1234 is not a valid Target for this element (must be one of [Patient, RelatedPerson]) - Patient.link[0].other", + ignoreCase = true, + ), + ) + + coVerify(exactly = 0) { + questionnaireViewModel.saveExtractedResources( + bundle = any(), + questionnaire = questionnaire, + questionnaireConfig = questionnaireConfig, + currentQuestionnaireResponse = questionnaireResponse, + ) + } + coVerify(exactly = 0) { + questionnaireViewModel.updateResourcesLastUpdatedProperty( + actionParameters, + ) + } + + coVerify(exactly = 0) { onSuccessfulSubmission(any(), questionnaireResponse) } + } else { + coVerify { + questionnaireViewModel.saveExtractedResources( + bundle = any(), + questionnaire = questionnaire, + questionnaireConfig = questionnaireConfig, + currentQuestionnaireResponse = questionnaireResponse, + ) + } + coVerify { + questionnaireViewModel.updateResourcesLastUpdatedProperty( + actionParameters, + ) + } + + coVerify { onSuccessfulSubmission(any(), questionnaireResponse) } + } + unmockkObject(Timber) + unmockkObject(ResourceMapper) + } + // TODO Write integration test for QuestionnaireActivity to compliment this unit test; @Test - fun testHandleQuestionnaireSubmission() = runTest { + fun testHandleQuestionnaireSubmission() { mockkObject(ResourceMapper) val questionnaire = extractionQuestionnaire().apply { @@ -329,6 +440,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { subject = capture(subjectSlot), bundle = capture(bundleSlot), questionnaireConfig = questionnaireConfig, + context = context, ) questionnaireViewModel.executeCql( @@ -684,7 +796,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { Bundle().apply { addEntry(Bundle.BundleEntryComponent().apply { resource = patient }) } val questionnaireConfig = questionnaireConfig.copy(planDefinitions = listOf("planDefId")) - questionnaireViewModel.generateCarePlan(patient, bundle, questionnaireConfig) + questionnaireViewModel.generateCarePlan(patient, bundle, questionnaireConfig, context) coVerify { fhirCarePlanGenerator.generateOrUpdateCarePlan( planDefinitionId = "planDefId",