From ee6d05e4a5e1ad01316e98b09074e487fabe33ca Mon Sep 17 00:00:00 2001 From: Rachel Murabula <110402503+Raynafs@users.noreply.github.com> Date: Sat, 1 Jun 2024 10:56:28 +0300 Subject: [PATCH] Load resources like Questionnaire, StructureMap,PlanDefinition from resources folder when using appId with `/debug` (#3065) * Update loading on resources locally * Load SMS, PD and Qs locally from configJson * Add resources to DB * Run spotlessApply * Update resources to DB * Fix resource addition to DB * Fix some resources not updated * Add tests for loading local resources * Add docs * Fix spotless errors * Beautify docs * Fix coverage * Test exception --------- Co-authored-by: SebaMutuku Co-authored-by: Sebastian <36365043+SebaMutuku@users.noreply.github.com> --- .../configuration/ConfigurationRegistry.kt | 59 ++-- .../patient_profile_config.json | 0 .../household_register_config.json | 0 .../patient_register_config.json | 0 .../questionnaire/sample_questionnaire.json | 87 ++++++ .../fhircore/engine/app/fakes/Faker.kt | 2 +- .../ConfigurationRegistryTest.kt | 291 +++++++++++++----- .../sample_patient_registration.json | 0 .../{ => resources}/test-questionnaire.json | 0 .../QuestionnaireActivityTest.kt | 4 +- .../QuestionnaireViewModelTest.kt | 4 +- .../configuring/working-in-debug-mode.mdx | 20 ++ 12 files changed, 359 insertions(+), 108 deletions(-) rename android/engine/src/test/assets/configs/app/{ => profiles}/patient_profile_config.json (100%) rename android/engine/src/test/assets/configs/app/{ => registers}/household_register_config.json (100%) rename android/engine/src/test/assets/configs/app/{ => registers}/patient_register_config.json (100%) create mode 100644 android/engine/src/test/assets/configs/app/resources/questionnaire/sample_questionnaire.json rename android/quest/src/main/assets/{ => resources}/sample_patient_registration.json (100%) rename android/quest/src/main/assets/{ => resources}/test-questionnaire.json (100%) create mode 100644 docs/engineering/android-app/configuring/working-in-debug-mode.mdx diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index 5da34c0be0..cf985bf633 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -18,7 +18,9 @@ package org.smartregister.fhircore.engine.configuration import android.content.Context import android.database.SQLException +import ca.uhn.fhir.context.ConfigurationException import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.parser.DataFormatException import com.google.android.fhir.FhirEngine import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get @@ -38,7 +40,6 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import okhttp3.RequestBody.Companion.toRequestBody import okio.ByteString.Companion.decodeBase64 -import org.apache.commons.lang3.StringUtils import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.Bundle @@ -298,7 +299,7 @@ constructor( } } - private suspend fun populateConfigurationsMap( + suspend fun populateConfigurationsMap( context: Context, composition: Composition, loadFromAssets: Boolean, @@ -308,21 +309,33 @@ constructor( if (loadFromAssets) { retrieveAssetConfigs(context, appId).forEach { fileName -> // Create binary config from asset and add to map, skip composition resource - // Use file name as the key. Conventionally navigation configs MUST end with - // "_config." + // Use file name as the key. Conventionally configs MUST end with _config." // File names in asset should match the configType/id (MUST be unique) in the config JSON + // Resource configs are saved to the database if (!fileName.equals(String.format(COMPOSITION_CONFIG_PATH, appId), ignoreCase = true)) { - val configKey = - fileName - .lowercase(Locale.ENGLISH) - .substring( - fileName.indexOfLast { it == '/' }.plus(1), - fileName.lastIndexOf(CONFIG_SUFFIX), - ) - .camelCase() - val configJson = context.assets.open(fileName).bufferedReader().readText() - configsJsonMap[configKey] = configJson + if (fileName.contains(RESOURCES_PATH)) { + try { + val resource = configJson.decodeResourceFromString() + if (resource.resourceType != null) { + addOrUpdate(resource) + } + } catch (configurationException: ConfigurationException) { + Timber.e("Error parsing FHIR resource", configurationException) + } catch (dataFormatException: DataFormatException) { + Timber.e("Error parsing FHIR resource", dataFormatException) + } + } else { + val configKey = + fileName + .lowercase(Locale.ENGLISH) + .substring( + fileName.indexOfLast { it == '/' }.plus(1), + fileName.lastIndexOf(CONFIG_SUFFIX), + ) + .camelCase() + configsJsonMap[configKey] = configJson + } } } } else { @@ -422,14 +435,17 @@ constructor( } else { val chunkedResourceIdList = entry.value.chunked(MANIFEST_PROCESSOR_BATCH_SIZE) - chunkedResourceIdList.forEach { parentIt -> + chunkedResourceIdList.forEach { sectionComponents -> Timber.d( - "Fetching config resource ${entry.key}: with ids ${StringUtils.join(parentIt,",")}", + "Fetching config resource ${entry.key}: with ids ${sectionComponents.joinToString(",")}", ) processCompositionManifestResources( - entry.key, - parentIt.map { sectionComponent -> sectionComponent.focus.extractId() }, - patientRelatedResourceTypes, + resourceType = entry.key, + resourceIdList = + sectionComponents.map { sectionComponent -> + sectionComponent.focus.extractId() + }, + patientRelatedResourceTypes = patientRelatedResourceTypes, ) } } @@ -622,7 +638,7 @@ constructor( this.apply { url = url - ?: "${openSrpApplication?.getFhirServerHost().toString()?.trimEnd { it == '/' }}/${this.referenceValue()}" + ?: """${openSrpApplication?.getFhirServerHost()?.toString()?.trimEnd { it == '/' }}/${this.referenceValue()}""" } fun writeToFile(resource: Resource): File { @@ -811,13 +827,14 @@ constructor( const val TYPE_REFERENCE_DELIMITER = "/" const val DEFAULT_COUNT = 200 const val PAGINATION_NEXT = "next" + const val RESOURCES_PATH = "resources/" /** * The list of resources whose types can be synced down as part of the Composition configs. * These are hardcoded as they are not meant to be easily configurable to avoid config vs data * sync issues */ - val FILTER_RESOURCE_LIST = + private val FILTER_RESOURCE_LIST = listOf( ResourceType.Questionnaire.name, ResourceType.StructureMap.name, diff --git a/android/engine/src/test/assets/configs/app/patient_profile_config.json b/android/engine/src/test/assets/configs/app/profiles/patient_profile_config.json similarity index 100% rename from android/engine/src/test/assets/configs/app/patient_profile_config.json rename to android/engine/src/test/assets/configs/app/profiles/patient_profile_config.json diff --git a/android/engine/src/test/assets/configs/app/household_register_config.json b/android/engine/src/test/assets/configs/app/registers/household_register_config.json similarity index 100% rename from android/engine/src/test/assets/configs/app/household_register_config.json rename to android/engine/src/test/assets/configs/app/registers/household_register_config.json diff --git a/android/engine/src/test/assets/configs/app/patient_register_config.json b/android/engine/src/test/assets/configs/app/registers/patient_register_config.json similarity index 100% rename from android/engine/src/test/assets/configs/app/patient_register_config.json rename to android/engine/src/test/assets/configs/app/registers/patient_register_config.json diff --git a/android/engine/src/test/assets/configs/app/resources/questionnaire/sample_questionnaire.json b/android/engine/src/test/assets/configs/app/resources/questionnaire/sample_questionnaire.json new file mode 100644 index 0000000000..a77db44add --- /dev/null +++ b/android/engine/src/test/assets/configs/app/resources/questionnaire/sample_questionnaire.json @@ -0,0 +1,87 @@ +{ + "resourceType": "Questionnaire", + "id": "3440", + "language": "en", + "name": "G6PD Test Photo Result", + "title": "G6PD Test Photo Result", + "status": "active", + "subjectType": [ + "Patient" + ], + "publisher": "ONA-Systems", + "useContext": [ + { + "code": { + "system": "http://hl7.org/fhir/codesystem-usage-context-type.html", + "code": "focus" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.ona.io", + "code": "000002", + "display": "G6PD Test Photo Results" + } + ] + } + } + ], + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap", + "valueCanonical": "https://fhir.labs.smartregister.org/StructureMap/5875" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-library", + "valueCanonical": "Library/46831" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-library", + "valueCanonical": "Library/46823" + } + ], + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationExtract", + "valueBoolean": true + } + ], + "linkId": "result_type", + "code": [ + { + "system": "http://fhir.ona.io", + "code": "000001", + "display": "G6PD Result Type" + } + ], + "text": "G6PD Result Type", + "type": "choice", + "required": true, + "answerOption": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "410680006", + "display": "Number" + } + }, + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "405358009", + "display": "Error" + } + }, + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "385432009", + "display": "N/A" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/app/fakes/Faker.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/app/fakes/Faker.kt index 8e6b3d91b4..cd835dcfed 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/app/fakes/Faker.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/app/fakes/Faker.kt @@ -103,7 +103,7 @@ object Faker { context = ApplicationProvider.getApplicationContext(), openSrpApplication = object : OpenSrpApplication() { - override fun getFhirServerHost(): URL? { + override fun getFhirServerHost(): URL { return URL("http://my_test_fhirbase_url/fhir/") } }, diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt index 5edfc73d71..0c4d080bed 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt @@ -26,19 +26,18 @@ import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.logicalId import com.google.android.fhir.search.Search import com.google.common.reflect.TypeToken -import com.google.gson.GsonBuilder import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import io.mockk.spyk -import java.io.File import java.net.URL import javax.inject.Inject +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json import okhttp3.RequestBody import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.Bundle @@ -48,21 +47,24 @@ import org.hl7.fhir.r4.model.Composition.SectionComponent import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.ListResource +import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType import org.junit.Assert import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.ArgumentMatchers import org.smartregister.fhircore.engine.OpenSrpApplication -import org.smartregister.fhircore.engine.app.AppConfigService import org.smartregister.fhircore.engine.app.fakes.Faker import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry.Companion.MANIFEST_PROCESSOR_BATCH_SIZE import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry.Companion.PAGINATION_NEXT import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration +import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.configuration.register.RegisterConfiguration import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService @@ -73,7 +75,6 @@ import org.smartregister.fhircore.engine.rule.CoroutineTestRule import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper -import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.getPayload import org.smartregister.fhircore.engine.util.extension.second @@ -81,52 +82,51 @@ import org.smartregister.fhircore.engine.util.extension.second class ConfigurationRegistryTest : RobolectricTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) - @kotlinx.coroutines.ExperimentalCoroutinesApi - @get:Rule(order = 1) - val coroutineRule = CoroutineTestRule() + @get:Rule(order = 1) val coroutineRule = CoroutineTestRule() @Inject lateinit var fhirEngine: FhirEngine @Inject lateinit var dispatcherProvider: DispatcherProvider + + @Inject lateinit var sharedPreferencesHelper: SharedPreferencesHelper + + @Inject lateinit var configService: ConfigService + + @Inject lateinit var json: Json + + @Inject lateinit var fhirContext: FhirContext + private val context: Context = ApplicationProvider.getApplicationContext() private val fhirResourceService = mockk() private lateinit var fhirResourceDataSource: FhirResourceDataSource private lateinit var configRegistry: ConfigurationRegistry - private lateinit var mockedContext: Context - private lateinit var mockedJsonParser: IParser @Before @kotlinx.coroutines.ExperimentalCoroutinesApi fun setUp() { hiltRule.inject() fhirResourceDataSource = spyk(FhirResourceDataSource(fhirResourceService)) - val sharedPreferencesHelper = - SharedPreferencesHelper(context, GsonBuilder().setLenient().create()) configRegistry = ConfigurationRegistry( - fhirEngine, - fhirResourceDataSource, - sharedPreferencesHelper, - dispatcherProvider, - AppConfigService(context), - Faker.json, + fhirEngine = fhirEngine, + fhirResourceDataSource = fhirResourceDataSource, + sharedPreferencesHelper = sharedPreferencesHelper, + dispatcherProvider = dispatcherProvider, + configService = configService, + json = json, context = ApplicationProvider.getApplicationContext(), openSrpApplication = object : OpenSrpApplication() { - override fun getFhirServerHost(): URL? { + override fun getFhirServerHost(): URL { return URL("http://my_test_fhirbase_url/fhir/") } }, ) - mockedContext = mockk() - mockedJsonParser = mockk() - configRegistry.setNonProxy(false) - Assert.assertNotNull(configRegistry) } @Test fun testRetrieveResourceBundleConfigurationReturnsNull() { - configRegistry.configsJsonMap["stringsEn"] = "name.title=Mr.\n" + "gender.male=Male" + configRegistry.configsJsonMap["stringsEn"] = "name.title=Mr." + "gender.male=Male" val resource = configRegistry.retrieveResourceBundleConfiguration("nonexistent") Assert.assertNull(resource) } @@ -135,7 +135,7 @@ class ConfigurationRegistryTest : RobolectricTest() { fun testRetrieveResourceBundleConfigurationMissingVariantReturnsBaseResourceBundle() { configRegistry.configsJsonMap["strings"] = "name.title=Mr.\n" + "gender.male=Male" val resource = configRegistry.retrieveResourceBundleConfiguration("strings_en") - Assert.assertNotNull(resource) + assertNotNull(resource) assertEquals("Mr.", resource?.getString("name.title")) assertEquals("Male", resource?.getString("gender.male")) } @@ -144,7 +144,7 @@ class ConfigurationRegistryTest : RobolectricTest() { fun testRetrieveResourceBundleConfigurationReturnsCorrectBundle() { configRegistry.configsJsonMap["stringsSw"] = "name.title=Bwana.\n" + "gender.male=Kijana" val resource = configRegistry.retrieveResourceBundleConfiguration("strings_sw") - Assert.assertNotNull(resource) + assertNotNull(resource) assertEquals("Bwana.", resource?.getString("name.title")) assertEquals("Kijana", resource?.getString("gender.male")) } @@ -153,7 +153,7 @@ class ConfigurationRegistryTest : RobolectricTest() { fun testRetrieveResourceBundleConfigurationWithLocaleVariantReturnsCorrectBundle() { configRegistry.configsJsonMap["stringsSw"] = "name.title=Bwana.\n" + "gender.male=Kijana" val resource = configRegistry.retrieveResourceBundleConfiguration("strings_sw_KE") - Assert.assertNotNull(resource) + assertNotNull(resource) assertEquals("Bwana.", resource?.getString("name.title")) assertEquals("Kijana", resource?.getString("gender.male")) } @@ -161,7 +161,8 @@ class ConfigurationRegistryTest : RobolectricTest() { @Test fun testRetrieveConfigurationParseTemplate() { val appId = "idOfApp" - configRegistry.configsJsonMap[ConfigType.Application.name] = "{\"appId\": \"${appId}\"}" + configRegistry.configsJsonMap[ConfigType.Application.name] = + """{"appId": "$appId", "configType" : "application"}""" val appConfig = configRegistry.retrieveConfiguration(ConfigType.Application) assertEquals(appId, appConfig.appId) @@ -172,10 +173,21 @@ class ConfigurationRegistryTest : RobolectricTest() { fun testRetrieveConfigurationParseTemplateMultiConfig() { val appId = "idOfApp" val id = "register" - configRegistry.configsJsonMap[ConfigType.Register.name] = - "{\"appId\": \"${appId}\", \"id\": \"${id}\", \"fhirResource\": {\"baseResource\": { \"resource\": \"Patient\"}}}" + configRegistry.configsJsonMap[id] = + """{ + "appId": "$appId", + "id": "$id", + "configType": "register", + "fhirResource": { + "baseResource": { + "resource": "Patient" + } + } + } + """ + .trimMargin() val registerConfig = - configRegistry.retrieveConfiguration(ConfigType.Register) + configRegistry.retrieveConfiguration(ConfigType.Register, id) assertEquals(appId, registerConfig.appId) assertEquals(id, registerConfig.id) } @@ -187,10 +199,25 @@ class ConfigurationRegistryTest : RobolectricTest() { val id = "register" val configId = "idOfConfig" configRegistry.configsJsonMap[configId] = - "{\"appId\": \"${appId}\", \"id\": \"${id}\", \"fhirResource\": {\"baseResource\": { \"resource\": \"Patient\"}}}" + """ + { + "appId": "$appId", + "id": "$id", + "configType" : "register", + "fhirResource": { + "baseResource": { + "resource": "Patient" + } + } + } + """ + .trimIndent() val registerConfig = - configRegistry.retrieveConfiguration(ConfigType.Register, configId) - Assert.assertTrue(configRegistry.configCacheMap.containsKey(configId)) + configRegistry.retrieveConfiguration( + configType = ConfigType.Register, + configId = configId, + ) + assertTrue(configRegistry.configCacheMap.containsKey(configId)) assertEquals(appId, registerConfig.appId) assertEquals(id, registerConfig.id) } @@ -204,14 +231,26 @@ class ConfigurationRegistryTest : RobolectricTest() { val paramId = "registerParam" val configId = "idOfConfig" configRegistry.configsJsonMap[configId] = - "{\"appId\": \"@{$appId}\", \"id\": \"@{$id}\", \"fhirResource\": {\"baseResource\": { \"resource\": \"Patient\"}}}" + """ + { + "appId": "@{$appId}", + "id": "@{$id}", + "configType": "register", + "fhirResource": { + "baseResource": { + "resource": "Patient" + } + } + } + """ + .trimIndent() val registerConfig = configRegistry.retrieveConfiguration( ConfigType.Register, configId, mapOf(appId to paramAppId, id to paramId), ) - Assert.assertTrue(configRegistry.configCacheMap.containsKey(configId)) + assertTrue(configRegistry.configCacheMap.containsKey(configId)) assertEquals(paramAppId, registerConfig.appId) assertEquals(paramId, registerConfig.id) } @@ -219,7 +258,10 @@ class ConfigurationRegistryTest : RobolectricTest() { @Test @kotlinx.coroutines.ExperimentalCoroutinesApi fun testRetrieveConfigurationParseResource() { - Assert.assertThrows("Configuration MUST be a template", IllegalArgumentException::class.java) { + Assert.assertThrows( + "Configuration MUST be a template", + IllegalArgumentException::class.java, + ) { configRegistry.retrieveConfiguration(ConfigType.Sync) } } @@ -227,9 +269,10 @@ class ConfigurationRegistryTest : RobolectricTest() { @Test @kotlinx.coroutines.ExperimentalCoroutinesApi fun testFetchNonWorkflowConfigResourcesNoAppId() { - runTest { configRegistry.fetchNonWorkflowConfigResources() } - - coVerify(inverse = true) { fhirEngine.search(any()) } + runTest { + configRegistry.fetchNonWorkflowConfigResources() + coVerify(inverse = true) { fhirEngine.search(any()) } + } } @Test @@ -253,7 +296,13 @@ class ConfigurationRegistryTest : RobolectricTest() { val appId = "theAppId" val composition = Composition().apply { identifier = Identifier().apply { value = appId } } coEvery { fhirEngine.search(any()) } returns - listOf(SearchResult(resource = composition, null, null)) + listOf( + SearchResult( + resource = composition, + included = null, + revIncluded = null, + ), + ) configRegistry.sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, appId) coEvery { fhirResourceDataSource.getResource("Composition?identifier=theAppId&_count=200") @@ -273,12 +322,16 @@ class ConfigurationRegistryTest : RobolectricTest() { id = "composition-id-1" identifier = Identifier().apply { value = appId } section = - listOf(SectionComponent().apply { focus.reference = ResourceType.Questionnaire.name }) + listOf( + SectionComponent().apply { focus.reference = ResourceType.Questionnaire.name }, + ) } configRegistry.sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, appId) coEvery { fhirEngine.create(composition) } returns listOf(composition.id) coEvery { fhirEngine.search(Search(composition.resourceType)) } returns - listOf(SearchResult(resource = composition, null, null)) + listOf( + SearchResult(resource = composition, null, null), + ) coEvery { fhirResourceDataSource.post(any(), any()) } returns Bundle() coEvery { fhirResourceDataSource.getResource("Composition?identifier=theAppId&_count=200") @@ -305,7 +358,9 @@ class ConfigurationRegistryTest : RobolectricTest() { Composition().apply { identifier = Identifier().apply { value = appId } section = - listOf(SectionComponent().apply { focus.reference = ResourceType.Questionnaire.name }) + listOf( + SectionComponent().apply { focus.reference = ResourceType.Questionnaire.name }, + ) } configRegistry.sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, appId) fhirEngine.create(composition) // Add composition to database instead of mocking @@ -373,7 +428,7 @@ class ConfigurationRegistryTest : RobolectricTest() { val createdResourceArgumentSlot = mutableListOf() coVerify { configRegistry.createOrUpdateRemote(capture(createdResourceArgumentSlot)) } - Assert.assertEquals( + assertEquals( "test-list-id", createdResourceArgumentSlot.filterIsInstance().first().id, ) @@ -407,8 +462,12 @@ class ConfigurationRegistryTest : RobolectricTest() { @kotlinx.coroutines.ExperimentalCoroutinesApi fun testAddOrUpdateCatchesResourceNotFound() { val patient = Faker.buildPatient() - coEvery { fhirEngine.get(patient.resourceType, patient.logicalId) } throws - ResourceNotFoundException("", "") + coEvery { + fhirEngine.get( + patient.resourceType, + patient.logicalId, + ) + } throws ResourceNotFoundException("", "") coEvery { fhirEngine.create(any(), isLocalOnly = true) } returns listOf(patient.id) runTest { @@ -440,7 +499,7 @@ class ConfigurationRegistryTest : RobolectricTest() { coEvery { fhirEngine.search(any()) } returns listOf() runTest { configRegistry.loadConfigurations(appId, context) } - Assert.assertTrue(configRegistry.configsJsonMap.isEmpty()) + assertTrue(configRegistry.configsJsonMap.isEmpty()) } @Test @@ -449,10 +508,16 @@ class ConfigurationRegistryTest : RobolectricTest() { val appId = "the app id" val composition = Composition().apply { section = listOf() } coEvery { fhirEngine.search(any()) } returns - listOf(SearchResult(resource = composition, null, null)) + listOf( + SearchResult( + resource = composition, + null, + null, + ), + ) runTest { configRegistry.loadConfigurations(appId, context) } - Assert.assertTrue(configRegistry.configsJsonMap.isEmpty()) + assertTrue(configRegistry.configsJsonMap.isEmpty()) } @Test @@ -461,10 +526,16 @@ class ConfigurationRegistryTest : RobolectricTest() { val appId = "the app id" val composition = Composition().apply { section = listOf(SectionComponent()) } coEvery { fhirEngine.search(any()) } returns - listOf(SearchResult(resource = composition, null, null)) + listOf( + SearchResult( + resource = composition, + null, + null, + ), + ) runTest { configRegistry.loadConfigurations(appId, context) } - Assert.assertTrue(configRegistry.configsJsonMap.isEmpty()) + assertTrue(configRegistry.configsJsonMap.isEmpty()) } @Test @@ -487,10 +558,16 @@ class ConfigurationRegistryTest : RobolectricTest() { ) } coEvery { fhirEngine.search(any()) } returns - listOf(SearchResult(resource = composition, null, null)) + listOf( + SearchResult( + resource = composition, + null, + null, + ), + ) runTest { configRegistry.loadConfigurations(appId, context) } - Assert.assertTrue(configRegistry.configsJsonMap.isEmpty()) + assertTrue(configRegistry.configsJsonMap.isEmpty()) } @Test @@ -518,9 +595,19 @@ class ConfigurationRegistryTest : RobolectricTest() { ) } coEvery { fhirEngine.search(any()) } returns - listOf(SearchResult(resource = composition, null, null)) - coEvery { fhirEngine.get(ResourceType.Binary, referenceId) } returns - Binary().apply { content = ByteArray(0) } + listOf( + SearchResult( + resource = composition, + null, + null, + ), + ) + coEvery { + fhirEngine.get( + ResourceType.Binary, + referenceId, + ) + } returns Binary().apply { content = ByteArray(0) } runTest { configRegistry.loadConfigurations(appId, context) } Assert.assertFalse(configRegistry.configsJsonMap.isEmpty()) @@ -551,12 +638,18 @@ class ConfigurationRegistryTest : RobolectricTest() { ) } coEvery { fhirEngine.search(any()) } returns - listOf(SearchResult(resource = composition, null, null)) + listOf( + SearchResult( + resource = composition, + null, + null, + ), + ) runTest { configRegistry.loadConfigurations(appId, context) } coVerify(inverse = true) { fhirEngine.get(ResourceType.Binary, referenceId) } - Assert.assertTrue(configRegistry.configsJsonMap.isEmpty()) + assertTrue(configRegistry.configsJsonMap.isEmpty()) } @Test @@ -571,20 +664,20 @@ class ConfigurationRegistryTest : RobolectricTest() { configType = ConfigType.Application, ) - Assert.assertNotNull(applicationConfiguration) + assertNotNull(applicationConfiguration) assertEquals("thisApp", applicationConfiguration.appId) - Assert.assertNotNull(ConfigType.Application.name, applicationConfiguration.configType) + assertNotNull(ConfigType.Application.name, applicationConfiguration.configType) // Config cache map now contains application config - Assert.assertTrue(configRegistry.configCacheMap.containsKey(ConfigType.Application.name)) + assertTrue(configRegistry.configCacheMap.containsKey(ConfigType.Application.name)) val anotherApplicationConfig = configRegistry.retrieveConfiguration( configType = ConfigType.Application, ) - Assert.assertTrue(configRegistry.configCacheMap.containsKey(ConfigType.Application.name)) - Assert.assertNotNull(anotherApplicationConfig) + assertTrue(configRegistry.configCacheMap.containsKey(ConfigType.Application.name)) + assertNotNull(anotherApplicationConfig) assertEquals("thisApp", anotherApplicationConfig.appId) - Assert.assertNotNull(ConfigType.Application.name, anotherApplicationConfig.configType) + assertNotNull(ConfigType.Application.name, anotherApplicationConfig.configType) } @Test @@ -629,17 +722,17 @@ class ConfigurationRegistryTest : RobolectricTest() { paramsMap = paramsMap, ) - Assert.assertNotNull(applicationConfiguration) + assertNotNull(applicationConfiguration) assertEquals("thisApp", applicationConfiguration.appId) - Assert.assertNotNull(ConfigType.Application.name, applicationConfiguration.configType) + assertNotNull(ConfigType.Application.name, applicationConfiguration.configType) // Config cache map now contains application config val anotherApplicationConfig = configRegistry.retrieveConfiguration( configType = ConfigType.Application, ) - Assert.assertNotNull(anotherApplicationConfig) - Assert.assertTrue(configRegistry.configCacheMap.containsKey(ConfigType.Application.name)) + assertNotNull(anotherApplicationConfig) + assertTrue(configRegistry.configCacheMap.containsKey(ConfigType.Application.name)) } @Test @@ -821,8 +914,12 @@ class ConfigurationRegistryTest : RobolectricTest() { "List?_id=46464&_page=1&_count=200", ) } - coEvery { fhirEngine.get(any(), any()) } throws - ResourceNotFoundException(ResourceType.Group.name, "some-id") + coEvery { + fhirEngine.get( + any(), + any(), + ) + } throws ResourceNotFoundException(ResourceType.Group.name, "some-id") coEvery { fhirEngine.create(any(), isLocalOnly = true) } returns listOf() @@ -888,7 +985,10 @@ class ConfigurationRegistryTest : RobolectricTest() { BundleEntryComponent() .setResource( Bundle().apply { - entry = listOf(BundleEntryComponent().setResource(listResource)) + entry = + listOf( + BundleEntryComponent().setResource(listResource), + ) }, ), ) @@ -897,8 +997,12 @@ class ConfigurationRegistryTest : RobolectricTest() { ) } - coEvery { fhirEngine.get(any(), any()) } throws - ResourceNotFoundException(ResourceType.Group.name, "some-id-not-found") + coEvery { + fhirEngine.get( + any(), + any(), + ) + } throws ResourceNotFoundException(ResourceType.Group.name, "some-id-not-found") coEvery { fhirEngine.create(any(), isLocalOnly = true) } returns listOf() @@ -951,15 +1055,34 @@ class ConfigurationRegistryTest : RobolectricTest() { @Test fun writeToFileWithMetadataResourceWithNameShouldCreateFileWithResourceName() { - val resource = Faker.buildPatient().apply { id = "1661662881" } - val expectedFileName = "1661662881.json" - every { mockedContext.filesDir } returns File(ArgumentMatchers.anyString()) - every { mockedJsonParser.encodeResourceToString(any()) } returns - resource.encodeResourceToString() - val expectedEncodedResource = mockedJsonParser.encodeResourceToString(resource) - + val parser = fhirContext.newJsonParser() + val resource = Faker.buildPatient() val resultFile = configRegistry.writeToFile(resource) - assertEquals(expectedFileName, resultFile.name) - assertEquals(expectedEncodedResource, resultFile.readText()) + assertNotNull(resultFile) + assertEquals( + resource.logicalId, + (parser.parseResource(resultFile.readText()) as Patient).logicalId, + ) + } + + @Test + fun testPopulateConfigurationsMapShouldAddResourcesToDatabase() { + runBlocking { + val appId = "app" + val questionnaireId = "3440" + + // Verify questionnaire does not exist + assertThrows(ResourceNotFoundException::class.java) { + runBlocking { fhirEngine.get(ResourceType.Questionnaire, questionnaireId) } + } + + configRegistry.populateConfigurationsMap(context, Composition(), true, appId) {} + + // Confirm configs/app/resources/sample_questionnaire.json was added to the database + val questionnaire = fhirEngine.get(ResourceType.Questionnaire, questionnaireId) + assertTrue(configRegistry.configsJsonMap.isNotEmpty()) + assertNotNull(questionnaire) + assertEquals(questionnaireId, questionnaire.logicalId) + } } } diff --git a/android/quest/src/main/assets/sample_patient_registration.json b/android/quest/src/main/assets/resources/sample_patient_registration.json similarity index 100% rename from android/quest/src/main/assets/sample_patient_registration.json rename to android/quest/src/main/assets/resources/sample_patient_registration.json diff --git a/android/quest/src/main/assets/test-questionnaire.json b/android/quest/src/main/assets/resources/test-questionnaire.json similarity index 100% rename from android/quest/src/main/assets/test-questionnaire.json rename to android/quest/src/main/assets/resources/test-questionnaire.json diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index 77abe78244..fb7c9edd1c 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -136,7 +136,9 @@ class QuestionnaireActivityTest : RobolectricTest() { ), ) questionnaireJson = - context.assets.open("sample_patient_registration.json").bufferedReader().use { it.readText() } + context.assets.open("resources/sample_patient_registration.json").bufferedReader().use { + it.readText() + } questionnaire = questionnaireJson.decodeResourceFromString() } 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 4f24ee3175..8b014ba0f4 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 @@ -199,7 +199,9 @@ class QuestionnaireViewModelTest : RobolectricTest() { // Sample questionnaire val questionnaireJson = - context.assets.open("sample_patient_registration.json").bufferedReader().use { it.readText() } + context.assets.open("resources/sample_patient_registration.json").bufferedReader().use { + it.readText() + } samplePatientRegisterQuestionnaire = questionnaireJson.decodeResourceFromString() } diff --git a/docs/engineering/android-app/configuring/working-in-debug-mode.mdx b/docs/engineering/android-app/configuring/working-in-debug-mode.mdx new file mode 100644 index 0000000000..dca970fb4a --- /dev/null +++ b/docs/engineering/android-app/configuring/working-in-debug-mode.mdx @@ -0,0 +1,20 @@ +# Working in debug mode + +If you want to test local configurations or running debug application flavours you can do the following +* Copy app configurations into assets/configs folder. +* Rename the folder you copied to your appId name. Ensure the folder consists of the following sub-directories and files + - profiles + - registers + - translations + - application_config.json + - composition_config.json + - navigation_config.json + - sync_config.json + +* If you would like to work with local resources e.g Questionnaires, StructureMaps, planDefinitions e.t.c, create a folder called `resources` + inside `assets` and copy your files inside folders names. e.g Questionnaires can be added to a folder `questionnaire/your_questionnaire_files.json`. The + app will read these files and save your resources into sqlite db. You can now go ahead and reference the resource ids inside configurations files + +* Choose `variantNameDebug` as your app variant and run the app. e.g If your flavour name is called `sidCadre`, use the variant called `sidCadreDebug` +* When asked to enter the `applicationId`, type your `appIdName/debug` inside the edit text. e.g `sidCadre/debug` +* Proceed to input correct `usernames` and `password` and wait for the app to sync.