diff --git a/android/build.gradle b/android/build.gradle index dc07d7c96d4..d0b35770683 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -35,6 +35,14 @@ allprojects { url = uri("/Users/ndegwamartin/.m2.dev/fhirsdk") } } + + configurations { + configureEach { + resolutionStrategy { + force "ca.uhn.hapi.fhir:hapi-fhir-validation:6.0.1" + } + } + } } subprojects { diff --git a/android/engine/build.gradle b/android/engine/build.gradle index e4caabf3413..9a24075ffd4 100644 --- a/android/engine/build.gradle +++ b/android/engine/build.gradle @@ -112,12 +112,6 @@ android { } } } - - configurations.all { - resolutionStrategy { - force "ca.uhn.hapi.fhir:org.hl7.fhir.utilities:5.5.7" - } - } } dependencies { @@ -147,6 +141,9 @@ dependencies { implementation(group: "com.github.java-json-tools", name: "msg-simple", version: "1.2"); implementation 'org.codehaus.woodstox:woodstox-core-asl:4.4.1' implementation "ca.uhn.hapi.fhir:hapi-fhir-android:5.4.0" + implementation ("ca.uhn.hapi.fhir:hapi-fhir-validation:6.0.1"){ + exclude module: "commons-logging" + } implementation 'org.opencds.cqf.cql:engine:1.5.4' implementation 'org.opencds.cqf.cql:engine.fhir:1.5.4' implementation 'org.opencds.cqf.cql:evaluator:1.4.2' 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 00000000000..728dc259cba --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/FhirValidatorModule.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2021 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.validatorResourceFetcher + // instanceValidator.setCustomExtensionDomains() + instanceValidator.invalidateCaches() + return fhirContext.newValidator().apply { registerValidatorModule(instanceValidator) } + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt index 129fc52b518..8750b518342 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt @@ -58,5 +58,6 @@ constructor( } } ) + override fun getFhirEngine(): FhirEngine = engine } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/ExtractionProgress.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/ExtractionProgress.kt index c02d36d39f0..9b4dba28df3 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/ExtractionProgress.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/ExtractionProgress.kt @@ -20,5 +20,5 @@ import org.hl7.fhir.r4.model.Resource sealed class ExtractionProgress { class Success(val extras: List? = null) : ExtractionProgress() - object Failed : ExtractionProgress() + class Failed(val errorMessages: String? = null) : ExtractionProgress() } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt index bbb3359b20d..b084ea76c0a 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt @@ -356,6 +356,13 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList if (result is ExtractionProgress.Success) { onPostSave(true, questionnaireResponse, result.extras) } else { + result as ExtractionProgress.Failed + AlertDialogue.showErrorAlert( + this, + result.errorMessages ?: "", + getString(R.string.questionnaire_alert_extraction_fail) + ) + onPostSave(false, questionnaireResponse) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt index e4e64a7bc84..83674496454 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt @@ -18,6 +18,8 @@ package org.smartregister.fhircore.engine.ui.questionnaire import android.content.Context import android.content.Intent +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -25,6 +27,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.rest.gclient.TokenClientParam import ca.uhn.fhir.rest.param.ParamPrefixEnum +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.datacapture.mapping.StructureMapExtractionContext @@ -82,8 +85,10 @@ import org.smartregister.fhircore.engine.util.USER_INFO_SHARED_PREFERENCE_KEY import org.smartregister.fhircore.engine.util.extension.addTags import org.smartregister.fhircore.engine.util.extension.asReference import org.smartregister.fhircore.engine.util.extension.assertSubject +import org.smartregister.fhircore.engine.util.extension.checkResourceValid import org.smartregister.fhircore.engine.util.extension.cqfLibraryIds import org.smartregister.fhircore.engine.util.extension.deleteRelatedResources +import org.smartregister.fhircore.engine.util.extension.errorMessages import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.filterByResourceTypeId @@ -110,14 +115,20 @@ constructor( val dispatcherProvider: DispatcherProvider, val sharedPreferencesHelper: SharedPreferencesHelper, val libraryEvaluatorProvider: Provider, + val fhirValidatorProvider: Provider, var tracer: PerformanceReporter ) : ViewModel() { @Inject lateinit var fhirCarePlanGenerator: FhirCarePlanGenerator - val extractionProgress = MutableLiveData() + private val _extractionProgress = MutableLiveData() + val extractionProgress: LiveData = _extractionProgress + val questionnaireResponseLiveData = MutableLiveData(null) - val extractionProgressMessage = MutableLiveData() + @Suppress("PropertyName") + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val _extractionProgressMessage = MutableLiveData() + val extractionProgressMessage: LiveData = _extractionProgressMessage var editQuestionnaireResponse: QuestionnaireResponse? = null @@ -334,7 +345,23 @@ constructor( Timber.w( "${questionnaire.name}(${questionnaire.logicalId}) is experimental and not save any data" ) - } else saveBundleResources(bundle) + } else { + val unsuccessfulValidationResults = + bundle.entry + .flatMap { fhirValidatorProvider.get().checkResourceValid(it.resource) } + .filter { it.errorMessages.isNotBlank() } + + if (unsuccessfulValidationResults.isNotEmpty()) { + val mergedErrorMessages = buildString { + unsuccessfulValidationResults.forEach { appendLine(it.errorMessages) } + } + Timber.e(mergedErrorMessages) + _extractionProgress.postValue(ExtractionProgress.Failed(mergedErrorMessages)) + return@launch + } + + saveBundleResources(bundle) + } if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) { questionnaireResponse.retainMetadata(editQuestionnaireResponse!!) @@ -357,7 +384,7 @@ constructor( } tracer.stopTrace(QUESTIONNAIRE_TRACE) viewModelScope.launch(Dispatchers.Main) { - extractionProgress.postValue(ExtractionProgress.Success(extras)) + _extractionProgress.postValue(ExtractionProgress.Success(extras)) } } } @@ -398,7 +425,7 @@ constructor( .runCatching { fhirCarePlanGenerator.generateCarePlan(planId, subject, data) } .onFailure { Timber.e(it) - extractionProgressMessage.postValue("Error extracting care plan. ${it.message}") + _extractionProgressMessage.postValue("Error extracting care plan. ${it.message}") } } } @@ -420,7 +447,7 @@ constructor( libraryEvaluatorProvider.get().runCqlLibrary(it, patient, data, defaultRepository) } .forEach { output -> - if (output.isNotEmpty()) extractionProgressMessage.postValue(output.joinToString("\n")) + if (output.isNotEmpty()) _extractionProgressMessage.postValue(output.joinToString("\n")) } } } @@ -466,7 +493,6 @@ constructor( questionnaire.useContext.filter { it.hasValueCodeableConcept() }.forEach { it.valueCodeableConcept.coding.forEach { questionnaireResponse.meta.addTag(it) } } - defaultRepository.addOrUpdate(true, questionnaireResponse) } @@ -615,10 +641,6 @@ constructor( return tasks.filter { it.status in arrayOf(TaskStatus.READY, TaskStatus.INPROGRESS) } } - fun saveResource(resource: Resource) { - viewModelScope.launch { defaultRepository.save(resource = resource) } - } - fun extractRelevantObservation( resource: Bundle, questionnaireLogicalId: String, 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 00000000000..72077584cc5 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirValidatorExtension.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 + +suspend fun FhirValidator.checkResourceValid( + vararg resource: Resource, +): List { + 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/main/res/values/strings.xml b/android/engine/src/main/res/values/strings.xml index df308091865..9fe69f92e77 100644 --- a/android/engine/src/main/res/values/strings.xml +++ b/android/engine/src/main/res/values/strings.xml @@ -71,6 +71,7 @@ Yes Given details have validation errors. Resolve errors and submit again Validation Failed + Extraction did not succeed Ok Username Password diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt index 2b581b1c475..30f901a0b28 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt @@ -31,6 +31,7 @@ import androidx.fragment.app.commitNow import androidx.test.core.app.ApplicationProvider import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum +import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.datacapture.QuestionnaireFragment import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator.checkQuestionnaireResponse import dagger.hilt.android.testing.BindValue @@ -38,6 +39,7 @@ import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -45,10 +47,15 @@ import io.mockk.runs import io.mockk.spyk import io.mockk.unmockkObject import io.mockk.verify +import javax.inject.Inject +import javax.inject.Provider import kotlin.test.assertFailsWith import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest +import org.hl7.fhir.r4.model.CanonicalType +import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Encounter @@ -78,8 +85,6 @@ import org.smartregister.fhircore.engine.trace.FakePerformanceReporter import org.smartregister.fhircore.engine.trace.PerformanceReporter import org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireActivity.Companion.QUESTIONNAIRE_FRAGMENT_TAG import org.smartregister.fhircore.engine.util.AssetUtil -import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider -import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.distinctifyLinkId import org.smartregister.fhircore.engine.util.extension.encodeResourceToString @@ -100,8 +105,6 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { @get:Rule var coroutinesTestRule = CoroutineTestRule() - val dispatcherProvider: DispatcherProvider = spyk(DefaultDispatcherProvider()) - private val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() @BindValue @JvmField val performanceReporter: PerformanceReporter = FakePerformanceReporter() @@ -110,25 +113,29 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { @BindValue val syncBroadcaster = mockk() - @BindValue - val questionnaireViewModel: QuestionnaireViewModel = - spyk( - QuestionnaireViewModel( - fhirEngine = mockk(), - defaultRepository = mockk { coEvery { addOrUpdate(true, any()) } just runs }, - configurationRegistry = mockk(), - transformSupportServices = mockk(), - dispatcherProvider = dispatcherProvider, - sharedPreferencesHelper = mockk(), - libraryEvaluatorProvider = { mockk() }, - tracer = FakePerformanceReporter() - ) - ) + @Inject lateinit var fhirValidatorProvider: Provider + + @BindValue lateinit var questionnaireViewModel: QuestionnaireViewModel @Before fun setUp() { // TODO Proper set up hiltRule.inject() + questionnaireViewModel = + spyk( + QuestionnaireViewModel( + fhirEngine = mockk(), + defaultRepository = mockk { coEvery { addOrUpdate(true, any()) } just runs }, + configurationRegistry = mockk(), + transformSupportServices = mockk(), + dispatcherProvider = coroutinesTestRule.testDispatcherProvider, + sharedPreferencesHelper = mockk(), + libraryEvaluatorProvider = { mockk() }, + fhirValidatorProvider = fhirValidatorProvider, + tracer = FakePerformanceReporter() + ) + ) + ApplicationProvider.getApplicationContext().apply { setTheme(R.style.AppTheme) } intent = Intent().apply { @@ -508,6 +515,53 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { } } + @Test + fun testHandleQuestionnaireSubmitWithExtractionProgressFailureShowsErrorDialog() = runTest { + val sampleExtractedCarePlan = CarePlan() + coEvery { questionnaireViewModel.performExtraction(any(), any(), any()) } returns + org.hl7.fhir.r4.model.Bundle().apply { + addEntry().apply { resource = sampleExtractedCarePlan } + } + val questionnaire = + Questionnaire().apply { + addExtension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap", + CanonicalType("1234") + ) + } + + ReflectionHelpers.setField( + questionnaireViewModel, + "questionnaireConfig", + QuestionnaireConfig("form", "title", "form-id", setPractitionerDetails = false) + ) + ReflectionHelpers.setField(questionnaireActivity, "questionnaire", questionnaire) + questionnaireActivity.handleQuestionnaireSubmit() + advanceUntilIdle() + + val dialog = shadowOf(ShadowAlertDialog.getLatestDialog()) + val alertDialog = ReflectionHelpers.getField(dialog, "realDialog") + + Assert.assertNotNull(questionnaireViewModel.extractionProgress.value) + Assert.assertTrue(questionnaireViewModel.extractionProgress.value is ExtractionProgress.Failed) + + val alertDialogText = alertDialog.findViewById(R.id.tv_alert_message)!!.text + Assert.assertTrue( + "CarePlan.status: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/CarePlan) - CarePlan" in + alertDialogText + ) + Assert.assertTrue( + "CarePlan.intent: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/CarePlan) - CarePlan" in + alertDialogText + ) + Assert.assertTrue( + "CarePlan.subject: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/CarePlan) - CarePlan" in + alertDialogText + ) + + coVerify(exactly = 0) { questionnaireViewModel.defaultRepository.addOrUpdate(resource = any()) } + } + @Test fun testOnClickSaveButtonShouldShowSubmitConfirmationAlert() { ReflectionHelpers.setField( @@ -599,7 +653,7 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { @Test fun testPostSaveSuccessfulWithExtractionMessageShouldShowAlert() = runTest { - questionnaireActivity.questionnaireViewModel.extractionProgressMessage.postValue("ABC") + questionnaireActivity.questionnaireViewModel._extractionProgressMessage.postValue("ABC") questionnaireActivity.postSaveSuccessful(QuestionnaireResponse()) val dialog = shadowOf(ShadowAlertDialog.getLatestDialog()) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt index cc83d6f510c..a833d2ff3b5 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -23,6 +23,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser +import ca.uhn.fhir.validation.FhirValidator import com.google.android.fhir.FhirEngine import com.google.android.fhir.SearchResult import com.google.android.fhir.datacapture.mapping.ResourceMapper @@ -47,6 +48,7 @@ import io.mockk.verify import java.util.Calendar import java.util.Date import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -110,6 +112,8 @@ class QuestionnaireViewModelTest : RobolectricTest() { @Inject lateinit var sharedPreferencesHelper: SharedPreferencesHelper + @Inject lateinit var fhirValidatorProvider: Provider + @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) var instantTaskExecutorRule = InstantTaskExecutorRule() @@ -175,6 +179,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { dispatcherProvider = defaultRepo.dispatcherProvider, sharedPreferencesHelper = sharedPreferencesHelper, libraryEvaluatorProvider = { libraryEvaluator }, + fhirValidatorProvider = fhirValidatorProvider, tracer = FakePerformanceReporter() ) ) @@ -470,6 +475,96 @@ class QuestionnaireViewModelTest : RobolectricTest() { unmockkObject(ResourceMapper) } + @Test + fun testExtractAndSaveResourcesWithTargetStructureMapShouldCallSaveForValidCarePlan() = runTest { + mockkObject(ResourceMapper) + val patient = samplePatient() + val sampleExtractedCarePlan = + CarePlan().apply { + status = CarePlan.CarePlanStatus.ACTIVE + intent = CarePlan.CarePlanIntent.PLAN + subject = patient.asReference() + } + + coEvery { ResourceMapper.extract(any(), any(), any()) } returns + Bundle().apply { addEntry().apply { this.resource = sampleExtractedCarePlan } } + + val questionnaire = + Questionnaire().apply { + addUseContext().apply { + code = Coding().apply { code = "focus" } + value = CodeableConcept().apply { addCoding().apply { code = "1234567" } } + } + addExtension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap", + CanonicalType("1234") + ) + } + + Shadows.shadowOf(Looper.getMainLooper()).idle() + + val questionnaireResponse = QuestionnaireResponse() + + questionnaireViewModel.extractAndSaveResources( + context = context, + resourceId = "12345", + questionnaireResponse = questionnaireResponse, + questionnaire = questionnaire + ) + + coVerify { defaultRepo.addOrUpdate(resource = sampleExtractedCarePlan) } + coVerify { defaultRepo.addOrUpdate(resource = questionnaireResponse) } + coVerify(timeout = 10000) { ResourceMapper.extract(any(), any(), any()) } + unmockkObject(ResourceMapper) + } + + @Test + fun testExtractAndSaveResourcesWithTargetStructureMapDoesNotSaveWhenExtractedResourceHasValidationError() = + runTest { + mockkObject(ResourceMapper) + val sampleExtractedCarePlan = CarePlan() + + coEvery { ResourceMapper.extract(any(), any(), any()) } returns + Bundle().apply { + addEntry(Bundle.BundleEntryComponent().apply { resource = sampleExtractedCarePlan }) + } + val questionnaire = + Questionnaire().apply { + addExtension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap", + CanonicalType("1234") + ) + } + val questionnaireResponse = QuestionnaireResponse() + questionnaireViewModel.extractAndSaveResources( + context = context, + resourceId = "12345", + questionnaireResponse = questionnaireResponse, + questionnaire = questionnaire + ) + + coVerify(exactly = 0) { defaultRepo.addOrUpdate(resource = sampleExtractedCarePlan) } + coVerify(exactly = 0) { defaultRepo.addOrUpdate(resource = questionnaireResponse) } + + val extractionProgressValue = questionnaireViewModel.extractionProgress.value + Assert.assertNotNull(extractionProgressValue) + extractionProgressValue!! + Assert.assertTrue(extractionProgressValue is ExtractionProgress.Failed) + extractionProgressValue as ExtractionProgress.Failed + Assert.assertNotNull(extractionProgressValue.errorMessages) + Assert.assertTrue( + "CarePlan.status: minimum required = 1" in extractionProgressValue.errorMessages!! + ) + Assert.assertTrue( + "CarePlan.intent: minimum required = 1" in extractionProgressValue.errorMessages!! + ) + Assert.assertTrue( + "CarePlan.subject: minimum required = 1" in extractionProgressValue.errorMessages!! + ) + + unmockkObject(ResourceMapper) + } + @Test fun testExtractAndSaveResourcesWithExtractionExtensionAndNullResourceShouldAssignTags() { val questionnaire = @@ -630,13 +725,6 @@ class QuestionnaireViewModelTest : RobolectricTest() { } } - @Test - fun testSaveResourceShouldVerifyResourceSaveMethodCall() { - coEvery { defaultRepo.save(any()) } returns Unit - questionnaireViewModel.saveResource(mockk()) - coVerify(exactly = 1) { defaultRepo.save(any()) } - } - @Test fun testGetPopulationResourcesShouldReturnListOfResources() = runTest { coEvery { questionnaireViewModel.loadPatient("2") } returns Patient().apply { id = "2" } @@ -1093,14 +1181,6 @@ class QuestionnaireViewModelTest : RobolectricTest() { coVerify { libraryEvaluator.runCqlLibrary("123", any(), any(), any()) } } - @Test - fun testSaveResourceShouldCallDefaultRepositorySave() { - val sourcePatient = Patient().apply { id = "test_patient_1_id" } - questionnaireViewModel.saveResource(sourcePatient) - - coVerify { defaultRepo.save(sourcePatient) } - } - @Test fun `getStructureMapProvider() should return valid provider`() { Assert.assertNull(questionnaireViewModel.structureMapProvider) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/helper/InstanceValidatorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/helper/InstanceValidatorTest.kt new file mode 100644 index 00000000000..c3edcc12e22 --- /dev/null +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/helper/InstanceValidatorTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2021 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.helper + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.validation.FhirValidator +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.smartregister.fhircore.engine.di.errorMessages + +class InstanceValidatorTest { + + private lateinit var validator: FhirValidator + + @Before + fun setUp() { + val ctx = FhirContext.forR4Cached() + validator = ctx.newValidator() + + val validatorModule = FhirInstanceValidator(ctx) + validator.registerValidatorModule(validatorModule) + } + + @Test + fun validateCarePlan() { + val bundle = + "{\"resourceType\": \"Bundle\", \"type\": \"collection\", \"entry\": [{\"resource\": {\"resourceType\": \"CarePlan\", \"intent\": \"plan\", \"subject\": {\"reference\": \"Patient/TEST_PATIENT\"}}}]}" + val carePlan = + "{\"resourceType\": \"CarePlan\", \"status\": \"active\", \"intent\": \"plan\", \"subject\": {\"reference\": \"Patient/TEST_PATIENT\"}}" + val result = validator.validateWithResult(carePlan) + + // The result object now contains the validation results + // The result object now contains the validation results + if (!result.isSuccessful) print(result.errorMessages) + Assert.assertTrue(result.isSuccessful) + } +} diff --git a/android/quest/build.gradle b/android/quest/build.gradle index 334ad41e5d6..952ebaa425f 100644 --- a/android/quest/build.gradle +++ b/android/quest/build.gradle @@ -198,13 +198,6 @@ android { versionName "0.0.1" } } - - configurations.all { - resolutionStrategy { - force "ca.uhn.hapi.fhir:org.hl7.fhir.utilities:5.5.7" - } - } - } dependencies {