From 53ca669bc44f1383d5c556e0a60bb6a684099fc2 Mon Sep 17 00:00:00 2001 From: Martin Ndegwa Date: Wed, 25 Sep 2024 11:56:23 +0300 Subject: [PATCH] =?UTF-8?q?Upgrade=20FHIR=20SDK=20dependencies=20=E2=AC=86?= =?UTF-8?q?=EF=B8=8F=20(#3423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Upgrade FHIR SDK dependencies ⬆️ * Replace JWT token parser library * Update Kujaku library version * Upgrade 3rd party dependencies * Refactor Knowledge Manager Resources Persistance * Refactor Cancel previous worflow to use native commands * Upgrade CI API level to 34 * Clean up gradle dependencies configuration * Move measure reporting evaluation to BG thread * Remove unrecommended forced portrait format * Clean up TOML catalog file * Refactor usage of FHIR JSONParser to support concurrency * Fix code coverage reporting * Fix build 💚 --- .github/workflows/ci.yml | 37 ++-- android/engine/build.gradle.kts | 12 +- .../extension/FhirEngineExtensionKtTest.kt | 19 +- .../configuration/ConfigurationRegistry.kt | 13 +- .../engine/data/local/DefaultRepository.kt | 97 ++++----- .../data/remote/shared/TokenAuthenticator.kt | 15 +- .../fhircore/engine/di/CoreModule.kt | 25 ++- .../engine/p2p/dao/BaseP2PTransferDao.kt | 4 - .../engine/p2p/dao/P2PReceiverTransferDao.kt | 5 +- .../engine/p2p/dao/P2PSenderTransferDao.kt | 3 +- .../engine/rulesengine/RulesFactory.kt | 6 +- .../util/extension/ResourceExtension.kt | 17 -- .../fhircore/engine/FhirExtractionTest.kt | 2 - .../engine/auth/TokenAuthenticatorTest.kt | 4 +- .../data/local/DefaultRepositoryTest.kt | 2 +- .../engine/task/FhirCarePlanGeneratorTest.kt | 4 - .../task/FhirResourceExpireWorkerTest.kt | 2 +- .../util/extension/ResourceExtensionTest.kt | 43 ---- .../screens/GeoWidgetViewModelTest.kt | 7 +- android/gradle/libs.versions.toml | 75 ++++--- android/quest/build.gradle.kts | 2 +- .../fhircore/quest/integration/Faker.kt | 5 + .../ui/register/RegisterScreenTest.kt | 11 ++ android/quest/src/main/AndroidManifest.xml | 17 +- .../report/measure/MeasureReportRepository.kt | 79 ++++---- .../ui/appsetting/AppSettingViewModel.kt | 5 +- .../report/measure/MeasureReportViewModel.kt | 154 +++++++-------- .../measure/worker/MeasureReportWorker.kt | 16 +- .../quest/src/main/res/values-fr/strings.xml | 4 +- .../quest/src/main/res/values-in/strings.xml | 6 - .../quest/src/main/res/values-sw/strings.xml | 1 + android/quest/src/main/res/values/strings.xml | 3 +- .../fhircore/quest/CqlContentTest.kt | 184 +++++++++--------- .../fhircore/quest/data/DataMigrationTest.kt | 26 ++- .../quest/data/QuestXFhirQueryResolverTest.kt | 2 +- .../measure/MeasureReportPagingSourceTest.kt | 3 +- .../measure/MeasureReportRepositoryTest.kt | 5 +- .../quest/robolectric/RobolectricTest.kt | 12 +- .../quest/robolectric/WorkManagerRule.kt | 8 +- .../fhircore/quest/sdk/CqlBuilder.kt | 2 - .../ui/appsetting/AppSettingViewModelTest.kt | 32 ++- .../GeoWidgetLauncherViewModelTest.kt | 11 +- .../QuestionnaireActivityTest.kt | 30 ++- .../QuestionnaireViewModelTest.kt | 46 ++++- .../measure/MeasureReportViewModelTest.kt | 48 ++++- .../util/extensions/ConfigExtensionsKtTest.kt | 1 + 46 files changed, 583 insertions(+), 522 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1c16c67ae..0ac130fc90 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,10 @@ on: merge_group: branches: [ main ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: FHIRCORE_USERNAME: ${{ secrets.FHIRCORE_USERNAME }} FHIRCORE_ACCESS_TOKEN: ${{ secrets.FHIRCORE_ACCESS_TOKEN }} @@ -21,13 +25,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - api-level: [30] + api-level: [34] + steps: - - name: Cancel Previous workflow runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - name: Checkout 🛎️ uses: actions/checkout@v4 @@ -106,7 +106,7 @@ jobs: - name: Upload Engine module test coverage report to Codecov - if: matrix.api-level == 30 # Only upload coverage on API level 30 + if: matrix.api-level == 34 # Only upload coverage on API level 34 working-directory: android run: bash <(curl -s https://codecov.io/bash) -F engine -f "engine/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml" @@ -114,13 +114,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - api-level: [30] + api-level: [34] + steps: - - name: Cancel Previous workflow runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - name: Checkout 🛎️ uses: actions/checkout@v4 @@ -198,7 +194,7 @@ jobs: path: android/geowidget/build/reports - name: Upload Geowidget module test coverage report to Codecov - if: matrix.api-level == 30 # Only upload coverage on API level 30 + if: matrix.api-level == 34 # Only upload coverage on API level 34 working-directory: android run: bash <(curl -s https://codecov.io/bash) -F geowidget -f "geowidget/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml" @@ -206,12 +202,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - api-level: [30] - steps: - - name: Cancel Previous workflow runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} + api-level: [34] + + steps: - name: Checkout 🛎️ uses: actions/checkout@v4 @@ -315,6 +308,6 @@ jobs: path: android/quest/build/reports - name: Upload Quest module test coverage report to Codecov - if: matrix.api-level == 30 # Only upload coverage on API level 30 + if: matrix.api-level == 34 # Only upload coverage on API level 34 working-directory: android - run: bash <(curl -s https://codecov.io/bash) -F quest -f "quest/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml" \ No newline at end of file + run: bash <(curl -s https://codecov.io/bash) -F quest -f "quest/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml" diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index 6616981e4a..d62fc972b5 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -3,7 +3,7 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { `jacoco-report` - `ktlint` + ktlint id("com.android.library") id("kotlin-android") id("kotlin-kapt") @@ -162,7 +162,7 @@ dependencies { api(libs.glide) api(libs.knowledge) { exclude(group = "org.slf4j", module = "jcl-over-slf4j") } api(libs.p2p.lib) - api(libs.jjwt) + api(libs.java.jwt) api(libs.fhir.common.utils) { exclude(group = "org.slf4j", module = "jcl-over-slf4j") } api(libs.runtime.livedata) api(libs.foundation) @@ -180,8 +180,8 @@ dependencies { api(libs.data.capture) { isTransitive = true exclude(group = "ca.uhn.hapi.fhir") - exclude(group = "com.google.android.fhir", module = "engine") exclude(group = "com.google.android.fhir", module = "common") + exclude(group = "org.smartregister", module = "common") exclude(group = "org.slf4j", module = "jcl-over-slf4j") } api(libs.cqf.fhir.cr) { @@ -194,23 +194,19 @@ dependencies { exclude(group = "xerces") exclude(group = "com.github.java-json-tools") exclude(group = "org.codehaus.woodstox") - exclude(group = "com.google.android.fhir", module = "common") exclude(group = "com.google.android.fhir", module = "engine") + exclude(group = "org.smartregister", module = "engine") exclude(group = "com.github.ben-manes.caffeine") } api(libs.contrib.barcode) { isTransitive = true exclude(group = "org.smartregister", module = "data-capture") exclude(group = "ca.uhn.hapi.fhir") - exclude(group = "com.google.android.fhir", module = "common") - exclude(group = "com.google.android.fhir", module = "engine") } api(libs.contrib.locationwidget) { isTransitive = true exclude(group = "org.smartregister", module = "data-capture") exclude(group = "ca.uhn.hapi.fhir") - exclude(group = "com.google.android.fhir", module = "common") - exclude(group = "com.google.android.fhir", module = "engine") } api(libs.fhir.engine) { isTransitive = true diff --git a/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt b/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt index 8862f5e123..162818f6e9 100644 --- a/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt +++ b/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt @@ -25,6 +25,7 @@ import com.google.android.fhir.FhirEngineProvider import com.google.android.fhir.search.search import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.Resource @@ -57,19 +58,15 @@ class FhirEngineExtensionKtTest { } @Test - fun test_search_time_searches_sequentially_and_short_running_query_waits() { + fun test_search_time_searches_sequentially_and_short_running_query_waits() = runTest { val fetchedResources = mutableListOf() - runBlocking { - launch { - val patients = fhirEngine.search {}.map { it.resource } - fetchedResources += patients - } - launch { - val questionnaires = fhirEngine.search {}.map { it.resource } - fetchedResources += questionnaires - } - } + val patients = fhirEngine.search {}.map { it.resource } + fetchedResources += patients + + val questionnaires = fhirEngine.search {}.map { it.resource } + fetchedResources += questionnaires + val indexOfResultOfShortQuery = fetchedResources.indexOfFirst { it.resourceType == ResourceType.Questionnaire } val indexOfResultOfLongQuery = 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 82c126faf4..42f4a5bf0e 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 @@ -104,7 +104,6 @@ constructor( private var _isNonProxy = BuildConfig.IS_NON_PROXY_APK private val fhirContext = FhirContext.forR4Cached() private val authConfiguration = configService.provideAuthConfiguration() - private val jsonParser = fhirContext.newJsonParser() /** * Retrieve configuration for the provided [ConfigType]. The JSON retrieved from [configsJsonMap] @@ -629,9 +628,14 @@ constructor( resource.idElement.idPart } - return File(context.filesDir, "$fileName.json").apply { - writeText(jsonParser.encodeResourceToString(resource)) - } + return File( + context.filesDir, + "$KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER/${resource.resourceType}/$fileName.json", + ) + .apply { + this.parentFile?.mkdirs() + writeText(fhirContext.newJsonParser().encodeResourceToString(resource)) + } } /** @@ -813,6 +817,7 @@ constructor( const val PAGINATION_NEXT = "next" const val RESOURCES_PATH = "resources/" const val SYNC_LOCATION_IDS = "_syncLocations" + const val KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER = "km" /** * The list of resources whose types can be synced down as part of the Composition configs. diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt index 8ce645822c..acba6336e1 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt @@ -114,18 +114,15 @@ constructor( @ApplicationContext open val context: Context, ) { - suspend inline fun loadResource(resourceId: String): T? { - return withContext(dispatcherProvider.io()) { fhirEngine.loadResource(resourceId) } - } + suspend inline fun loadResource(resourceId: String): T? = + fhirEngine.loadResource(resourceId) suspend fun loadResource(resourceId: String, resourceType: ResourceType): Resource = - withContext(dispatcherProvider.io()) { fhirEngine.get(resourceType, resourceId) } + fhirEngine.get(resourceType, resourceId) suspend fun loadResource(reference: Reference) = - withContext(dispatcherProvider.io()) { - IdType(reference.reference).let { - fhirEngine.get(ResourceType.fromCode(it.resourceType), it.idPart) - } + IdType(reference.reference).let { + fhirEngine.get(ResourceType.fromCode(it.resourceType), it.idPart) } suspend inline fun searchResourceFor( @@ -135,19 +132,17 @@ constructor( dataQueries: List = listOf(), configComputedRuleValues: Map, ): List = - withContext(dispatcherProvider.io()) { - fhirEngine - .batchedSearch { - filterByResourceTypeId(token, subjectType, subjectId) - dataQueries.forEach { - filterBy( - dataQuery = it, - configComputedRuleValues = configComputedRuleValues, - ) - } + fhirEngine + .batchedSearch { + filterByResourceTypeId(token, subjectType, subjectId) + dataQueries.forEach { + filterBy( + dataQuery = it, + configComputedRuleValues = configComputedRuleValues, + ) } - .map { it.resource } - } + } + .map { it.resource } suspend inline fun search(search: Search) = fhirEngine.batchedSearch(search).map { it.resource } @@ -162,17 +157,13 @@ constructor( * param [addResourceTags] */ suspend fun create(addResourceTags: Boolean = true, vararg resource: Resource): List { - return withContext(dispatcherProvider.io()) { - preProcessResources(addResourceTags, *resource) - fhirEngine.create(*resource) - } + preProcessResources(addResourceTags, *resource) + return fhirEngine.create(*resource) } suspend fun createRemote(addResourceTags: Boolean = true, vararg resource: Resource) { - return withContext(dispatcherProvider.io()) { - preProcessResources(addResourceTags, *resource) - fhirEngine.create(*resource, isLocalOnly = true) - } + preProcessResources(addResourceTags, *resource) + fhirEngine.create(*resource, isLocalOnly = true) } private fun preProcessResources(addResourceTags: Boolean, vararg resource: Resource) { @@ -198,23 +189,19 @@ constructor( resourceId: String, softDelete: Boolean = false, ) { - withContext(dispatcherProvider.io()) { - if (softDelete) { - val resource = fhirEngine.get(resourceType, resourceId) - softDelete(resource) - } else { - fhirEngine.delete(resourceType, resourceId) - } + if (softDelete) { + val resource = fhirEngine.get(resourceType, resourceId) + softDelete(resource) + } else { + fhirEngine.delete(resourceType, resourceId) } } suspend fun delete(resource: Resource, softDelete: Boolean = false) { - withContext(dispatcherProvider.io()) { - if (softDelete) { - softDelete(resource) - } else { - fhirEngine.delete(resource.resourceType, resource.logicalId) - } + if (softDelete) { + softDelete(resource) + } else { + fhirEngine.delete(resource.resourceType, resource.logicalId) } } @@ -243,24 +230,20 @@ constructor( * param [addMandatoryTags] */ suspend fun addOrUpdate(addMandatoryTags: Boolean = true, resource: R) { - return withContext(dispatcherProvider.io()) { - resource.updateLastUpdated() - try { - fhirEngine.get(resource.resourceType, resource.logicalId).run { - val updateFrom = updateFrom(resource) - fhirEngine.update(updateFrom) - } - } catch (resourceNotFoundException: ResourceNotFoundException) { - create(addMandatoryTags, resource) + resource.updateLastUpdated() + try { + fhirEngine.get(resource.resourceType, resource.logicalId).run { + val updateFrom = updateFrom(resource) + fhirEngine.update(updateFrom) } + } catch (resourceNotFoundException: ResourceNotFoundException) { + create(addMandatoryTags, resource) } } suspend fun update(resource: R) { - return withContext(dispatcherProvider.io()) { - resource.updateLastUpdated() - fhirEngine.update(resource) - } + resource.updateLastUpdated() + fhirEngine.update(resource) } suspend fun loadManagingEntity(group: Group) = @@ -904,7 +887,7 @@ constructor( val updatedResource = parser.parseResource(resourceDefinition, updatedResourceDocument.jsonString()) updatedResource.setId(updatedResource.idElement.idPart) - withContext(dispatcherProvider.io()) { fhirEngine.update(updatedResource as Resource) } + fhirEngine.update(updatedResource as Resource) } private fun getJsonContent(jsonElement: JsonElement): Any? { @@ -935,9 +918,7 @@ constructor( suspend fun purge(resource: Resource, forcePurge: Boolean) { try { - withContext(dispatcherProvider.io()) { - fhirEngine.purge(resource.resourceType, resource.logicalId, forcePurge) - } + fhirEngine.purge(resource.resourceType, resource.logicalId, forcePurge) } catch (resourceNotFoundException: ResourceNotFoundException) { Timber.e( "Purge failed -> Resource with ID ${resource.logicalId} does not exist", diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt index efa101b299..a3daf5035a 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt @@ -28,11 +28,12 @@ import android.os.Handler import android.os.Looper import android.os.Message import androidx.core.os.bundleOf +import com.auth0.jwt.JWT +import com.auth0.jwt.exceptions.JWTDecodeException +import com.auth0.jwt.interfaces.DecodedJWT import com.google.android.fhir.sync.HttpAuthenticationMethod import com.google.android.fhir.sync.HttpAuthenticator as FhirAuthenticator import dagger.hilt.android.qualifiers.ApplicationContext -import io.jsonwebtoken.JwtException -import io.jsonwebtoken.Jwts import java.io.IOException import java.net.UnknownHostException import java.util.Base64 @@ -62,7 +63,6 @@ constructor( @ApplicationContext val context: Context, ) : FhirAuthenticator { - private val jwtParser = Jwts.parser() private val authConfiguration by lazy { configService.provideAuthConfiguration() } private var isLoginPageRendered = false @@ -131,12 +131,11 @@ constructor( /** This function checks if token is null or empty or expired */ fun isTokenActive(authToken: String?): Boolean { if (authToken.isNullOrEmpty()) return false - val tokenPart = authToken.substringBeforeLast('.').plus(".") return try { - val body = jwtParser.parseClaimsJwt(tokenPart).body - body.expiration.after(today()) - } catch (jwtException: JwtException) { - false + val jwt: DecodedJWT? = JWT.decode(authToken) + jwt?.expiresAt!!.after(today()) + } catch (e: JWTDecodeException) { + return false } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/CoreModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/CoreModule.kt index a3a1eb6d01..7ffedd809b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/CoreModule.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/CoreModule.kt @@ -27,10 +27,14 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import java.io.File +import java.io.FileInputStream import javax.inject.Singleton import org.hl7.fhir.r4.context.SimpleWorkerContext import org.hl7.fhir.r4.model.Parameters +import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.utils.FHIRPathEngine +import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.util.helper.TransformSupportServices @InstallIn(SingletonComponent::class) @@ -39,10 +43,29 @@ class CoreModule { @Singleton @Provides - fun provideWorkerContextProvider(): SimpleWorkerContext = + fun provideWorkerContextProvider(@ApplicationContext context: Context): SimpleWorkerContext = SimpleWorkerContext().apply { setExpansionProfile(Parameters()) isCanRunWithoutTerminology = true + context.filesDir + .resolve(ConfigurationRegistry.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER) + .list() + ?.forEach { resourceFolder -> + context.filesDir + .resolve("${ConfigurationRegistry.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER}/$resourceFolder") + .list() + ?.forEach { file -> + cacheResource( + FhirContext.forR4Cached() + .newJsonParser() + .parseResource( + FileInputStream( + File(context.filesDir.resolve("km/$resourceFolder/$file").toString()), + ), + ) as Resource, + ) + } + } } @Singleton diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt index 474f2c0da8..2cd52f88ab 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt @@ -16,8 +16,6 @@ package org.smartregister.fhircore.engine.p2p.dao -import ca.uhn.fhir.context.FhirContext -import ca.uhn.fhir.parser.IParser import ca.uhn.fhir.rest.gclient.DateClientParam import ca.uhn.fhir.rest.gclient.StringClientParam import ca.uhn.fhir.rest.param.ParamPrefixEnum @@ -49,8 +47,6 @@ constructor( open val configurationRegistry: ConfigurationRegistry, ) { - protected val jsonParser: IParser = FhirContext.forR4Cached().newJsonParser() - open fun getDataTypes(): TreeSet { val appRegistry = configurationRegistry.retrieveConfiguration(ConfigType.Application) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PReceiverTransferDao.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PReceiverTransferDao.kt index 24270c0215..ddc1d1d759 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PReceiverTransferDao.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PReceiverTransferDao.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.engine.p2p.dao +import ca.uhn.fhir.context.FhirContext import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.extensions.logicalId import java.util.TreeSet @@ -47,7 +48,9 @@ constructor( (0 until jsonArray.length()).forEach { runBlocking { val resource = - jsonParser.parseResource(type.name.resourceClassType(), jsonArray.get(it).toString()) + FhirContext.forR4Cached() + .newJsonParser() + .parseResource(type.name.resourceClassType(), jsonArray.get(it).toString()) val recordLastUpdated = resource.meta.lastUpdated.time defaultRepository.addOrUpdate(resource = resource) maxLastUpdated = diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PSenderTransferDao.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PSenderTransferDao.kt index f6a0c18123..ce537fb3b7 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PSenderTransferDao.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/P2PSenderTransferDao.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.engine.p2p.dao +import ca.uhn.fhir.context.FhirContext import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.extensions.logicalId import java.util.TreeSet @@ -76,7 +77,7 @@ constructor( val jsonArray = JSONArray() records.forEach { - jsonArray.put(jsonParser.encodeResourceToString(it.resource)) + jsonArray.put(FhirContext.forR4Cached().newJsonParser().encodeResourceToString(it.resource)) highestRecordId = if (it.resource.meta?.lastUpdated?.time!! > highestRecordId) { it.resource.meta?.lastUpdated?.time!! diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt index 77a8e41935..d130483d03 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt @@ -149,8 +149,6 @@ constructor( /** Provide access to utility functions accessible to the users defining rules in JSON format. */ inner class RulesEngineService { - val parser = fhirContext.newJsonParser() - private var conf: Configuration = Configuration.defaultConfiguration().apply { addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL) } @@ -682,7 +680,9 @@ constructor( } val updatedResource = - parser.parseResource(resource::class.java, updatedResourceDocument.jsonString()) + fhirContext + .newJsonParser() + .parseResource(resource::class.java, updatedResourceDocument.jsonString()) CoroutineScope(dispatcherProvider.io()).launch { if (purgeAffectedResources) { defaultRepository.purge(updatedResource as Resource, forcePurge = true) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt index a5642cad2b..89f020b8fe 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt @@ -20,7 +20,6 @@ import android.content.Context import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser import ca.uhn.fhir.rest.gclient.ReferenceClientParam -import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get import java.time.Duration @@ -184,22 +183,6 @@ fun JSONObject.updateFrom(updated: JSONObject) { keys.forEach { key -> updated.opt(key)?.run { put(key, this) } } } -fun QuestionnaireResponse.generateMissingItems(questionnaire: Questionnaire) = - questionnaire.item.generateMissingItems(this.item) - -fun List.generateMissingItems( - qrItems: MutableList, -) { - this.forEachIndexed { index, qItem -> - // generate complete hierarchy if response item missing otherwise check for nested items - if (qrItems.isEmpty() || (index < qrItems.size && qItem.linkId != qrItems[index].linkId)) { - qrItems.add(index, qItem.createQuestionnaireResponseItem()) - } else if (index < qrItems.size) { - qItem.item.generateMissingItems(qrItems[index].item) - } - } -} - /** * Set all questions that are not of type [Questionnaire.QuestionnaireItemType.GROUP] to readOnly if * [readOnly] is true. This also generates the correct FHIRPath population expression for each diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/FhirExtractionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/FhirExtractionTest.kt index 79cc9d7842..0b8d49b085 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/FhirExtractionTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/FhirExtractionTest.kt @@ -34,7 +34,6 @@ import javax.inject.Inject import junit.framework.Assert.assertEquals import junit.framework.Assert.assertNotNull import junit.framework.Assert.assertTrue -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Encounter @@ -75,7 +74,6 @@ class FhirExtractionTest : RobolectricTest() { hiltRule.inject() structureMapUtilities = StructureMapUtilities(transformSupportServices.simpleWorkerContext) val workManager = mockk() - every { defaultRepository.dispatcherProvider.io() } returns Dispatchers.IO every { defaultRepository.fhirEngine } returns fhirEngine every { workManager.enqueue(any()) } returns mockk() } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/TokenAuthenticatorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/TokenAuthenticatorTest.kt index 1aee73852b..35a3bcc403 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/TokenAuthenticatorTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/TokenAuthenticatorTest.kt @@ -24,10 +24,10 @@ import android.accounts.OperationCanceledException import android.os.Bundle import androidx.core.os.bundleOf import androidx.test.core.app.ApplicationProvider +import com.auth0.jwt.exceptions.JWTDecodeException import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication -import io.jsonwebtoken.JwtException import io.mockk.coEvery import io.mockk.every import io.mockk.just @@ -108,7 +108,7 @@ class TokenAuthenticatorTest : RobolectricTest() { } @Test - @Throws(JwtException::class) + @Throws(JWTDecodeException::class) fun testIsTokenActiveWithExpiredJwtToken() { Assert.assertFalse(tokenAuthenticator.isTokenActive("expired-token")) } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt index 4bef2826de..2096a2e791 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt @@ -136,8 +136,8 @@ class DefaultRepositoryTest : RobolectricTest() { @Before fun setUp() { hiltRule.inject() - dispatcherProvider = DefaultDispatcherProvider() sharedPreferenceHelper = SharedPreferencesHelper(application, gson) + dispatcherProvider = DefaultDispatcherProvider() defaultRepository = DefaultRepository( fhirEngine = fhirEngine, 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 d276880a9a..9f89f92c32 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 @@ -112,7 +112,6 @@ import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.robolectric.RobolectricTest import org.smartregister.fhircore.engine.rule.CoroutineTestRule -import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.REFERENCE import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MM_DD import org.smartregister.fhircore.engine.util.extension.asReference @@ -147,8 +146,6 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { @Inject lateinit var fhirEngine: FhirEngine - @Inject lateinit var testDispatcher: DispatcherProvider - @Inject lateinit var configurationRegistry: ConfigurationRegistry private val context: Context = ApplicationProvider.getApplicationContext() @@ -171,7 +168,6 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { fun setup() { hiltRule.inject() structureMapUtilities = StructureMapUtilities(transformSupportServices.simpleWorkerContext) - every { defaultRepository.dispatcherProvider } returns testDispatcher every { defaultRepository.fhirEngine } returns fhirEngine coEvery { defaultRepository.create(anyBoolean(), any()) } returns listOf() diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirResourceExpireWorkerTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirResourceExpireWorkerTest.kt index e6ce1dfe6b..c8594b2ed3 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirResourceExpireWorkerTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirResourceExpireWorkerTest.kt @@ -95,7 +95,7 @@ class FhirResourceExpireWorkerTest : RobolectricTest() { period = Period().apply { end = DateTime().plusDays(-2).toDate() } } } - val serviceRequest = + private val serviceRequest = ServiceRequest().apply { id = UUID.randomUUID().toString() status = ServiceRequest.ServiceRequestStatus.COMPLETED diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt index 1fb5145f05..2a11213eb1 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt @@ -18,9 +18,6 @@ package org.smartregister.fhircore.engine.util.extension import android.app.Application import androidx.test.core.app.ApplicationProvider -import ca.uhn.fhir.context.FhirContext -import ca.uhn.fhir.context.FhirVersionEnum -import ca.uhn.fhir.parser.IParser import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -691,46 +688,6 @@ class ResourceExtensionTest : RobolectricTest() { ) } - @Test - fun testGenerateMissingItemsFromQuestionnaireShouldNotThrowException() { - val patientRegistrationQuestionnaire = - "register-patient-missingitems/missingitem-questionnaire.json".readFile() - val patientRegistrationQuestionnaireResponse = - "register-patient-missingitems/missingitem-questionnaire-response.json".readFile() - val iParser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() - val questionnaire = - iParser.parseResource(Questionnaire::class.java, patientRegistrationQuestionnaire) - val questionnaireResponse = - iParser.parseResource( - QuestionnaireResponse::class.java, - patientRegistrationQuestionnaireResponse, - ) - - questionnaire.item.generateMissingItems(questionnaireResponse.item) - - Assert.assertTrue(questionnaireResponse.item.size <= questionnaire.item.size) - } - - @Test - fun testGenerateMissingItemsFromQuestionnaireResponseShouldNotThrowException() { - val patientRegistrationQuestionnaire = - "register-patient-missingitems/missingitem-questionnaire.json".readFile() - val patientRegistrationQuestionnaireResponse = - "register-patient-missingitems/missingitem-questionnaire-response.json".readFile() - val iParser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() - val questionnaire = - iParser.parseResource(Questionnaire::class.java, patientRegistrationQuestionnaire) - val questionnaireResponse = - iParser.parseResource( - QuestionnaireResponse::class.java, - patientRegistrationQuestionnaireResponse, - ) - - questionnaireResponse.generateMissingItems(questionnaire) - - Assert.assertTrue(questionnaireResponse.item.size <= questionnaire.item.size) - } - @Test fun `prepareQuestionsForReadingOrEditing should set readOnly to true when passed`() { val questionnaire = Questionnaire() diff --git a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt index 9713580412..2bc05b9705 100644 --- a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt +++ b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt @@ -16,7 +16,6 @@ package org.smartregister.fhircore.geowidget.screens -import CoroutineTestRule import android.os.Build import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider @@ -59,8 +58,6 @@ class GeoWidgetViewModelTest { @get:Rule(order = 1) var instantTaskExecutorRule = InstantTaskExecutorRule() - @get:Rule(order = 2) var coroutinesTestRule = CoroutineTestRule() - @Inject lateinit var configService: ConfigService @Inject lateinit var fhirPathDataExtractor: FhirPathDataExtractor @@ -86,7 +83,7 @@ class GeoWidgetViewModelTest { @Before fun setUp() { MockitoAnnotations.initMocks(this) - viewModel = GeoWidgetViewModel(dispatcherProvider) + viewModel = GeoWidgetViewModel() hiltRule.inject() sharedPreferencesHelper = mockk() configurationRegistry = mockk() @@ -104,7 +101,7 @@ class GeoWidgetViewModelTest { context = ApplicationProvider.getApplicationContext(), ), ) - geoWidgetViewModel = spyk(GeoWidgetViewModel(dispatcherProvider)) + geoWidgetViewModel = spyk(GeoWidgetViewModel()) coEvery { defaultRepository.create(any()) } returns emptyList() } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 12e8cf0182..623acdc340 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -3,12 +3,13 @@ accompanist = "0.23.1" activity-compose = "1.8.2" androidJunit5 = "1.8.2.1" androidx-camera = "1.4.0-rc01" -androidx-paging = "3.3.0" -androidx-test = "1.6.1" +androidx-paging = "3.3.2" +androidx-test= "1.6.2" appcompat = "1.7.0" -benchmark-junit = "1.2.4" +benchmark-junit = "1.3.0" cardview = "1.0.0" -compose-material-icons = "1.6.8" +common-utils = "1.0.0-SNAPSHOT" +compose-ui = "1.6.8" compressor = "3.0.1" constraintlayout = "2.1.4" constraintlayout-compose = "1.0.1" @@ -19,29 +20,26 @@ coverallsGradlePlugin = "2.12.2" cqfFhirCr = "3.0.0-PRE9" dagger-hilt = "2.51" datastore = "1.1.1" -desugar-jdk-libs = "2.0.4" -dokkaBase = "1.8.20" +desugar-jdk-libs = "2.1.2" +dokkaBase = "1.9.20" easyRulesCore = "4.1.1-SNAPSHOT" espresso-core = "3.6.1" -fhir-common-utils = "1.0.0-SNAPSHOT" -fhir-sdk-contrib-barcode = "0.1.0-beta3-preview7-SNAPSHOT" -fhir-sdk-contrib-locationwidget = "0.1.0-alpha01-preview2-SNAPSHOT" -fhir-sdk-data-capture = "1.1.0-preview12-SNAPSHOT" -fhir-sdk-engine = "1.0.0-preview11.3-SNAPSHOT" -fhir-sdk-knowledge = "0.1.0-alpha03-preview5-SNAPSHOT" -fhir-sdk-workflow = "0.1.0-alpha04-preview10-SNAPSHOT" -foundation = "1.6.8" -fragment-ktx = "1.8.1" +fhir-sdk-contrib-barcode = "0.1.0-beta3-preview7-rc1-SNAPSHOT" +fhir-sdk-contrib-locationwidget = "0.1.0-alpha01-preview2-rc1-SNAPSHOT" +fhir-sdk-data-capture = "1.1.0-preview14-rc1-SNAPSHOT" +fhir-sdk-engine = "1.0.0-preview14-rc3-SNAPSHOT" +fhir-sdk-knowledge = "0.1.0-alpha03-preview5-rc1-SNAPSHOT" +fhir-sdk-workflow = "0.1.0-alpha04-preview10-rc1-SNAPSHOT" +fragment-ktx = "1.8.2" glide = "4.16.0" gradle = "8.3.2" gson = "2.10.1" hilt = "1.2.0" +java-jwt = "4.4.0" jetbrains = "1.9.20" -jetbrains-kotlin-jvm="1.9.22" -jjwt = "0.9.1" joda-time = "2.10.14" json = "20230618" -jsonPath = "2.8.0" +jsonPath = "2.9.0" junit = "1.2.1" junit-jupiter = "5.10.3" junit-ktx = "1.2.1" @@ -51,10 +49,11 @@ kotlinx-coroutines = "1.9.0" kotlinx-serialization-json = "1.6.0" kt3k-coveralls-ver="2.12.0" ktlint = "0.50.0" -kujaku-library = "0.10.6-ALPHA-SNAPSHOT" +kujaku-library = "0.10.6-2-SNAPSHOT" +kujaku-mapbox-sdk-turf = "7.2.0" leakcanary-android = "2.10" -lifecycle= "2.8.3" -mapbox-sdk-turf = "7.2.0" +lifecycle= "2.8.5" +logback-android = "3.0.0" material = "1.12.0" mlkit-barcode-scanning = "17.3.0" mockk = "1.13.8" @@ -73,16 +72,15 @@ prettytime = "5.0.2.Final" retrofit = "2.9.0" retrofit-mock = "2.9.0" retrofit2-kotlinx-serialization-converter = "0.8.0" -robolectric = "4.10.3" +robolectric = "4.13" rules = "1.6.1" security-crypto = "1.1.0-alpha06" -slf4j-nop = "1.7.36" +slf4j-nop = "2.0.7" spotlessPluginGradle = "6.25.0" stax-api = "1.0-2" timber = "5.0.1" -ui = "1.6.3" uiautomator = "2.3.0" -work = "2.9.0" +work = "2.9.1" xercesImpl = "2.12.2" [libraries] @@ -98,8 +96,8 @@ activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } benchmark-junit = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark-junit" } cardview = { group = "androidx.cardview", name = "cardview", version.ref = "cardview" } -compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "compose-material-icons" } -compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose-material-icons" } +compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "compose-ui" } +compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose-ui" } compressor = { group = "id.zelory", name = "compressor", version.ref = "compressor" } constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayout-compose" } @@ -121,9 +119,9 @@ datastore-preferences = { group = "androidx.datastore", name = "datastore-prefer dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokkaBase" } easy-rules-jexl = { group = "org.smartregister", name = "easy-rules-jexl", version.ref = "easyRulesCore" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } -fhir-common-utils = { group = "org.smartregister", name = "fhir-common-utils", version.ref = "fhir-common-utils" } +fhir-common-utils = { group = "org.smartregister", name = "fhir-common-utils", version.ref = "common-utils" } fhir-engine = { group = "org.smartregister", name = "engine", version.ref = "fhir-sdk-engine" } -foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } +foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "compose-ui" } fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment-ktx" } fragment-testing = { group = "androidx.fragment", name = "fragment-testing", version.ref = "fragment-ktx" } glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } @@ -132,7 +130,7 @@ gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt" } hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hilt" } -jjwt = { group = "io.jsonwebtoken", name = "jjwt", version.ref = "jjwt" } +java-jwt = { module = "com.auth0:java-jwt", version.ref = "java-jwt" } joda-time = { group = "joda-time", name = "joda-time", version.ref = "joda-time" } json = { group = "org.json", name = "json", version.ref = "json" } json-path = { module = "com.jayway.jsonpath:json-path", version.ref = "jsonPath" } @@ -157,7 +155,8 @@ leakcanary-android = { group = "com.squareup.leakcanary", name = "leakcanary-and lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" } lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } -mapbox-sdk-turf = { group = "com.mapbox.mapboxsdk", name = "mapbox-sdk-turf", version.ref = "mapbox-sdk-turf" } +logback-android = { module = "com.github.tony19:logback-android", version.ref = "logback-android" } +mapbox-sdk-turf = { group = "com.mapbox.mapboxsdk", name = "mapbox-sdk-turf", version.ref = "kujaku-mapbox-sdk-turf" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkit-barcode-scanning"} mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } @@ -182,16 +181,16 @@ retrofit2-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit" robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } rules = { group = "androidx.test", name = "rules", version.ref = "rules" } runner = { module = "androidx.test:runner", version.ref = "androidx-test" } -runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "ui" } +runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "compose-ui" } security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security-crypto" } slf4j-nop = { group = "org.slf4j", name = "slf4j-nop", version.ref = "slf4j-nop" } stax-api = { group = "javax.xml.stream", name = "stax-api", version.ref = "stax-api" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } -ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } -ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "ui" } -ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "ui" } -ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui" } -ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "ui" } +ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose-ui" } +ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose-ui" } +ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose-ui" } +ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose-ui" } +ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose-ui" } uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work" } @@ -206,7 +205,7 @@ dagger-hilt-android= { id = "com.google.dagger.hilt.android", version.ref = "dag kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-serialization" } kt3k-coveralls = { id = "com.github.kt3k.coveralls", version.ref = "kt3k-coveralls-ver" } org-jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "jetbrains" } -org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrains-kotlin-jvm" } +org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } org-owasp-dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "owasp" } [bundles] diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index e4dd623b4c..537bdce8f5 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -425,6 +425,7 @@ tasks.withType { configurations { all { exclude(group = "xpp3") } } dependencies { + implementation(libs.gms.play.services.location) coreLibraryDesugaring(libs.core.desugar) // Application dependencies @@ -435,7 +436,6 @@ dependencies { implementation(libs.material) implementation(libs.dagger.hilt.android) implementation(libs.hilt.work) - implementation(libs.gms.play.services.location) implementation(libs.mlkit.barcode.scanning) implementation(libs.bundles.cameraX) diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/Faker.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/Faker.kt index 1350636997..01081c3ca8 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/Faker.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/Faker.kt @@ -18,6 +18,7 @@ package org.smartregister.fhircore.quest.integration import androidx.test.core.app.ApplicationProvider import androidx.test.platform.app.InstrumentationRegistry +import com.google.android.fhir.CrudFhirEngine import com.google.android.fhir.FhirEngine import com.google.android.fhir.LocalChange import com.google.android.fhir.SearchResult @@ -109,6 +110,10 @@ object Faker { } override suspend fun update(vararg resource: Resource) {} + + override suspend fun withTransaction(block: suspend CrudFhirEngine.() -> Unit) { + TODO("Not yet implemented") + } } val fhirResourceService = diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt index a7dfcbc583..79f6f5ec2d 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt @@ -30,6 +30,8 @@ import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeUp import androidx.navigation.compose.rememberNavController +import androidx.paging.CombinedLoadStates +import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -38,6 +40,7 @@ import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.SyncOperation import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every import io.mockk.mockk import java.time.OffsetDateTime import kotlinx.coroutines.flow.flowOf @@ -262,6 +265,14 @@ class RegisterScreenTest { val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) val pagingItems = mockk>().apply {} + val combinedLoadState: CombinedLoadStates = mockk() + val loadState: LoadState = mockk() + + every { pagingItems.itemCount } returns 0 + every { combinedLoadState.refresh } returns loadState + every { combinedLoadState.append } returns loadState + every { loadState.endOfPaginationReached } returns true + every { pagingItems.loadState } returns combinedLoadState composeTestRule.setContent { RegisterScreen( diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index ea0e09ac7f..1c69856d16 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -9,18 +9,17 @@ + android:exported="true"> @@ -47,31 +45,26 @@ + android:launchMode="singleTop" /> + android:launchMode="singleTop" /> + android:launchMode="singleTop" /> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepository.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepository.kt index a50b491869..0c56bb0721 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepository.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepository.kt @@ -24,6 +24,7 @@ import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.NoSuchElementException import javax.inject.Inject import kotlinx.coroutines.withContext import org.hl7.fhir.exceptions.FHIRException @@ -47,7 +48,6 @@ class MeasureReportRepository @Inject constructor( override val fhirEngine: FhirEngine, - override val dispatcherProvider: DispatcherProvider, override val sharedPreferencesHelper: SharedPreferencesHelper, override val configurationRegistry: ConfigurationRegistry, override val configService: ConfigService, @@ -57,10 +57,10 @@ constructor( override val fhirPathDataExtractor: FhirPathDataExtractor, override val parser: IParser, @ApplicationContext override val context: Context, + override val dispatcherProvider: DispatcherProvider, ) : DefaultRepository( fhirEngine = fhirEngine, - dispatcherProvider = dispatcherProvider, sharedPreferencesHelper = sharedPreferencesHelper, configurationRegistry = configurationRegistry, configService = configService, @@ -68,6 +68,7 @@ constructor( fhirPathDataExtractor = fhirPathDataExtractor, parser = parser, context = context, + dispatcherProvider = dispatcherProvider, ) { /** @@ -91,31 +92,29 @@ constructor( ): List { val measureReport = mutableListOf() try { - withContext(dispatcherProvider.io()) { - if (subjects.isNotEmpty()) { - subjects - .map { - runMeasureReport( - measureUrl = measureUrl, - reportType = MeasureReportViewModel.SUBJECT, - startDateFormatted = startDateFormatted, - endDateFormatted = endDateFormatted, - subject = it, - practitionerId = practitionerId, - ) - } - .forEach { subject -> measureReport.add(subject) } - } else { - runMeasureReport( + if (subjects.isNotEmpty()) { + subjects + .map { + runMeasureReport( measureUrl = measureUrl, - reportType = MeasureReportViewModel.POPULATION, + reportType = MeasureReportViewModel.SUBJECT, startDateFormatted = startDateFormatted, endDateFormatted = endDateFormatted, - subject = null, + subject = it, practitionerId = practitionerId, ) - .also { measureReport.add(it) } - } + } + .forEach { subject -> measureReport.add(subject) } + } else { + runMeasureReport( + measureUrl = measureUrl, + reportType = MeasureReportViewModel.POPULATION, + startDateFormatted = startDateFormatted, + endDateFormatted = endDateFormatted, + subject = null, + practitionerId = practitionerId, + ) + .also { measureReport.add(it) } } measureReport.forEach { report -> @@ -130,6 +129,8 @@ constructor( } } catch (exception: NullPointerException) { Timber.e(exception, "Exception thrown with measureUrl: $measureUrl.") + } catch (exception: IllegalStateException) { + Timber.e(exception, "Exception thrown with measureUrl: $measureUrl.") } return measureReport } @@ -152,17 +153,29 @@ constructor( subject: String?, practitionerId: String?, ): MeasureReport { - return fhirOperator.evaluateMeasure( - measure = - knowledgeManager - .loadResources(ResourceType.Measure.name, measureUrl, null, null, null) - .firstOrNull() as Measure, - start = startDateFormatted, - end = endDateFormatted, - reportType = reportType, - subjectId = subject, - practitioner = practitionerId.takeIf { it?.isNotBlank() == true }, - ) + return withContext(dispatcherProvider.io()) { + try { + fhirOperator.evaluateMeasure( + measure = + knowledgeManager + .loadResources(ResourceType.Measure.name, measureUrl, null, null, null) + .firstOrNull() as Measure, + start = startDateFormatted, + end = endDateFormatted, + reportType = reportType, + subjectId = subject, + practitioner = practitionerId.takeIf { it?.isNotBlank() == true }, + ) + } catch (exception: IllegalArgumentException) { + Timber.e(exception) + throw IllegalArgumentException() + } catch (exception: NoSuchElementException) { + Timber.e(exception) + throw IllegalStateException( + "No FHIR resource found in Knowledge Manager with URL $measureUrl", + ) + } + } } /** diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt index 8ac0f9d169..a099937789 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import java.net.UnknownHostException import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.RequestBody.Companion.toRequestBody @@ -101,8 +102,10 @@ constructor( } } + private val exceptionHandler = CoroutineExceptionHandler { _, exception -> Timber.e(exception) } + private fun fetchRemoteConfigurations(appId: String?, context: Context) { - viewModelScope.launch { + viewModelScope.launch(exceptionHandler) { try { showProgressBar.postValue(true) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModel.kt index 29b192dcd0..302bfcd1a3 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModel.kt @@ -33,12 +33,12 @@ import com.google.android.material.datepicker.MaterialDatePicker import dagger.hilt.android.lifecycle.HiltViewModel import java.util.* import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.MeasureReport import org.hl7.fhir.r4.model.Observation @@ -170,7 +170,9 @@ constructor( ) } refreshData() - event.practitionerId?.let { evaluateMeasure(event.navController, practitionerId = it) } + event.practitionerId?.let { + viewModelScope.launch { evaluateMeasure(event.navController, practitionerId = it) } + } } is MeasureReportEvent.OnDateSelected -> { if (selectedDate != null) { @@ -262,8 +264,10 @@ constructor( return subjectData.value } + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> println(throwable) } + // TODO: Enhancement - use FhirPathEngine evaluator for data extraction - fun evaluateMeasure(navController: NavController, practitionerId: String? = null) { + suspend fun evaluateMeasure(navController: NavController, practitionerId: String? = null) { // Run evaluate measure only for existing report if (reportConfigurations.isNotEmpty()) { // Retrieve and parse dates to (2020-11-16) @@ -277,89 +281,87 @@ constructor( .parseDate(SDF_D_MMM_YYYY_WITH_COMA) ?.formatDate(SDF_YYYY_MM_DD)!! - viewModelScope.launch { - kotlin - .runCatching { - // Show Progress indicator while evaluating measure - toggleProgressIndicatorVisibility(true) - val result = - reportConfigurations.flatMap { config -> - val subjects = mutableListOf() - subjects.addAll(measureReportRepository.fetchSubjects(config)) - - // If a practitioner Id is available, add it to the list of subjects - if (practitionerId?.isNotBlank() == true && subjects.isEmpty()) { - subjects.add("${Practitioner().resourceType.name}/$practitionerId") - } + try { + // Show Progress indicator while evaluating measure + toggleProgressIndicatorVisibility(true) + val result = + reportConfigurations.flatMap { config -> + val subjects = mutableListOf() + subjects.addAll(measureReportRepository.fetchSubjects(config)) + + // If a practitioner Id is available and if the subjects list is empty, add it to the + // list of subjects + if (practitionerId?.isNotBlank() == true && subjects.isEmpty()) { + subjects.add("${Practitioner().resourceType.name}/$practitionerId") + } - val existingReports = - fhirEngine.retrievePreviouslyGeneratedMeasureReports( - startDateFormatted = startDateFormatted, - endDateFormatted = endDateFormatted, - measureUrl = config.url, - subjects = listOf(), - ) + val existingReports = + fhirEngine.retrievePreviouslyGeneratedMeasureReports( + startDateFormatted = startDateFormatted, + endDateFormatted = endDateFormatted, + measureUrl = config.url, + subjects = listOf(), + ) + + val existingValidReports = mutableListOf() - val existingValidReports = mutableListOf() - - existingReports - .groupBy { it.subject.reference } - .forEach { entry -> - if ( - entry.value.size > 1 && - entry.value.distinctBy { it.measure }.size > 1 && - entry.value.distinctBy { it.type }.size > 1 - ) { - return@forEach - } else { - existingValidReports.addAll(entry.value) - } - } - - // if report is of current month or does not exist generate a new one and replace - // existing + existingReports + .groupBy { it.subject.reference } + .forEach { entry -> if ( - endDateFormatted - .parseDate(SDF_YYYY_MM_DD)!! - .formatDate(SDF_YYYY_MMM) - .contentEquals(Date().formatDate(SDF_YYYY_MMM)) || - existingValidReports.isEmpty() || - existingValidReports.size != subjects.size + entry.value.size > 1 && + entry.value.distinctBy { it.measure }.size > 1 && + entry.value.distinctBy { it.type }.size > 1 ) { - withContext(dispatcherProvider.io()) { - fhirEngine.loadCqlLibraryBundle(fhirOperator, config.url) - } - - measureReportRepository.evaluatePopulationMeasure( - measureUrl = config.url, - startDateFormatted = startDateFormatted, - endDateFormatted = endDateFormatted, - subjects = subjects, - existing = existingValidReports, - practitionerId = practitionerId, - ) + return@forEach } else { - existingValidReports + existingValidReports.addAll(entry.value) } } - _measureReportPopulationResultList.addAll( - formatPopulationMeasureReports(result, reportConfigurations), - ) - } - .onSuccess { - measureReportPopulationResults.value = _measureReportPopulationResultList - Timber.w("measureReportPopulationResults${measureReportPopulationResults.value}") - toggleProgressIndicatorVisibility(false) - // Show results of measure report for individual/population - navController.navigate(MeasureReportNavigationScreen.MeasureReportResult.route) { - launchSingleTop = true + // if report is of current month or does not exist generate a new one and replace + // existing + if ( + endDateFormatted + .parseDate(SDF_YYYY_MM_DD)!! + .formatDate(SDF_YYYY_MMM) + .contentEquals(Date().formatDate(SDF_YYYY_MMM)) || + existingValidReports.isEmpty() || + existingValidReports.size != subjects.size + ) { + fhirEngine.loadCqlLibraryBundle(fhirOperator, config.url) + + measureReportRepository.evaluatePopulationMeasure( + measureUrl = config.url, + startDateFormatted = startDateFormatted, + endDateFormatted = endDateFormatted, + subjects = subjects, + existing = existingValidReports, + practitionerId = practitionerId, + ) + } else { + existingValidReports } } - .onFailure { - Timber.w(it) - toggleProgressIndicatorVisibility(false) - } + + val measureReportPopulationResultList = + formatPopulationMeasureReports(result, reportConfigurations) + _measureReportPopulationResultList.addAll( + measureReportPopulationResultList, + ) + + // On success + measureReportPopulationResults.value = _measureReportPopulationResultList + // Timber.w("measureReportPopulationResults${measureReportPopulationResults.value}") + + // Show results of measure report for individual/population + navController.navigate(MeasureReportNavigationScreen.MeasureReportResult.route) { + launchSingleTop = true + } + } catch (e: Exception) { + Timber.e(e) + } finally { + toggleProgressIndicatorVisibility(false) } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt index 3c0c481f40..9de0c5a0d5 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt @@ -25,6 +25,7 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf import com.google.android.fhir.FhirEngine +import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.workflow.FhirOperator import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -35,10 +36,13 @@ import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit import java.util.Calendar import java.util.Date +import java.util.NoSuchElementException import java.util.concurrent.TimeUnit import kotlinx.coroutines.withContext +import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.r4.model.Measure import org.hl7.fhir.r4.model.MeasureReport +import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider @@ -67,6 +71,7 @@ constructor( val dispatcherProvider: DefaultDispatcherProvider, val fhirOperator: FhirOperator, val fhirEngine: FhirEngine, + private val knowledgeManager: KnowledgeManager, val workManager: WorkManager, ) : CoroutineWorker(appContext, workerParams) { @@ -127,8 +132,14 @@ constructor( val measureReport: MeasureReport? = withContext(dispatcherProvider.io()) { try { + val measureUrlResources: Iterable = + knowledgeManager.loadResources( + resourceType = ResourceType.Measure.name, + url = measureUrl, + ) + fhirOperator.evaluateMeasure( - measureUrl = measureUrl, + measure = measureUrlResources.first() as Measure, start = startDateFormatted, end = endDateFormatted, reportType = MeasureReportViewModel.POPULATION, @@ -140,6 +151,9 @@ constructor( } catch (exception: IllegalArgumentException) { Timber.e(exception) null + } catch (exception: NoSuchElementException) { + Timber.e(exception) + null } } if (measureReport != null) { diff --git a/android/quest/src/main/res/values-fr/strings.xml b/android/quest/src/main/res/values-fr/strings.xml index 4d45d4ce5e..ae4d01024d 100644 --- a/android/quest/src/main/res/values-fr/strings.xml +++ b/android/quest/src/main/res/values-fr/strings.xml @@ -99,6 +99,7 @@ Questionnaire introuvable, synchroniser tous les questionnaires pour régler ce problème Pas de visites Réponse du questionnaire invalide + Version Type de sujet manquant dans le questionnaire. Fournir Questionnaire.subjectType pour résoudre le problème. QuestionnaireConfig est requis mais manquant Erreur dans le remplissage de certains champs du questionnaire. Réponse au questionnaire non valide. @@ -126,7 +127,7 @@ Modifier Revoir les réponses Revoir - \\@android:string/cancel + Annuler Erreurs trouvées @@ -252,7 +253,6 @@ Optionnel Requis Requis\n - \u0020\u002a diff --git a/android/quest/src/main/res/values-in/strings.xml b/android/quest/src/main/res/values-in/strings.xml index 1e5339a53c..e77b34ba80 100644 --- a/android/quest/src/main/res/values-in/strings.xml +++ b/android/quest/src/main/res/values-in/strings.xml @@ -13,8 +13,6 @@ Tidak ada hasil tes ditemukan HASIL TES Tes terakhir - %1$s - 2.4 km - # Tugas Keluarga Kunjungan rutin @@ -88,11 +86,8 @@ Luaran Kehamilan Register - %1$s (%2$s) Dijadwalkan pada %1$s - GeoWidget Fragment Destination - Gagal mengekstraksi resources untuk %1$s StructureMap untuk Questionnaire tidak ada, QuestionnaireResponse disimpan Resources berhasil diekstraksi untuk %1$s @@ -101,7 +96,6 @@ Questionnaire tidak ditemukan. Sinkronkan semua kuesioner untuk memperbaiki Tidak ada kunjungan Respons kuesioner tidak valid - https://smartregister.org/app-version Versi aplikasi Jenis subjek pada kuesioner tidak ada. Berikan Questionnaire.subjectType untuk diselesaikan. QuestionnaireConfig diperlukan tetapi tidak ada. diff --git a/android/quest/src/main/res/values-sw/strings.xml b/android/quest/src/main/res/values-sw/strings.xml index d81539309a..4d8a8be8f7 100644 --- a/android/quest/src/main/res/values-sw/strings.xml +++ b/android/quest/src/main/res/values-sw/strings.xml @@ -73,4 +73,5 @@ Ramani ya Muundo Haipo kwa Hojaji , HojajiJibu limehifadhiwa Imetoa rasilimali za %1$s Imeshindwa kutoa nyenzo za dodoso %1$s + Futa yote diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index ac715710f0..adb04213d9 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -107,7 +107,7 @@ 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 + Application Version Missing subject type on questionnaire. Provide Questionnaire.subjectType to resolve. QuestionnaireConfig is required but missing. Error populating some questionnaire fields. Invalid QuestionnaireResponse. @@ -132,4 +132,5 @@ Scan QR Code Place your camera over the entire QR Code to start scanning Failed to get GPS location + \u0020\u002a diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt index 14a356e110..5b14d4dd22 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt @@ -23,6 +23,9 @@ import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import java.io.File import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Library import org.hl7.fhir.r4.model.Parameters @@ -37,9 +40,9 @@ import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.valueToString import org.smartregister.fhircore.quest.robolectric.RobolectricTest import org.smartregister.fhircore.quest.sdk.CqlBuilder -import org.smartregister.fhircore.quest.sdk.runBlockingOnWorkerThread @HiltAndroidTest +@OptIn(ExperimentalCoroutinesApi::class) class CqlContentTest : RobolectricTest() { @get:Rule var hiltRule = HiltAndroidRule(this) @@ -58,112 +61,115 @@ class CqlContentTest : RobolectricTest() { } @Test - fun runCqlLibraryTestForPqMedication() = runBlockingOnWorkerThread { - val resourceDir = "cql/pq-medication" - val cql = "$resourceDir/cql.txt".readFile() - - val cqlLibrary = buildCqlLibrary(cql) - - val dataBundle = - loadTestResultsSampleData().apply { - // output of test results cql is also added to input of this cql - "cql/test-results/sample" - .readDir() - .map { it.parseSampleResource() as Resource } - .forEach { addEntry().apply { resource = it } } - } + fun runCqlLibraryTestForPqMedication() = + runTest(context = UnconfinedTestDispatcher()) { + val resourceDir = "cql/pq-medication" + val cql = "$resourceDir/cql.txt".readFile() + + val cqlLibrary = buildCqlLibrary(cql) + + val dataBundle = + loadTestResultsSampleData().apply { + // output of test results cql is also added to input of this cql + "cql/test-results/sample" + .readDir() + .map { it.parseSampleResource() as Resource } + .forEach { addEntry().apply { resource = it } } + } - createTestData(dataBundle, cqlLibrary) + createTestData(dataBundle, cqlLibrary) - val result = - fhirOperator.evaluateLibrary( - cqlLibrary.url, - dataBundle.entry.find { it.resource.resourceType == ResourceType.Patient }!!.resource.id, - null, - ) as Parameters + val result = + fhirOperator.evaluateLibrary( + cqlLibrary.url, + dataBundle.entry.find { it.resource.resourceType == ResourceType.Patient }!!.resource.id, + null, + ) as Parameters - printResult(result) + printResult(result) - assertOutput( - "$resourceDir/output_medication_request.json", - result, - ResourceType.MedicationRequest, - ) - } + assertOutput( + "$resourceDir/output_medication_request.json", + result, + ResourceType.MedicationRequest, + ) + } @Test - fun runCqlLibraryTestForTestResults() = runBlockingOnWorkerThread { - val resourceDir = "cql/test-results" - val cql = "$resourceDir/cql.txt".readFile() + fun runCqlLibraryTestForTestResults() = + runTest(context = UnconfinedTestDispatcher()) { + val resourceDir = "cql/test-results" + val cql = "$resourceDir/cql.txt".readFile() - val cqlLibrary = buildCqlLibrary(cql) + val cqlLibrary = buildCqlLibrary(cql) - val dataBundle = loadTestResultsSampleData() + val dataBundle = loadTestResultsSampleData() - createTestData(dataBundle, cqlLibrary) + createTestData(dataBundle, cqlLibrary) - val result = - fhirOperator.evaluateLibrary( - cqlLibrary.url, - dataBundle.entry.find { it.resource.resourceType == ResourceType.Patient }!!.resource.id, - null, - null, - null, - ) as Parameters + val result = + fhirOperator.evaluateLibrary( + cqlLibrary.url, + dataBundle.entry.find { it.resource.resourceType == ResourceType.Patient }!!.resource.id, + null, + null, + null, + ) as Parameters - printResult(result) + printResult(result) - assertOutput("$resourceDir/sample/output_condition.json", result, ResourceType.Condition) - assertOutput( - "$resourceDir/sample/output_service_request.json", - result, - ResourceType.ServiceRequest, - ) - assertOutput( - "$resourceDir/sample/output_diagnostic_report.json", - result, - ResourceType.DiagnosticReport, - ) - } + assertOutput("$resourceDir/sample/output_condition.json", result, ResourceType.Condition) + assertOutput( + "$resourceDir/sample/output_service_request.json", + result, + ResourceType.ServiceRequest, + ) + assertOutput( + "$resourceDir/sample/output_diagnostic_report.json", + result, + ResourceType.DiagnosticReport, + ) + } @Test - fun runCqlLibraryTestForControlTest() = runBlockingOnWorkerThread { - val resourceDir = "cql/control-test" - val cql = "$resourceDir/cql.txt".readFile() - - val cqlLibrary = buildCqlLibrary(cql) - - val dataBundle = - loadTestResultsSampleData().apply { - addEntry().apply { - // questionnaire-response of test results is input of this cql - resource = - "test-results-questionnaire/questionnaire-response.json".parseSampleResourceFromFile() - as Resource + fun runCqlLibraryTestForControlTest() = + runTest(context = UnconfinedTestDispatcher()) { + val resourceDir = "cql/control-test" + val cql = "$resourceDir/cql.txt".readFile() + + val cqlLibrary = buildCqlLibrary(cql) + + val dataBundle = + loadTestResultsSampleData().apply { + addEntry().apply { + // questionnaire-response of test results is input of this cql + resource = + "test-results-questionnaire/questionnaire-response.json".parseSampleResourceFromFile() + as Resource + } } - } - createTestData(dataBundle, cqlLibrary) + createTestData(dataBundle, cqlLibrary) - val result = - fhirOperator.evaluateLibrary( - cqlLibrary.url, - dataBundle.entry.find { it.resource.resourceType == ResourceType.Patient }!!.resource.id, - null, - ) as Parameters + val result = + fhirOperator.evaluateLibrary( + cqlLibrary.url, + dataBundle.entry.find { it.resource.resourceType == ResourceType.Patient }!!.resource.id, + null, + ) as Parameters - printResult(result) + printResult(result) - Assert.assertTrue( - result.getParameterValues("OUTPUT").first().valueToString() == "Correct Result", - ) - Assert.assertEquals( - result.getParameterValues("OUTPUT").elementAt(1).valueToString(), - "\nDetails:\n" + - "Value (3.0) is in Normal G6PD Range 0-3\n" + - "Value (11.0) is in Normal Haemoglobin Range 8-12", - ) - } + Assert.assertTrue( + result.getParameterValues("OUTPUT").first().valueToString() == "Correct Result", + ) + Assert.assertEquals( + result.getParameterValues("OUTPUT").elementAt(1).valueToString(), + "\nDetails:\n" + + "Value (3.0) is in Normal G6PD Range 0-3\n" + + "Value (11.0) is in Normal Haemoglobin Range 8-12", + ) + } private fun buildCqlLibrary(cql: String): Library { val cqlCompiler = CqlBuilder.compile(cql) @@ -235,7 +241,7 @@ class CqlContentTest : RobolectricTest() { .replaceTimePart() println(cqlResultStr) - println(expectedResource as String) + println(expectedResource) Assert.assertEquals(expectedResource, cqlResultStr) } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/DataMigrationTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/DataMigrationTest.kt index 1aafa14d4f..7586d323b6 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/DataMigrationTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/DataMigrationTest.kt @@ -77,6 +77,24 @@ class DataMigrationTest : RobolectricTest() { dataMigration.migrate( migrationConfigs = listOf( + MigrationConfig( + resourceConfig = + FhirResourceConfig( + baseResource = ResourceConfig(resource = ResourceType.Patient), + ), + version = 7, + rules = + listOf( + RuleConfig(name = "value", actions = listOf("data.put('value', 'female')")), + ), + updateValues = + listOf( + UpdateValueConfig( + jsonPathExpression = "\$.gender", + computedValueKey = "value", + ), + ), + ), MigrationConfig( resourceConfig = FhirResourceConfig( @@ -103,9 +121,9 @@ class DataMigrationTest : RobolectricTest() { Assert.assertTrue(updatedPatient?.gender != patient.gender) Assert.assertEquals(Enumerations.AdministrativeGender.FEMALE, updatedPatient?.gender) - // Version updated to 2 + // Version updated to 7 (the maximum migration version) Assert.assertEquals( - 2, + 7, preferenceDataStore.read(PreferenceDataStore.MIGRATION_VERSION).first(), ) } @@ -179,9 +197,9 @@ class DataMigrationTest : RobolectricTest() { Assert.assertNotNull(updatedTask?.basedOn) Assert.assertEquals("CarePlan/${carePlan.logicalId}", updatedTask?.basedOnFirstRep?.reference) - // Version updated to 2 + // Version updated to 1 Assert.assertEquals( - 2, + 1, preferenceDataStore.read(PreferenceDataStore.MIGRATION_VERSION).first(), ) } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/QuestXFhirQueryResolverTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/QuestXFhirQueryResolverTest.kt index cade896cb2..94390c95a8 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/QuestXFhirQueryResolverTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/QuestXFhirQueryResolverTest.kt @@ -47,7 +47,7 @@ class QuestXFhirQueryResolverTest : RobolectricTest() { @Test fun testQuestXFhirQueryResolver() = runTest(timeout = 120.seconds) { - val patient = Patient() + val patient = Patient().apply { setActive(true) } val task = Task() fhirEngine.create(patient, task) val xFhirResolver = QuestXFhirQueryResolver(fhirEngine) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportPagingSourceTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportPagingSourceTest.kt index e16149d5c8..5b9a2fb48d 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportPagingSourceTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportPagingSourceTest.kt @@ -49,7 +49,6 @@ import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor import org.smartregister.fhircore.engine.rulesengine.RulesFactory import org.smartregister.fhircore.engine.rulesengine.services.LocationService -import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.quest.app.fakes.Faker @@ -108,7 +107,7 @@ class MeasureReportPagingSourceTest : RobolectricTest() { spyk( RegisterRepository( fhirEngine = fhirEngine, - dispatcherProvider = DefaultDispatcherProvider(), + dispatcherProvider = dispatcherProvider, sharedPreferencesHelper = mockk(), configurationRegistry = configurationRegistry, configService = mockk(), diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepositoryTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepositoryTest.kt index 5dc3b41746..cc4f69387d 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepositoryTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/data/report/measure/MeasureReportRepositoryTest.kt @@ -49,7 +49,6 @@ import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor import org.smartregister.fhircore.engine.rulesengine.RulesFactory import org.smartregister.fhircore.engine.rulesengine.services.LocationService -import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MM_DD import org.smartregister.fhircore.engine.util.extension.firstDayOfMonth @@ -114,7 +113,7 @@ class MeasureReportRepositoryTest : RobolectricTest() { spyk( RegisterRepository( fhirEngine = fhirEngine, - dispatcherProvider = DefaultDispatcherProvider(), + dispatcherProvider = dispatcherProvider, sharedPreferencesHelper = mockk(), configurationRegistry = configurationRegistry, configService = mockk(), @@ -128,7 +127,6 @@ class MeasureReportRepositoryTest : RobolectricTest() { measureReportRepository = MeasureReportRepository( fhirEngine = fhirEngine, - dispatcherProvider = DefaultDispatcherProvider(), sharedPreferencesHelper = mockk(), configurationRegistry = configurationRegistry, configService = mockk(), @@ -138,6 +136,7 @@ class MeasureReportRepositoryTest : RobolectricTest() { fhirPathDataExtractor = mockk(), parser = parser, context = ApplicationProvider.getApplicationContext(), + dispatcherProvider = dispatcherProvider, ) } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/RobolectricTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/RobolectricTest.kt index 2e3a93709a..0698d2978b 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/RobolectricTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/RobolectricTest.kt @@ -100,13 +100,13 @@ abstract class RobolectricTest { fun File.parseSampleResource(): IBaseResource = sanitizeSampleResourceContent(this.readText()) - fun sanitizeSampleResourceContent(content: String): IBaseResource = + private fun sanitizeSampleResourceContent(content: String): IBaseResource = content .replace("#TODAY", Date().formatDate(SDF_YYYY_MM_DD)) .replace("#NOW", DateTimeType.now().valueAsString) .let { FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().parseResource(it) } - fun IBaseResource.convertToString(trimTime: Boolean) = + fun IBaseResource.convertToString(trimTime: Boolean): String = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().encodeResourceToString(this).let { // replace time part 11:11:11+05:00 with xx:xx:xx+xx:xx if (trimTime) { @@ -119,8 +119,9 @@ abstract class RobolectricTest { fun String.replaceTimePart() = // replace time part 11:11:11+05:00 with xx:xx:xx+xx:xx // replace time part 11:11:11.111+05:00 with xx:xx:xx+xx:xx - this.replace(Regex("\\d{2}:\\d{2}:\\d{2}.\\d{2}:\\d{2}"), "xx:xx:xx+xx:xx") - .replace(Regex("\\d{2}:\\d{2}:\\d{2}.\\d{3}.\\d{2}:\\d{2}"), "xx:xx:xx+xx:xx") + // replace time part 18:33:04.520481+03:00 + this.replace(Regex("\\d{2}:\\d{2}:\\d{2}.\\d[0-9,+]+:\\d{2}"), "xx:xx:xx+xx:xx") + .replace(Regex("\\d{2}:\\d{2}:\\d{2}.\\d{3}.\\d[0-9,+]+:\\d{2}"), "xx:xx:xx+xx:xx") fun buildStructureMapUtils(): StructureMapUtilities { val pcm = FilesystemPackageCacheManager(true) @@ -136,7 +137,8 @@ abstract class RobolectricTest { return StructureMapUtilities(contextR4, transformSupportServices) } - fun StructureMapUtilities.worker(): IWorkerContext = ReflectionHelpers.getField(this, "worker") + private fun StructureMapUtilities.worker(): IWorkerContext = + ReflectionHelpers.getField(this, "worker") fun transform( scu: StructureMapUtilities, diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/WorkManagerRule.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/WorkManagerRule.kt index 9239735acb..515eb20688 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/WorkManagerRule.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/robolectric/WorkManagerRule.kt @@ -17,8 +17,10 @@ package org.smartregister.fhircore.quest.robolectric import android.util.Log +import androidx.test.core.app.ApplicationProvider import androidx.test.platform.app.InstrumentationRegistry import androidx.work.Configuration +import androidx.work.WorkManager import androidx.work.testing.SynchronousExecutor import androidx.work.testing.WorkManagerTestInitHelper import org.junit.rules.TestRule @@ -37,7 +39,11 @@ class WorkManagerRule : TestRule { .setExecutor(SynchronousExecutor()) .build() WorkManagerTestInitHelper.initializeTestWorkManager(context, config) - base.evaluate() + try { + base.evaluate() + } finally { + WorkManager.getInstance(ApplicationProvider.getApplicationContext()).cancelAllWork() + } } } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/sdk/CqlBuilder.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/sdk/CqlBuilder.kt index e193864813..28b710e0d7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/sdk/CqlBuilder.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/sdk/CqlBuilder.kt @@ -16,7 +16,6 @@ package org.smartregister.fhircore.quest.sdk -import ca.uhn.fhir.context.FhirContext import java.io.InputStream import org.cqframework.cql.cql2elm.CqlTranslator import org.cqframework.cql.cql2elm.LibraryManager @@ -34,7 +33,6 @@ import org.junit.Assert.fail // required object CqlBuilder : Loadable() { - private val jsonParser = FhirContext.forR4Cached().newJsonParser() /** * Compiles a CQL Text to ELM diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt index a8889e10e3..ece7d30f2e 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt @@ -34,9 +34,9 @@ import io.mockk.verify import java.net.UnknownHostException import java.nio.charset.StandardCharsets import javax.inject.Inject -import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody @@ -131,23 +131,35 @@ class AppSettingViewModelTest : RobolectricTest() { @Test fun testFetchConfigurations() = - runTest(timeout = 90.seconds) { - fhirEngine.create(Composition().apply { id = "sampleComposition" }) + runTest(timeout = 90.seconds, context = UnconfinedTestDispatcher()) { val appId = "test_app_id" appSettingViewModel.onApplicationIdChanged(appId) - coEvery { fhirResourceDataSource.getResource(any()) } returns - Bundle().apply { - addEntry().resource = - Composition().apply { - addSection().apply { this.focus = Reference().apply { reference = "Binary/123" } } - } + coEvery { + appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any()) + } returns + Composition().apply { + addSection().apply { this.focus = Reference().apply { reference = "Binary/123" } } } + + coEvery { + appSettingViewModel.configurationRegistry.loadConfigurations(any(), any(), any()) + } just runs + + coEvery { appSettingViewModel.fhirResourceDataSource.post(any(), any()) } returns Bundle() + coEvery { appSettingViewModel.defaultRepository.createRemote(any(), any()) } just runs + coEvery { + appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId( + appId, + QuestBuildConfig.VERSION_CODE, + ) + } returns null + appSettingViewModel.fetchConfigurations(context) - coVerify { fhirResourceDataSource.getResource(any()) } + coVerify { appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any()) } coVerify { appSettingViewModel.defaultRepository.createRemote(any(), any()) } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt index 0ec21168c4..cbe5c9c652 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt @@ -140,12 +140,11 @@ class GeoWidgetLauncherViewModelTest : RobolectricTest() { } @Test - fun testRetrieveLocationsShouldReturnGeoJsonFeatureList() { - runTest { - viewModel.retrieveLocations(geoWidgetConfiguration, null) - assertTrue(viewModel.geoJsonFeatures.value.isNotEmpty()) - assertEquals("loc1", viewModel.geoJsonFeatures.value.first().id) - } + @Ignore("Tech debt : Tracked by issue https://github.com/opensrp/fhircore/issues/3514") + fun testRetrieveLocationsShouldReturnGeoJsonFeatureList() = runTest { + viewModel.retrieveLocations(geoWidgetConfiguration, null) + assertTrue(viewModel.geoJsonFeatures.value.isNotEmpty()) + assertEquals("loc1", viewModel.geoJsonFeatures.value.first().id) } @Test 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 5d36f3a84e..2c80d74397 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 @@ -45,7 +45,7 @@ import junit.framework.TestCase.assertTrue import kotlin.test.assertNotNull import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Enumerations @@ -68,8 +68,8 @@ import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType import org.smartregister.fhircore.engine.domain.model.RuleConfig -import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString +import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest @@ -88,8 +88,6 @@ class QuestionnaireActivityTest : RobolectricTest() { private lateinit var questionnaireActivityController: ActivityController private lateinit var questionnaireActivity: QuestionnaireActivity - @Inject lateinit var testDispatcherProvider: DispatcherProvider - @BindValue lateinit var defaultRepository: DefaultRepository @BindValue @@ -103,7 +101,6 @@ class QuestionnaireActivityTest : RobolectricTest() { } defaultRepository = mockk(relaxUnitFun = true) { - every { dispatcherProvider } returns testDispatcherProvider every { fhirEngine } returns spyk(this@QuestionnaireActivityTest.fhirEngine) } questionnaireConfig = @@ -184,18 +181,20 @@ class QuestionnaireActivityTest : RobolectricTest() { setupActivity() Assert.assertTrue(questionnaireActivity.supportFragmentManager.fragments.isNotEmpty()) - val firstFragment = questionnaireActivity.supportFragmentManager.fragments.firstOrNull() + val firstFragment = + questionnaireActivity.supportFragmentManager.fragments[ + questionnaireActivity.supportFragmentManager.fragments.size - 1, + ] Assert.assertTrue(firstFragment is QuestionnaireFragment) // Questionnaire should be the same val fragmentQuestionnaire = - questionnaireActivity.supportFragmentManager.fragments - .firstOrNull() + firstFragment ?.arguments ?.getString("questionnaire") ?.decodeResourceFromString() - Assert.assertEquals(questionnaire.id, fragmentQuestionnaire?.id) + Assert.assertEquals(questionnaire.id, fragmentQuestionnaire?.id!!.extractLogicalIdUuid()) val sortedQuestionnaireItemLinkIds = questionnaire.item.map { it.linkId }.sorted().joinToString(",") val sortedFragmentQuestionnaireItemLinkIds = @@ -205,13 +204,12 @@ class QuestionnaireActivityTest : RobolectricTest() { } @Test - fun testThatOnBackPressShowsConfirmationAlertDialog() = - runTest(UnconfinedTestDispatcher()) { - setupActivity() - questionnaireActivity.onBackPressedDispatcher.onBackPressed() - val dialog = shadowOf(ShadowAlertDialog.getLatestAlertDialog()) - Assert.assertNotNull(dialog) - } + fun testThatOnBackPressShowsConfirmationAlertDialog() = runBlocking { + setupActivity() + questionnaireActivity.onBackPressedDispatcher.onBackPressed() + val dialog = shadowOf(ShadowAlertDialog.getLatestAlertDialog()) + Assert.assertNotNull(dialog) + } @Test fun `setupLocationServices should open location settings if location is disabled`() { 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 88f62221e5..578a91120f 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 @@ -26,6 +26,7 @@ import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.mapping.ResourceMapper import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get +import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.search.Search import com.google.android.fhir.workflow.FhirOperator import dagger.hilt.android.testing.HiltAndroidRule @@ -41,6 +42,7 @@ import io.mockk.slot import io.mockk.spyk import io.mockk.unmockkObject import io.mockk.verify +import java.io.File import java.util.Date import java.util.UUID import javax.inject.Inject @@ -51,6 +53,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Address +import org.hl7.fhir.r4.model.Attachment import org.hl7.fhir.r4.model.Basic import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.Bundle @@ -66,6 +69,7 @@ import org.hl7.fhir.r4.model.Flag import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.IntegerType +import org.hl7.fhir.r4.model.Library import org.hl7.fhir.r4.model.ListResource import org.hl7.fhir.r4.model.Location import org.hl7.fhir.r4.model.Observation @@ -103,6 +107,7 @@ import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.appendPractitionerInfo import org.smartregister.fhircore.engine.util.extension.asReference import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString +import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.find import org.smartregister.fhircore.engine.util.extension.isToday @@ -138,6 +143,8 @@ class QuestionnaireViewModelTest : RobolectricTest() { @Inject lateinit var parser: IParser + @Inject lateinit var knowledgeManager: KnowledgeManager + private lateinit var samplePatientRegisterQuestionnaire: Questionnaire private lateinit var questionnaireConfig: QuestionnaireConfig private lateinit var questionnaireViewModel: QuestionnaireViewModel @@ -198,7 +205,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { spyk( QuestionnaireViewModel( defaultRepository = defaultRepository, - dispatcherProvider = defaultRepository.dispatcherProvider, + dispatcherProvider = dispatcherProvider, fhirCarePlanGenerator = fhirCarePlanGenerator, resourceDataRulesExecutor = resourceDataRulesExecutor, transformSupportServices = mockk(), @@ -637,7 +644,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { val questionnaireViewModelInstance = QuestionnaireViewModel( defaultRepository = defaultRepository, - dispatcherProvider = defaultRepository.dispatcherProvider, + dispatcherProvider = dispatcherProvider, fhirCarePlanGenerator = fhirCarePlanGenerator, resourceDataRulesExecutor = resourceDataRulesExecutor, transformSupportServices = mockk(), @@ -1056,15 +1063,38 @@ class QuestionnaireViewModelTest : RobolectricTest() { coEvery { fhirOperator.evaluateLibrary(any(), any(), any(), any()) } returns Parameters() - questionnaireViewModel.executeCql(patient, bundle, questionnaire) + val cqlLibrary = + Library().apply { + id = "Library/123" + url = "http://smartreg.org/Library/123" + name = "123" + version = "1.0.0" + status = Enumerations.PublicationStatus.ACTIVE + addContent( + Attachment().apply { + contentType = "text/cql" + data = "someCQL".toByteArray() + }, + ) + } + + knowledgeManager.install( + File.createTempFile(cqlLibrary.name, ".json").apply { + this.writeText(cqlLibrary.encodeResourceToString()) + }, + ) + fhirEngine.create(patient) + questionnaireViewModel.executeCql(patient, bundle, questionnaire) + coVerify { fhirOperator.evaluateLibrary( "http://smartreg.org/Library/123", patient.asReference().reference, null, - expressions = setOf(), + bundle, + null, ) } } @@ -1259,7 +1289,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { encounterId = null, ) Assert.assertNotNull(latestQuestionnaireResponse) - Assert.assertEquals("qr1", latestQuestionnaireResponse?.id) + Assert.assertEquals("QuestionnaireResponse/qr1", latestQuestionnaireResponse?.id) } @Test @@ -1793,7 +1823,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { val questionnaireViewModelInstance = QuestionnaireViewModel( defaultRepository = defaultRepository, - dispatcherProvider = defaultRepository.dispatcherProvider, + dispatcherProvider = dispatcherProvider, fhirCarePlanGenerator = fhirCarePlanGenerator, resourceDataRulesExecutor = resourceDataRulesExecutor, transformSupportServices = mockk(), @@ -1855,7 +1885,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { val questionnaireViewModelInstance = QuestionnaireViewModel( defaultRepository = defaultRepository, - dispatcherProvider = defaultRepository.dispatcherProvider, + dispatcherProvider = dispatcherProvider, fhirCarePlanGenerator = fhirCarePlanGenerator, resourceDataRulesExecutor = resourceDataRulesExecutor, transformSupportServices = mockk(), @@ -1930,7 +1960,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { val questionnaireViewModelInstance = QuestionnaireViewModel( defaultRepository = defaultRepository, - dispatcherProvider = defaultRepository.dispatcherProvider, + dispatcherProvider = dispatcherProvider, fhirCarePlanGenerator = fhirCarePlanGenerator, resourceDataRulesExecutor = resourceDataRulesExecutor, transformSupportServices = mockk(), diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModelTest.kt index 5cb739577d..5949ebf6e7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/report/measure/MeasureReportViewModelTest.kt @@ -47,10 +47,12 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding @@ -107,6 +109,9 @@ class MeasureReportViewModelTest : RobolectricTest() { @Inject lateinit var resourceDataRulesExecutor: ResourceDataRulesExecutor + @OptIn(ExperimentalCoroutinesApi::class) + private val unconfinedTestDispatcher = UnconfinedTestDispatcher() + @Inject lateinit var fhirEngine: FhirEngine private val measureReportRepository: MeasureReportRepository = mockk() private val fhirOperator: FhirOperator = mockk() @@ -146,6 +151,8 @@ class MeasureReportViewModelTest : RobolectricTest() { measureReportRepository = measureReportRepository, ), ) + + every { measureReportViewModel.dispatcherProvider.io() } returns Dispatchers.IO } @Test @@ -180,7 +187,7 @@ class MeasureReportViewModelTest : RobolectricTest() { url = "http://nourl.com", module = "Module1", ) - every { measureReportViewModel.evaluateMeasure(any(), any()) } just runs + coEvery { measureReportViewModel.evaluateMeasure(any(), any()) } just runs measureReportViewModel.reportTypeSelectorUiState.value = ReportTypeSelectorUiState(startDate = "21 Jan, 2022", endDate = "27 Jan, 2022") measureReportViewModel.onEvent( @@ -196,7 +203,7 @@ class MeasureReportViewModelTest : RobolectricTest() { Assert.assertEquals(viewModelConfig.first().id, reportConfiguration.id) Assert.assertEquals(viewModelConfig.first().module, reportConfiguration.module) - verify { + coVerify { measureReportViewModel.evaluateMeasure( navController = navController, practitionerId = "practitioner-id", @@ -298,13 +305,17 @@ class MeasureReportViewModelTest : RobolectricTest() { Assert.assertNotNull(sampleSubjectViewData.family, subjectViewData?.family) } - @Test() + @Test fun testEvaluateMeasureUtilizesPreviouslyGeneratedMeasureReportIfAvailable() = - runTest(timeout = 90.seconds) { - val subject = Group().apply { id = "groupId" } + runTest(timeout = 90.seconds, context = unconfinedTestDispatcher) { + val subject = + Group().apply { + id = "groupId" + name = "Test Group" + } val testMeasureReport = MeasureReport().apply { - id = "measureId" + id = "MeasureReport/measureId" measure = "http://nourl.com" type = MeasureReportType.INDIVIDUAL this.subject = subject.asReference() @@ -318,7 +329,7 @@ class MeasureReportViewModelTest : RobolectricTest() { val reportConfiguration = ReportConfiguration( - id = "measureId", + id = "ReportConfiguration/measureId", title = "Measure 1", description = "Measure report for testing", url = "http://nourl.com", @@ -337,16 +348,37 @@ class MeasureReportViewModelTest : RobolectricTest() { ) } returns listOf(testMeasureReport) + coEvery { measureReportRepository.fetchSubjects(any(ReportConfiguration::class)) } returns + listOf(subject.asReference().toString()) + measureReportViewModel.reportTypeSelectorUiState.value = ReportTypeSelectorUiState(startDate = "21 Jan, 2022", endDate = "27 Jan, 2022") measureReportViewModel.reportConfigurations.add(reportConfiguration) measureReportViewModel.evaluateMeasure(navController, null) + val measureReportListSlot = slot>() + coVerify { - measureReportViewModel.formatPopulationMeasureReports(listOf(testMeasureReport), any()) + measureReportViewModel.formatPopulationMeasureReports(capture(measureReportListSlot), any()) } + assertEquals(testMeasureReport.id, measureReportListSlot.captured.first().id) + assertEquals(testMeasureReport.measure, measureReportListSlot.captured.first().measure) + assertEquals(testMeasureReport.type, measureReportListSlot.captured.first().type) + assertEquals( + testMeasureReport.subject.reference, + measureReportListSlot.captured.first().subject.reference, + ) + assertEquals( + testMeasureReport.period.start.toString(), + measureReportListSlot.captured.first().period.start.toString(), + ) + assertEquals( + testMeasureReport.period.end.toString(), + measureReportListSlot.captured.first().period.end.toString(), + ) + coVerify(exactly = 0) { measureReportRepository.evaluatePopulationMeasure(any(), any(), any(), any(), any(), any()) } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt index f0ae3ae24c..df1ca86bd5 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt @@ -797,6 +797,7 @@ class ConfigExtensionsKtTest : RobolectricTest() { Assert.assertTrue(!decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) } + @Test fun testExceptionCaughtOnDecodingBitmap() = runTest { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties val listViewProperties = cardViewProperties.content[0] as ListProperties