From ddbcf13d30af5d0b79d8b72bbecca42e058fcecf Mon Sep 17 00:00:00 2001 From: Raazia Tariq <89384079+RaaziaTarique@users.noreply.github.com> Date: Wed, 24 Aug 2022 15:48:59 +0200 Subject: [PATCH] Practitioner Details implementation (#890) * Refactor Practitioner Details implementation (#1520) * Refactor SharedPreference keys * Create custom JSON parser * Refactor SharedPreferencesHelper * Implement Practitioner Details * Add fhir-common-utils dependency * Returns null when Organization is not found * Add TODO tracker for multi Organization * Migrate practitioner endpoint url as a String extension Co-authored-by: Fikri Milano Co-authored-by: maimoona.kausar --- CHANGELOG.md | 1 + android/engine/build.gradle | 1 + .../assets/sample_practitioner_payload.json | 259 +++++++++++++++++ .../engine/auth/AccountAuthenticator.kt | 18 +- .../configuration/ConfigurationRegistry.kt | 4 +- .../engine/configuration/app/ConfigService.kt | 13 +- .../data/remote/model/response/UserInfo.kt | 4 + .../fhircore/engine/di/NetworkModule.kt | 3 +- .../fhircore/engine/sync/SyncBroadcaster.kt | 23 +- .../ui/appsetting/AppSettingActivity.kt | 8 +- .../ui/base/BaseMultiLanguageActivity.kt | 5 +- .../engine/ui/login/LoginViewModel.kt | 115 ++++---- .../fhircore/engine/ui/pin/PinViewModel.kt | 6 +- .../questionnaire/QuestionnaireViewModel.kt | 35 +-- .../ui/userprofile/UserProfileViewModel.kt | 5 +- ...refConstants.kt => SharedPreferenceKey.kt} | 16 +- .../engine/util/SharedPreferencesHelper.kt | 38 ++- .../util/extension/ApplicationExtension.kt | 5 +- .../util/extension/FhirContextExtension.kt | 36 +++ .../util/extension/ResourceExtension.kt | 2 +- .../engine/util/extension/StringExtensions.kt | 6 + .../assets/sample_practitioner_payload.json | 259 +++++++++++++++++ .../fhircore/engine/app/ConfigServiceTest.kt | 15 +- .../engine/auth/AccountAuthenticatorTest.kt | 15 +- .../engine/sync/SyncBroadcasterTest.kt | 3 +- .../ui/appsetting/AppSettingActivityTest.kt | 30 +- .../engine/ui/login/LoginActivityTest.kt | 6 +- .../engine/ui/login/LoginViewModelTest.kt | 267 ++++++++++++++---- .../QuestionnaireActivityTest.kt | 3 + .../QuestionnaireViewModelTest.kt | 45 ++- .../userprofile/UserProfileViewModelTest.kt | 9 +- .../util/SharedPreferencesHelperTest.kt | 43 +-- .../extension/ApplicationExtensionTest.kt | 3 +- .../util/extension/StringExtensionTest.kt | 31 ++ android/quest/build.gradle | 1 + .../quest/ui/main/AppMainViewModel.kt | 19 +- .../quest/ui/register/RegisterViewModel.kt | 5 +- .../report/measure/MeasureReportViewModel.kt | 15 +- .../fhircore/quest/QuestConfigServiceTest.kt | 14 +- .../quest/ui/main/AppMainViewModelTest.kt | 138 +++++++++ 40 files changed, 1257 insertions(+), 267 deletions(-) create mode 100644 android/engine/src/main/assets/sample_practitioner_payload.json rename android/engine/src/main/java/org/smartregister/fhircore/engine/util/{SharedPrefConstants.kt => SharedPreferenceKey.kt} (67%) create mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirContextExtension.kt create mode 100644 android/engine/src/test/assets/sample_practitioner_payload.json create mode 100644 android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt create mode 100644 android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d954fb044..b8fb8331f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Adds internationalization(MLS) for App configs - Adds register, navigation menus and profile configurations using JSON files +- Implements Practitioner Details ## [0.0.9 Quest, 0.0.10 EIR, 0.0.4 - ANC] - 2022-07-04, 2021-11-24, 2022-04-01 ### Added diff --git a/android/engine/build.gradle b/android/engine/build.gradle index 4392fc051e..661ce6ab87 100644 --- a/android/engine/build.gradle +++ b/android/engine/build.gradle @@ -110,6 +110,7 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.5.0' implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'androidx.security:security-crypto:1.1.0-alpha03' + implementation 'org.smartregister:fhir-common-utils:0.0.2-SNAPSHOT' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1' implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation "androidx.cardview:cardview:1.0.0" diff --git a/android/engine/src/main/assets/sample_practitioner_payload.json b/android/engine/src/main/assets/sample_practitioner_payload.json new file mode 100644 index 0000000000..ba1ec36258 --- /dev/null +++ b/android/engine/src/main/assets/sample_practitioner_payload.json @@ -0,0 +1,259 @@ +{ + "resourceType": "Bundle", + "id": "51d382cf-63e5-4aa8-bcb3-369d2fd300cb", + "meta": { + "lastUpdated": "2022-08-17T07:24:25.608+00:00" + }, + "type": "searchset", + "total": 1, + "link": [ + { + "relation": "self", + "url": "https://fhir.labs.smartregister.org:443/fhir/practitioner-details?keycloak-uuid=34f0d616-afc8-4446-ae87-bb60be4bdbc9" + } + ], + "entry": [ + { + "fullUrl": "https://fhir.labs.smartregister.org:443/fhir/practitioner-details/34f0d616-afc8-4446-ae87-bb60be4bdbc9", + "resource": { + "resourceType": "practitioner-details", + "id": "34f0d616-afc8-4446-ae87-bb60be4bdbc9", + "meta": { + "profile": [ + "http://hl7.org/fhir/profiles/custom-resource" + ] + }, + "KeycloakUserDetails": { + "id": "34f0d616-afc8-4446-ae87-bb60be4bdbc9", + "user-bio": [ + { + "identifier": "b87ff3c2-cbc6-43e6-b753-a9620756f9e4", + "userName": "demo", + "preferredName": "demo", + "familyName": "41887", + "givenName": "Demo", + "emailVerified": "false" + } + ], + "user-roles": [ + "ROLE_realm-admin", + "ROLE_OPENMRS", + "ROLE_EDIT_KEYCLOAK_USERS", + "ROLE_offline_access", + "ROLE_VIEW_KEYCLOAK_USERS", + "ROLE_uma_authorization", + "ROLE_ALL_EVENTS", + "ROLE_PLANS_FOR_USER" + ] + }, + "fhir": { + "id": "136252", + "careteams": [ + { + "resourceType": "CareTeam", + "id": "136253", + "meta": { + "versionId": "1", + "lastUpdated": "2022-07-25T16:41:45.221+00:00", + "source": "#98d1c5ca256d7e8b" + }, + "identifier": [ + { + "use": "official", + "value": "46384571-ad31-4c20-8aa0-266f80ef6582" + } + ], + "status": "active", + "name": "Nala Team", + "participant": [ + { + "member": { + "reference": "Practitioner/136252" + } + } + ] + } + ], + "teams": [ + { + "resourceType": "Organization", + "id": "136254", + "meta": { + "versionId": "2", + "lastUpdated": "2022-07-25T16:42:51.523+00:00", + "source": "#dad854aca8ea9fcd" + }, + "identifier": [ + { + "use": "official", + "value": "e58c9509-8ff2-4664-b805-e9dd5bf0cf8a" + } + ], + "active": true, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/organization-type", + "code": "prov" + } + ] + } + ], + "name": "Nala Team", + "alias": [ + "nala" + ] + } + ], + "locations": [ + { + "resourceType": "Location", + "id": "136256", + "meta": { + "versionId": "1", + "lastUpdated": "2022-07-25T16:44:38.620+00:00", + "source": "#b46640c1adfc6d38" + }, + "identifier": [ + { + "use": "official", + "value": "c3bd4bcc-889e-4e3d-a72b-a0b611b8fb64" + } + ], + "status": "active", + "name": "Nala Location ", + "physicalType": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/location-physical-type", + "code": "jdn", + "display": "Jurisdiction" + } + ] + }, + "partOf": { + "reference": "Location/109211", + "display": "Charlie Clinic" + } + } + ], + "locationHierarchyList": [ + { + "resourceType": "LocationHierarchy", + "id": "Location Resource : 136256", + "meta": { + "profile": [ + "http://hl7.org/fhir/profiles/custom-resource" + ] + }, + "LocationHierarchyTree": { + "locationsHierarchy": { + "listOfNodes": { + "treeNodeId": "Location/136256", + "treeNode": [ + { + "nodeId": "Location/136256", + "label": "Nala Location ", + "node": { + "resourceType": "Location", + "id": "136256", + "meta": { + "versionId": "1", + "lastUpdated": "2022-07-25T16:44:38.620+00:00", + "source": "#b46640c1adfc6d38" + }, + "identifier": [ + { + "use": "official", + "value": "c3bd4bcc-889e-4e3d-a72b-a0b611b8fb64" + } + ], + "status": "active", + "name": "Nala Location ", + "physicalType": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/location-physical-type", + "code": "jdn", + "display": "Jurisdiction" + } + ] + }, + "partOf": { + "reference": "Location/109211", + "display": "Charlie Clinic" + } + }, + "parent": "Location/109211", + "children": [ + { + "childId": "Location/137151", + "treeNode": { + "nodeId": "Location/137151", + "label": "Test Village", + "node": { + "resourceType": "Location", + "id": "137151", + "meta": { + "versionId": "1", + "lastUpdated": "2022-08-04T11:41:32.683+00:00", + "source": "#1bb63352eef7cefc" + }, + "identifier": [ + { + "use": "official", + "value": "f7f4c729-fd05-40aa-bdc3-fccd74589264" + } + ], + "status": "active", + "name": "Test Village", + "description": "Test", + "physicalType": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/location-physical-type", + "code": "jdn", + "display": "Jurisdiction" + } + ] + }, + "partOf": { + "reference": "Location/136256", + "display": "Nala Location " + } + }, + "parent": "Location/136256" + } + } + ] + } + ] + }, + "parentChildren": [ + { + "identifier": "Location/109211", + "childIdentifiers": [ + "Location/136256" + ] + }, + { + "identifier": "Location/136256", + "childIdentifiers": [ + "Location/137151" + ] + } + ] + } + }, + "locationId": "136256" + } + ], + "practitionerId": [ + "136252" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/auth/AccountAuthenticator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/auth/AccountAuthenticator.kt index df9fe28269..fc1fd400e2 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/auth/AccountAuthenticator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/auth/AccountAuthenticator.kt @@ -29,6 +29,7 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import androidx.core.os.bundleOf +import ca.uhn.fhir.parser.IParser import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Locale import javax.inject.Inject @@ -37,11 +38,12 @@ import okhttp3.ResponseBody import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.auth.OAuthService +import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse import org.smartregister.fhircore.engine.ui.login.LoginActivity -import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SecureSharedPreference import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.fhircore.engine.util.extension.practitionerEndpointUrl import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.engine.util.toSha1 import retrofit2.Call @@ -56,11 +58,12 @@ constructor( @ApplicationContext val context: Context, val accountManager: AccountManager, val oAuthService: OAuthService, + val fhirResourceService: FhirResourceService, + val parser: IParser, val configService: ConfigService, val secureSharedPreference: SecureSharedPreference, val tokenManagerService: TokenManagerService, val sharedPreference: SharedPreferencesHelper, - val dispatcherProvider: DispatcherProvider ) : AbstractAccountAuthenticator(context) { override fun addAccount( @@ -183,6 +186,16 @@ constructor( } } + fun getPractitionerDetailsFromAssets(): org.hl7.fhir.r4.model.Bundle { + val jsonPayload = + context.assets.open(PATH_PRACTITIONER_DETAILS_PAYLOAD).bufferedReader().use { it.readText() } + return parser.parseResource(jsonPayload) as org.hl7.fhir.r4.model.Bundle + } + + suspend fun getPractitionerDetails(keycloakUuid: String): org.hl7.fhir.r4.model.Bundle { + return fhirResourceService.getResource(url = keycloakUuid.practitionerEndpointUrl()) + } + @Throws(NetworkErrorException::class) fun fetchToken(username: String, password: CharArray): Call { val data = buildOAuthPayload(PASSWORD) @@ -348,5 +361,6 @@ constructor( const val USERNAME = "username" const val PASSWORD = "password" const val REFRESH_TOKEN = "refresh_token" + const val PATH_PRACTITIONER_DETAILS_PAYLOAD = "sample_practitioner_payload.json" } } 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 a4f91d3f0b..5087584ce3 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 @@ -31,8 +31,8 @@ import org.hl7.fhir.r4.model.Composition import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource -import org.smartregister.fhircore.engine.util.APP_ID_KEY import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.camelCase import org.smartregister.fhircore.engine.util.extension.decodeJson @@ -276,7 +276,7 @@ constructor( // TODO load these type of configs from assets too CoroutineScope(dispatcherProvider.io()).launch { try { - sharedPreferencesHelper.read(APP_ID_KEY, null)?.let { appId: String -> + sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null)?.let { appId: String -> repository.searchCompositionByIdentifier(appId)?.let { composition -> composition .retrieveCompositionSections() diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt index 022ccfcd87..632ee9bb48 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt @@ -34,9 +34,9 @@ import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.SearchParameter import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.task.FhirTaskPlanWorker +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import timber.log.Timber /** An interface that provides the application configurations. */ @@ -82,7 +82,7 @@ interface ConfigService { /** Retrieve registry sync params */ fun loadRegistrySyncParams( configurationRegistry: ConfigurationRegistry, - authenticatedUserInfo: UserInfo?, + paramsMap: Map>?, ): Map> { val pairs = mutableListOf>>() @@ -100,8 +100,13 @@ interface ConfigService { val paramExpression = sp.expression val expressionValue = when (paramName) { - ConfigurationRegistry.ORGANIZATION -> authenticatedUserInfo?.organization - ConfigurationRegistry.PUBLISHER -> authenticatedUserInfo?.questionnairePublisher + // TODO: Does not support multi organization yet, + // https://github.com/opensrp/fhircore/issues/1550 + ConfigurationRegistry.ORGANIZATION -> + paramsMap + ?.get(SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name) + ?.firstOrNull() + ?.substringAfter("/") ConfigurationRegistry.ID -> paramExpression ConfigurationRegistry.COUNT -> appConfig.remoteSyncPageSize.toString() else -> null diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/model/response/UserInfo.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/model/response/UserInfo.kt index e0e57682aa..137cc7956c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/model/response/UserInfo.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/model/response/UserInfo.kt @@ -24,5 +24,9 @@ data class UserInfo( @SerialName("questionnaire_publisher") var questionnairePublisher: String? = null, @SerialName("organization") var organization: String? = null, @SerialName("location") var location: String? = null, + @SerialName("family_name") var familyName: String? = null, + @SerialName("given_name") var givenName: String? = null, + @SerialName("name") var name: String? = null, + @SerialName("preferred_username") var preferredUsername: String? = null, @SerialName("sub") var keycloakUuid: String? = null ) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt index c1a3980ee6..6d5729da82 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt @@ -32,6 +32,7 @@ import org.smartregister.fhircore.engine.data.remote.auth.OAuthService import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirConverterFactory import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService import org.smartregister.fhircore.engine.data.remote.shared.interceptor.OAuthInterceptor +import org.smartregister.fhircore.engine.util.extension.getCustomJsonParser import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -73,7 +74,7 @@ class NetworkModule { .build() .create(OAuthService::class.java) - @Provides fun provideParser(): IParser = FhirContext.forR4Cached().newJsonParser() + @Provides fun provideParser(): IParser = FhirContext.forR4Cached().getCustomJsonParser() @Provides fun provideFhirResourceService( diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index 02b4291225..42a0db66af 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -27,12 +27,10 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ConfigService -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper -import org.smartregister.fhircore.engine.util.USER_INFO_SHARED_PREFERENCE_KEY -import org.smartregister.fhircore.engine.util.extension.decodeJson import timber.log.Timber /** @@ -50,20 +48,23 @@ class SyncBroadcaster( ) { fun runSync() { CoroutineScope(dispatcherProvider.io()).launch { + val paramsMap = + mutableMapOf>().apply { + put( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name, + sharedPreferencesHelper.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name + ) + ?: listOf() + ) + } try { syncJob.run( fhirEngine = fhirEngine, downloadManager = ResourceParamsBasedDownloadWorkManager( syncParams = - configService - .loadRegistrySyncParams( - configurationRegistry, - sharedPreferencesHelper - .read(USER_INFO_SHARED_PREFERENCE_KEY, null) - ?.decodeJson() - ) - .toMap() + configService.loadRegistrySyncParams(configurationRegistry, paramsMap).toMap() ), subscribeTo = sharedSyncStatus, resolver = AcceptLocalConflictResolver diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivity.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivity.kt index e9bc4fb740..1345248e1f 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivity.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivity.kt @@ -34,8 +34,8 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.ui.login.LoginActivity import org.smartregister.fhircore.engine.ui.login.LoginService import org.smartregister.fhircore.engine.ui.theme.AppTheme -import org.smartregister.fhircore.engine.util.APP_ID_KEY import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.showToast @@ -73,7 +73,7 @@ class AppSettingActivity : AppCompatActivity() { configurationRegistry.loadConfigurations(context = appSettingActivity, appId = appId) { loadSuccessful: Boolean -> if (loadSuccessful) { - sharedPreferencesHelper.write(APP_ID_KEY, appId) + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, appId) if (!isLoggedIn) { accountAuthenticator.launchScreen(LoginActivity::class.java) } else { @@ -95,7 +95,7 @@ class AppSettingActivity : AppCompatActivity() { configurationRegistry.loadConfigurations(context = appSettingActivity, appId = appId) { loadSuccessful: Boolean -> if (loadSuccessful) { - sharedPreferencesHelper.write(APP_ID_KEY, appId) + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, appId) accountAuthenticator.launchScreen(LoginActivity::class.java) finish() } else { @@ -130,7 +130,7 @@ class AppSettingActivity : AppCompatActivity() { } } - val lastAppId = sharedPreferencesHelper.read(APP_ID_KEY, null)?.trimEnd() + val lastAppId = sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null)?.trimEnd() lastAppId?.let { with(appSettingViewModel) { onApplicationIdChanged(it) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt index 5667accf51..9658781142 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt @@ -22,6 +22,7 @@ import androidx.appcompat.app.AppCompatActivity import java.lang.UnsupportedOperationException import java.util.Locale import javax.inject.Inject +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.setAppLocale @@ -33,7 +34,7 @@ abstract class BaseMultiLanguageActivity : AppCompatActivity() { inject() super.onCreate(savedInstanceState) val themePref = - sharedPreferencesHelper.read(key = SharedPreferencesHelper.THEME, defaultValue = "")!! + sharedPreferencesHelper.read(key = SharedPreferenceKey.THEME.name, defaultValue = "")!! if (themePref.isNotEmpty()) { val resourceId = this.resources.getIdentifier(themePref, "style", packageName) @@ -45,7 +46,7 @@ abstract class BaseMultiLanguageActivity : AppCompatActivity() { val lang = baseContext .getSharedPreferences(SharedPreferencesHelper.PREFS_NAME, Context.MODE_PRIVATE) - .getString(SharedPreferencesHelper.LANG, Locale.ENGLISH.toLanguageTag()) + .getString(SharedPreferenceKey.LANG.name, Locale.ENGLISH.toLanguageTag()) ?: Locale.ENGLISH.toLanguageTag() baseContext.setAppLocale(lang).run { super.attachBaseContext(baseContext) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/login/LoginViewModel.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/login/LoginViewModel.kt index 2c0b410f64..6230aedd20 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/login/LoginViewModel.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/login/LoginViewModel.kt @@ -25,31 +25,27 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.android.fhir.FhirEngine import dagger.hilt.android.lifecycle.HiltViewModel import java.io.IOException import java.net.UnknownHostException import javax.inject.Inject import kotlinx.coroutines.launch import okhttp3.ResponseBody -import org.hl7.fhir.r4.model.Practitioner -import org.hl7.fhir.r4.model.ResourceType import org.jetbrains.annotations.TestOnly import org.smartregister.fhircore.engine.auth.AccountAuthenticator import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration -import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo import org.smartregister.fhircore.engine.data.remote.shared.ResponseCallback import org.smartregister.fhircore.engine.data.remote.shared.ResponseHandler import org.smartregister.fhircore.engine.util.DispatcherProvider -import org.smartregister.fhircore.engine.util.LOGGED_IN_PRACTITIONER +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper -import org.smartregister.fhircore.engine.util.USER_INFO_SHARED_PREFERENCE_KEY import org.smartregister.fhircore.engine.util.extension.decodeJson -import org.smartregister.fhircore.engine.util.extension.encodeJson -import org.smartregister.fhircore.engine.util.extension.encodeResourceToString +import org.smartregister.model.practitioner.PractitionerDetails import retrofit2.Call import retrofit2.Response import timber.log.Timber @@ -58,11 +54,11 @@ import timber.log.Timber class LoginViewModel @Inject constructor( + val fhirEngine: FhirEngine, + val configurationRegistry: ConfigurationRegistry, val accountAuthenticator: AccountAuthenticator, val dispatcher: DispatcherProvider, - val sharedPreferences: SharedPreferencesHelper, - val fhirResourceDataSource: FhirResourceDataSource, - val configurationRegistry: ConfigurationRegistry + val sharedPreferences: SharedPreferencesHelper ) : ViewModel(), AccountManagerCallback { private val _launchDialPad: MutableLiveData = MutableLiveData(null) @@ -77,17 +73,33 @@ constructor( val responseBodyHandler = object : ResponseHandler { override fun handleResponse(call: Call, response: Response) { - if (response.isSuccessful) { - response.body()?.let { - with(it.string().decodeJson()) { - sharedPreferences.write(USER_INFO_SHARED_PREFERENCE_KEY, this.encodeJson()) - fetchLoggedInPractitioner(this) - } - } - } else { + if (!response.isSuccessful) { handleFailure(call, IOException("Network call failed with $response")) + Timber.i(response.errorBody()?.toString()) + return + } + + val jsonResponseBody = response.body()!!.string() + + viewModelScope.launch(dispatcher.io()) { + kotlin + .runCatching { + val bundle = + accountAuthenticator.getPractitionerDetails( + keycloakUuid = jsonResponseBody.decodeJson().keycloakUuid!! + ) + savePractitionerDetails(bundle) + } + .onSuccess { + _showProgressBar.postValue(false) + _navigateToHome.postValue(true) + } + .onFailure { throwable -> + Timber.e("Error fetching practitioner details", throwable) + handleErrorMessage(throwable) + _showProgressBar.postValue(false) + } } - Timber.i(response.errorBody()?.toString() ?: "No error") } override fun handleFailure(call: Call, throwable: Throwable) { @@ -97,6 +109,40 @@ constructor( } } + suspend fun savePractitionerDetails(bundle: org.hl7.fhir.r4.model.Bundle) { + if (!bundle.hasEntry()) return + + val practitionerDetails = bundle.entry.first().resource as PractitionerDetails + + val careTeams = practitionerDetails.fhirPractitionerDetails.careTeams ?: listOf() + val organizations = practitionerDetails.fhirPractitionerDetails.organizations ?: listOf() + val locations = practitionerDetails.fhirPractitionerDetails.locations ?: listOf() + val locationHierarchies = + practitionerDetails.fhirPractitionerDetails.locationHierarchyList ?: listOf() + + val careTeamIds = fhirEngine.create(*careTeams.toTypedArray()) + val organizationIds = fhirEngine.create(*organizations.toTypedArray()) + val locationIds = fhirEngine.create(*locations.toTypedArray()) + + sharedPreferences.write( + SharedPreferenceKey.PRACTITIONER_DETAILS_USER_DETAIL.name, + practitionerDetails.userDetail + ) + sharedPreferences.write( + SharedPreferenceKey.PRACTITIONER_DETAILS_CARE_TEAM_IDS.name, + careTeamIds + ) + sharedPreferences.write( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name, + organizationIds + ) + sharedPreferences.write(SharedPreferenceKey.PRACTITIONER_DETAILS_LOCATION_IDS.name, locationIds) + sharedPreferences.write( + SharedPreferenceKey.PRACTITIONER_DETAILS_LOCATION_HIERARCHIES.name, + locationHierarchies + ) + } + private val userInfoResponseCallback: ResponseCallback by lazy { object : ResponseCallback(responseBodyHandler) {} } @@ -166,37 +212,6 @@ constructor( configurationRegistry.retrieveConfiguration(ConfigType.Application) } - fun fetchLoggedInPractitioner(userInfo: UserInfo) { - if (!userInfo.keycloakUuid.isNullOrEmpty() && - sharedPreferences.read(LOGGED_IN_PRACTITIONER, null) == null - ) { - viewModelScope.launch(dispatcher.io()) { - try { - fhirResourceDataSource.search( - ResourceType.Practitioner.name, - mapOf(IDENTIFIER to userInfo.keycloakUuid!!) - ) - .run { - if (!this.entry.isNullOrEmpty()) { - sharedPreferences.write( - LOGGED_IN_PRACTITIONER, - (this.entryFirstRep.resource as Practitioner).encodeResourceToString() - ) - } - } - } catch (throwable: Throwable) { - Timber.e("Error fetching practitioner details", throwable) - } finally { - _showProgressBar.postValue(false) - _navigateToHome.postValue(true) - } - } - } else { - _showProgressBar.postValue(false) - _navigateToHome.postValue(true) - } - } - fun attemptLocalLogin(): Boolean { return accountAuthenticator.validLocalCredentials( username.value!!.trim(), diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/pin/PinViewModel.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/pin/PinViewModel.kt index d42143f847..65b219645e 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/pin/PinViewModel.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/pin/PinViewModel.kt @@ -29,9 +29,9 @@ import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.ui.components.PIN_INPUT_MAX_THRESHOLD -import org.smartregister.fhircore.engine.util.APP_ID_KEY import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SecureSharedPreference +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper @HiltViewModel @@ -83,7 +83,7 @@ constructor( val username = secureSharedPreference.retrieveSessionUsername() pinUiState.value = PinUiState( - appId = sharedPreferences.read(APP_ID_KEY, "")!!, + appId = sharedPreferences.read(SharedPreferenceKey.APP_ID.name, "")!!, appName = applicationConfiguration.appTitle, savedPin = secureSharedPreference.retrieveSessionPin() ?: "", isSetupPage = isSetup, @@ -129,7 +129,7 @@ constructor( } fun onMenuSettingClicked() { - sharedPreferences.remove(APP_ID_KEY) + sharedPreferences.remove(SharedPreferenceKey.APP_ID.name) _navigateToSettings.value = true } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt index eca507f19f..190242c79c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt @@ -41,7 +41,6 @@ import org.hl7.fhir.r4.model.Encounter import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.Patient -import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Reference @@ -53,14 +52,12 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.cql.LibraryEvaluator import org.smartregister.fhircore.engine.data.local.DefaultRepository -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo import org.smartregister.fhircore.engine.domain.model.QuestionnaireType import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator import org.smartregister.fhircore.engine.util.AssetUtil import org.smartregister.fhircore.engine.util.DispatcherProvider -import org.smartregister.fhircore.engine.util.LOGGED_IN_PRACTITIONER +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper -import org.smartregister.fhircore.engine.util.USER_INFO_SHARED_PREFERENCE_KEY import org.smartregister.fhircore.engine.util.extension.asReference import org.smartregister.fhircore.engine.util.extension.assertSubject import org.smartregister.fhircore.engine.util.extension.cqfLibraryIds @@ -75,6 +72,7 @@ import org.smartregister.fhircore.engine.util.extension.referenceValue import org.smartregister.fhircore.engine.util.extension.retainMetadata import org.smartregister.fhircore.engine.util.extension.setPropertySafely import org.smartregister.fhircore.engine.util.helper.TransformSupportServices +import org.smartregister.model.practitioner.KeycloakUserDetails import timber.log.Timber @HiltViewModel @@ -103,14 +101,15 @@ constructor( private val jsonParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() - private val authenticatedUserInfo by lazy { - sharedPreferencesHelper.read(USER_INFO_SHARED_PREFERENCE_KEY) + private val authenticatedOrganizationIds by lazy { + sharedPreferencesHelper.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name + ) } - private val loggedInPractitioner by lazy { - sharedPreferencesHelper.read( - key = LOGGED_IN_PRACTITIONER, - decodeFhirResource = true + private val loggedInUserDetail by lazy { + sharedPreferencesHelper.read( + key = SharedPreferenceKey.PRACTITIONER_DETAILS_USER_DETAIL.name ) } @@ -162,18 +161,20 @@ constructor( } fun appendOrganizationInfo(resource: Resource) { - authenticatedUserInfo?.organization?.let { org -> + authenticatedOrganizationIds.let { ids -> val organizationRef = - Reference().apply { reference = "${ResourceType.Organization.name}/$org" } + Reference().apply { reference = "${ResourceType.Organization.name}/${ids?.first()}" } - if (resource is Patient) resource.managingOrganization = organizationRef - else if (resource is Group) resource.managingEntity = organizationRef - else if (resource is Encounter) resource.serviceProvider = organizationRef + when (resource) { + is Patient -> resource.managingOrganization = organizationRef + is Group -> resource.managingEntity = organizationRef + is Encounter -> resource.serviceProvider = organizationRef + } } } fun appendPractitionerInfo(resource: Resource) { - loggedInPractitioner?.id?.let { + loggedInUserDetail?.id?.let { val practitionerRef = Reference().apply { reference = it } if (resource is Patient) resource.generalPractitioner = arrayListOf(practitionerRef) @@ -363,7 +364,7 @@ constructor( questionnaireResponse.subject = when (subjectType) { ResourceType.Organization.name -> - authenticatedUserInfo?.organization?.asReference(ResourceType.Organization) + authenticatedOrganizationIds?.first()?.asReference(ResourceType.Organization) else -> resourceId?.asReference(ResourceType.valueOf(subjectType)) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModel.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModel.kt index 8ecd9a07f5..06ae45ff70 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModel.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModel.kt @@ -26,6 +26,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.domain.model.Language import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.util.SecureSharedPreference +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.fetchLanguages @@ -61,13 +62,13 @@ constructor( fun loadSelectedLanguage(): String = Locale.forLanguageTag( - sharedPreferencesHelper.read(SharedPreferencesHelper.LANG, Locale.ENGLISH.toLanguageTag()) + sharedPreferencesHelper.read(SharedPreferenceKey.LANG.name, Locale.ENGLISH.toLanguageTag()) ?: Locale.ENGLISH.toLanguageTag() ) .displayName fun setLanguage(language: Language) { - sharedPreferencesHelper.write(SharedPreferencesHelper.LANG, language.tag) + sharedPreferencesHelper.write(SharedPreferenceKey.LANG.name, language.tag) this.language.postValue(language) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPrefConstants.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt similarity index 67% rename from android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPrefConstants.kt rename to android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt index dea91da66d..f11403dce9 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPrefConstants.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt @@ -16,7 +16,15 @@ package org.smartregister.fhircore.engine.util -const val LAST_SYNC_TIMESTAMP = "last_sync_timestamp" -const val USER_INFO_SHARED_PREFERENCE_KEY = "user_info" -const val LOGGED_IN_PRACTITIONER = "logged_in_practitioner" -const val APP_ID_KEY = "app_id" +enum class SharedPreferenceKey { + APP_ID, + LAST_SYNC_TIMESTAMP, + LANG, + MEASURE_RESOURCES_LOADED, + PRACTITIONER_DETAILS_USER_DETAIL, + PRACTITIONER_DETAILS_CARE_TEAM_IDS, + PRACTITIONER_DETAILS_ORGANIZATION_IDS, + PRACTITIONER_DETAILS_LOCATION_IDS, + PRACTITIONER_DETAILS_LOCATION_HIERARCHIES, + THEME +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt index 894d45148b..2f50d9d2ac 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt @@ -18,6 +18,7 @@ package org.smartregister.fhircore.engine.util import android.content.Context import android.content.SharedPreferences +import com.google.gson.Gson import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -25,7 +26,12 @@ import org.smartregister.fhircore.engine.util.extension.decodeJson import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString @Singleton -class SharedPreferencesHelper @Inject constructor(@ApplicationContext val context: Context) { +class SharedPreferencesHelper +@Inject +constructor( + @ApplicationContext val context: Context, + val gson: Gson, +) { private var prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -64,18 +70,34 @@ class SharedPreferencesHelper @Inject constructor(@ApplicationContext val contex } } + /** Read any JSON object with type T */ + inline fun read( + key: String, + isFhirResource: Boolean = false, + isSerialized: Boolean = false + ): T? = + if (isFhirResource) { + this.read(key, null)?.decodeResourceFromString() + } else if (isSerialized) { + this.read(key, null)?.decodeJson() + } else { + val json = this.read(key, null) + gson.fromJson(json, T::class.java) + } + + /** Write any object by saving it as JSON */ + fun write(key: String, value: Any?) { + with(prefs.edit()) { + putString(key, gson.toJson(value)) + commit() + } + } + fun remove(key: String) { prefs.edit().remove(key).apply() } - inline fun read(key: String, decodeFhirResource: Boolean = false): T? = - if (decodeFhirResource) this.read(key, null)?.decodeResourceFromString() - else this.read(key, null)?.decodeJson() - companion object { - const val LANG = "shared_pref_lang" - const val THEME = "shared_pref_theme" const val PREFS_NAME = "params" - const val MEASURE_RESOURCES_LOADED = "measure_resources_loaded" } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt index 1d89a50230..20da3bf269 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt @@ -49,6 +49,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.domain.model.Language import org.smartregister.fhircore.engine.sync.SyncBroadcaster +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import timber.log.Timber @@ -79,7 +80,7 @@ suspend fun FhirEngine.loadCqlLibraryBundle( try { val jsonParser = FhirContext.forR4().newJsonParser() val savedResources = - sharedPreferencesHelper.read(SharedPreferencesHelper.MEASURE_RESOURCES_LOADED, "") + sharedPreferencesHelper.read(SharedPreferenceKey.MEASURE_RESOURCES_LOADED.name, "") context.assets.open(resourcesBundlePath, AssetManager.ACCESS_RANDOM).bufferedReader().use { val bundle = jsonParser.parseResource(it) as Bundle @@ -90,7 +91,7 @@ suspend fun FhirEngine.loadCqlLibraryBundle( if (!savedResources!!.contains(resourcesBundlePath)) { create(entry.resource) sharedPreferencesHelper.write( - SharedPreferencesHelper.MEASURE_RESOURCES_LOADED, + SharedPreferenceKey.MEASURE_RESOURCES_LOADED.name, savedResources.plus(",").plus(resourcesBundlePath) ) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirContextExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirContextExtension.kt new file mode 100644 index 0000000000..d65c6128c4 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirContextExtension.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.util.extension + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.parser.IParser +import org.smartregister.model.location.LocationHierarchy +import org.smartregister.model.practitioner.FhirPractitionerDetails +import org.smartregister.model.practitioner.KeycloakUserDetails +import org.smartregister.model.practitioner.PractitionerDetails +import org.smartregister.model.practitioner.UserBioData + +fun FhirContext.getCustomJsonParser(): IParser { + return this.apply { + registerCustomType(PractitionerDetails::class.java) + registerCustomType(FhirPractitionerDetails::class.java) + registerCustomType(LocationHierarchy::class.java) + registerCustomType(KeycloakUserDetails::class.java) + registerCustomType(UserBioData::class.java) + } + .newJsonParser() +} 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 83fd51cab4..b7aad37338 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 @@ -50,7 +50,7 @@ import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import timber.log.Timber -private val fhirR4JsonParser = FhirContext.forR4Cached().newJsonParser() +private val fhirR4JsonParser = FhirContext.forR4Cached().getCustomJsonParser() fun Base?.valueToString(): String { return when { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt index f17784efd9..64ae773d7e 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt @@ -85,3 +85,9 @@ val String.fileExtension /** Function that converts snake_case string to camelCase */ fun String.camelCase(): String = CaseUtils.toCamelCase(this, false, '_') + +/** + * Get the practitioner endpoint url and append the keycloak-uuid. The original String is assumed to + * be a keycloak-uuid. + */ +fun String.practitionerEndpointUrl(): String = "practitioner-details?keycloak-uuid=$this" diff --git a/android/engine/src/test/assets/sample_practitioner_payload.json b/android/engine/src/test/assets/sample_practitioner_payload.json new file mode 100644 index 0000000000..ba1ec36258 --- /dev/null +++ b/android/engine/src/test/assets/sample_practitioner_payload.json @@ -0,0 +1,259 @@ +{ + "resourceType": "Bundle", + "id": "51d382cf-63e5-4aa8-bcb3-369d2fd300cb", + "meta": { + "lastUpdated": "2022-08-17T07:24:25.608+00:00" + }, + "type": "searchset", + "total": 1, + "link": [ + { + "relation": "self", + "url": "https://fhir.labs.smartregister.org:443/fhir/practitioner-details?keycloak-uuid=34f0d616-afc8-4446-ae87-bb60be4bdbc9" + } + ], + "entry": [ + { + "fullUrl": "https://fhir.labs.smartregister.org:443/fhir/practitioner-details/34f0d616-afc8-4446-ae87-bb60be4bdbc9", + "resource": { + "resourceType": "practitioner-details", + "id": "34f0d616-afc8-4446-ae87-bb60be4bdbc9", + "meta": { + "profile": [ + "http://hl7.org/fhir/profiles/custom-resource" + ] + }, + "KeycloakUserDetails": { + "id": "34f0d616-afc8-4446-ae87-bb60be4bdbc9", + "user-bio": [ + { + "identifier": "b87ff3c2-cbc6-43e6-b753-a9620756f9e4", + "userName": "demo", + "preferredName": "demo", + "familyName": "41887", + "givenName": "Demo", + "emailVerified": "false" + } + ], + "user-roles": [ + "ROLE_realm-admin", + "ROLE_OPENMRS", + "ROLE_EDIT_KEYCLOAK_USERS", + "ROLE_offline_access", + "ROLE_VIEW_KEYCLOAK_USERS", + "ROLE_uma_authorization", + "ROLE_ALL_EVENTS", + "ROLE_PLANS_FOR_USER" + ] + }, + "fhir": { + "id": "136252", + "careteams": [ + { + "resourceType": "CareTeam", + "id": "136253", + "meta": { + "versionId": "1", + "lastUpdated": "2022-07-25T16:41:45.221+00:00", + "source": "#98d1c5ca256d7e8b" + }, + "identifier": [ + { + "use": "official", + "value": "46384571-ad31-4c20-8aa0-266f80ef6582" + } + ], + "status": "active", + "name": "Nala Team", + "participant": [ + { + "member": { + "reference": "Practitioner/136252" + } + } + ] + } + ], + "teams": [ + { + "resourceType": "Organization", + "id": "136254", + "meta": { + "versionId": "2", + "lastUpdated": "2022-07-25T16:42:51.523+00:00", + "source": "#dad854aca8ea9fcd" + }, + "identifier": [ + { + "use": "official", + "value": "e58c9509-8ff2-4664-b805-e9dd5bf0cf8a" + } + ], + "active": true, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/organization-type", + "code": "prov" + } + ] + } + ], + "name": "Nala Team", + "alias": [ + "nala" + ] + } + ], + "locations": [ + { + "resourceType": "Location", + "id": "136256", + "meta": { + "versionId": "1", + "lastUpdated": "2022-07-25T16:44:38.620+00:00", + "source": "#b46640c1adfc6d38" + }, + "identifier": [ + { + "use": "official", + "value": "c3bd4bcc-889e-4e3d-a72b-a0b611b8fb64" + } + ], + "status": "active", + "name": "Nala Location ", + "physicalType": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/location-physical-type", + "code": "jdn", + "display": "Jurisdiction" + } + ] + }, + "partOf": { + "reference": "Location/109211", + "display": "Charlie Clinic" + } + } + ], + "locationHierarchyList": [ + { + "resourceType": "LocationHierarchy", + "id": "Location Resource : 136256", + "meta": { + "profile": [ + "http://hl7.org/fhir/profiles/custom-resource" + ] + }, + "LocationHierarchyTree": { + "locationsHierarchy": { + "listOfNodes": { + "treeNodeId": "Location/136256", + "treeNode": [ + { + "nodeId": "Location/136256", + "label": "Nala Location ", + "node": { + "resourceType": "Location", + "id": "136256", + "meta": { + "versionId": "1", + "lastUpdated": "2022-07-25T16:44:38.620+00:00", + "source": "#b46640c1adfc6d38" + }, + "identifier": [ + { + "use": "official", + "value": "c3bd4bcc-889e-4e3d-a72b-a0b611b8fb64" + } + ], + "status": "active", + "name": "Nala Location ", + "physicalType": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/location-physical-type", + "code": "jdn", + "display": "Jurisdiction" + } + ] + }, + "partOf": { + "reference": "Location/109211", + "display": "Charlie Clinic" + } + }, + "parent": "Location/109211", + "children": [ + { + "childId": "Location/137151", + "treeNode": { + "nodeId": "Location/137151", + "label": "Test Village", + "node": { + "resourceType": "Location", + "id": "137151", + "meta": { + "versionId": "1", + "lastUpdated": "2022-08-04T11:41:32.683+00:00", + "source": "#1bb63352eef7cefc" + }, + "identifier": [ + { + "use": "official", + "value": "f7f4c729-fd05-40aa-bdc3-fccd74589264" + } + ], + "status": "active", + "name": "Test Village", + "description": "Test", + "physicalType": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/location-physical-type", + "code": "jdn", + "display": "Jurisdiction" + } + ] + }, + "partOf": { + "reference": "Location/136256", + "display": "Nala Location " + } + }, + "parent": "Location/136256" + } + } + ] + } + ] + }, + "parentChildren": [ + { + "identifier": "Location/109211", + "childIdentifiers": [ + "Location/136256" + ] + }, + { + "identifier": "Location/136256", + "childIdentifiers": [ + "Location/137151" + ] + } + ] + } + }, + "locationId": "136256" + } + ], + "practitionerId": [ + "136252" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/app/ConfigServiceTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/app/ConfigServiceTest.kt index eac3d036ba..32362d13bf 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/app/ConfigServiceTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/app/ConfigServiceTest.kt @@ -35,21 +35,29 @@ import org.robolectric.Shadows.shadowOf import org.robolectric.util.ReflectionHelpers.setStaticField import org.smartregister.fhircore.engine.app.fakes.Faker import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo import org.smartregister.fhircore.engine.robolectric.RobolectricTest import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.task.FhirTaskPlanWorker +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.extension.isIn @HiltAndroidTest class ConfigServiceTest : RobolectricTest() { + val configService = AppConfigService(ApplicationProvider.getApplicationContext()) val configurationRegistry = Faker.buildTestConfigurationRegistry(mockk()) @Test fun testLoadSyncParamsShouldLoadFromConfiguration() { - val syncParam = - configService.loadRegistrySyncParams(configurationRegistry, UserInfo("samplep", "sampleo")) + + val paramsMap = + mutableMapOf>().apply { + put( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name, + listOf("Organization/105") + ) + } + val syncParam = configService.loadRegistrySyncParams(configurationRegistry, paramsMap) Assert.assertTrue(syncParam.isNotEmpty()) @@ -99,7 +107,6 @@ class ConfigServiceTest : RobolectricTest() { } syncParam.keys.filter { it.isIn(ResourceType.Questionnaire) }.forEach { - Assert.assertTrue(syncParam[it]!!.containsKey("publisher")) Assert.assertTrue(syncParam[it]!!.containsKey("_count")) } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/AccountAuthenticatorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/AccountAuthenticatorTest.kt index cfb09c56f9..89e7982577 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/AccountAuthenticatorTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/auth/AccountAuthenticatorTest.kt @@ -101,11 +101,12 @@ class AccountAuthenticatorTest : RobolectricTest() { context = context, accountManager = accountManager, oAuthService = oAuthService, + fhirResourceService = mockk(), + parser = mockk(), configService = configService, secureSharedPreference = secureSharedPreference, tokenManagerService = tokenManagerService, - sharedPreference = sharedPreference, - dispatcherProvider = dispatcherProvider + sharedPreference = sharedPreference ) ) } @@ -218,11 +219,12 @@ class AccountAuthenticatorTest : RobolectricTest() { context = context, accountManager = accountManager, oAuthService = spyk(oAuthService), + fhirResourceService = mockk(), + parser = mockk(), configService = configService, secureSharedPreference = secureSharedPreference, tokenManagerService = tokenManagerService, - sharedPreference = sharedPreference, - dispatcherProvider = dispatcherProvider + sharedPreference = sharedPreference ) ) @@ -272,11 +274,12 @@ class AccountAuthenticatorTest : RobolectricTest() { context = context, accountManager = accountManager, oAuthService = spyk(oAuthService), + fhirResourceService = mockk(), + parser = mockk(), configService = configService, secureSharedPreference = secureSharedPreference, tokenManagerService = tokenManagerService, - sharedPreference = sharedPreference, - dispatcherProvider = dispatcherProvider + sharedPreference = sharedPreference ) ) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/SyncBroadcasterTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/SyncBroadcasterTest.kt index 5f7e9ca47d..8fafe233e6 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/SyncBroadcasterTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/SyncBroadcasterTest.kt @@ -20,6 +20,7 @@ import android.content.Context import com.google.android.fhir.FhirEngine import com.google.android.fhir.sync.State import com.google.android.fhir.sync.SyncJob +import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import org.junit.Before @@ -50,7 +51,7 @@ internal class SyncBroadcasterTest { @Before fun setup() { fhirResourceDataSource = FhirResourceDataSource(fhirResourceService) - sharedPreferencesHelper = SharedPreferencesHelper(context) + sharedPreferencesHelper = SharedPreferencesHelper(context = context, gson = mockk()) syncBroadcaster = SyncBroadcaster( configurationRegistry, diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivityTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivityTest.kt index ffa5551e91..12ff167c89 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivityTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/appsetting/AppSettingActivityTest.kt @@ -35,7 +35,7 @@ import org.smartregister.fhircore.engine.app.fakes.Faker import org.smartregister.fhircore.engine.di.NetworkModule import org.smartregister.fhircore.engine.robolectric.RobolectricTest import org.smartregister.fhircore.engine.rule.CoroutineTestRule -import org.smartregister.fhircore.engine.util.APP_ID_KEY +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper @UninstallModules(NetworkModule::class) @@ -65,17 +65,23 @@ class AppSettingActivityTest : RobolectricTest() { fun testAppSettingActivity_withAppId_hasNotBeenSubmitted() { activityScenarioRule.scenario.recreate() activityScenarioRule.scenario.onActivity { activity -> - Assert.assertEquals(null, activity.sharedPreferencesHelper.read(APP_ID_KEY, null)) + Assert.assertEquals( + null, + activity.sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null) + ) Assert.assertEquals(false, activity.accountAuthenticator.hasActiveSession()) } } @Test fun testAppSettingActivity_withAppId_hasBeenSubmitted_withUser_hasNotLoggedIn() { - sharedPreferencesHelper.write(APP_ID_KEY, "app") + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, "app") activityScenarioRule.scenario.recreate() activityScenarioRule.scenario.onActivity { activity -> - Assert.assertEquals("app", activity.sharedPreferencesHelper.read(APP_ID_KEY, null)) + Assert.assertEquals( + "app", + activity.sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null) + ) Assert.assertEquals(false, activity.accountAuthenticator.hasActiveSession()) } } @@ -83,27 +89,33 @@ class AppSettingActivityTest : RobolectricTest() { @Test @Ignore("Find a way to fake an access token to make hasActiveSession return true") fun testAppSettingActivity_withAppId_hasBeenSubmitted_withUser_hasLoggedIn() { - sharedPreferencesHelper.write(APP_ID_KEY, "app") + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, "app") activityScenarioRule.scenario.recreate() activityScenarioRule.scenario.onActivity { activity -> - Assert.assertEquals("app", activity.sharedPreferencesHelper.read(APP_ID_KEY, null)) + Assert.assertEquals( + "app", + activity.sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null) + ) Assert.assertEquals(true, activity.accountAuthenticator.hasActiveSession()) } } @Test fun testAppSettingActivity_withAppId_hasBeenSubmitted_withUser_hasLoggedIn_withSessionToken_hasExpired() { - sharedPreferencesHelper.write(APP_ID_KEY, "app") + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, "app") activityScenarioRule.scenario.recreate() activityScenarioRule.scenario.onActivity { activity -> - Assert.assertEquals("app", activity.sharedPreferencesHelper.read(APP_ID_KEY, null)) + Assert.assertEquals( + "app", + activity.sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null) + ) Assert.assertEquals(false, activity.accountAuthenticator.hasActiveSession()) } } @Test fun testAppSettingActivity_withConfig_hasBeenLoaded() { - sharedPreferencesHelper.write(APP_ID_KEY, "app/debug") + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, "app/debug") activityScenarioRule.scenario.recreate() activityScenarioRule.scenario.onActivity { activity -> activity.configurationRegistry.configsJsonMap.let { workflows -> diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginActivityTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginActivityTest.kt index 0af908328f..a6fd14cd9b 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginActivityTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginActivityTest.kt @@ -47,7 +47,7 @@ import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceS import org.smartregister.fhircore.engine.robolectric.ActivityRobolectricTest import org.smartregister.fhircore.engine.rule.CoroutineTestRule import org.smartregister.fhircore.engine.ui.pin.PinSetupActivity -import org.smartregister.fhircore.engine.util.APP_ID_KEY +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper @ExperimentalCoroutinesApi @@ -94,10 +94,10 @@ class LoginActivityTest : ActivityRobolectricTest() { loginViewModel = LoginViewModel( + fhirEngine = mockk(), accountAuthenticator = accountAuthenticator, dispatcher = coroutineTestRule.testDispatcherProvider, sharedPreferences = sharedPreferencesHelper, - fhirResourceDataSource = fhirResourceDataSource, configurationRegistry = configurationRegistry ) @@ -105,7 +105,7 @@ class LoginActivityTest : ActivityRobolectricTest() { loginActivity = controller.create().resume().get() loginActivity.configurationRegistry = configurationRegistry - sharedPreferencesHelper.write(APP_ID_KEY, "default") + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, "default") loginService = loginActivity.loginService } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginViewModelTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginViewModelTest.kt index df6ab6f01d..a923f7c35e 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginViewModelTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/login/LoginViewModelTest.kt @@ -20,10 +20,12 @@ import android.accounts.Account import android.app.Application import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider -import com.google.android.fhir.logicalId +import com.google.android.fhir.FhirEngine +import com.google.gson.Gson import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -34,10 +36,12 @@ import java.io.IOException import java.net.UnknownHostException import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.Bundle -import org.hl7.fhir.r4.model.Practitioner -import org.hl7.fhir.r4.model.ResourceType +import org.hl7.fhir.r4.model.CareTeam +import org.hl7.fhir.r4.model.Location +import org.hl7.fhir.r4.model.Organization +import org.hl7.fhir.r4.model.StringType import org.junit.After import org.junit.Assert import org.junit.Before @@ -51,13 +55,18 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo import org.smartregister.fhircore.engine.robolectric.AccountManagerShadow import org.smartregister.fhircore.engine.robolectric.RobolectricTest import org.smartregister.fhircore.engine.rule.CoroutineTestRule -import org.smartregister.fhircore.engine.util.LOGGED_IN_PRACTITIONER +import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.SecureSharedPreference +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.model.location.LocationHierarchy +import org.smartregister.model.practitioner.FhirPractitionerDetails +import org.smartregister.model.practitioner.KeycloakUserDetails +import org.smartregister.model.practitioner.PractitionerDetails +import org.smartregister.model.practitioner.UserBioData import retrofit2.Call import retrofit2.Response @@ -80,6 +89,8 @@ internal class LoginViewModelTest : RobolectricTest() { @Inject lateinit var configurationRegistry: ConfigurationRegistry + @Inject lateinit var gson: Gson + private lateinit var loginViewModel: LoginViewModel private lateinit var accountAuthenticatorSpy: AccountAuthenticator @@ -88,6 +99,8 @@ internal class LoginViewModelTest : RobolectricTest() { private lateinit var fhirResourceDataSource: FhirResourceDataSource + private val application = ApplicationProvider.getApplicationContext() + @Before fun setUp() { hiltRule.inject() @@ -100,10 +113,10 @@ internal class LoginViewModelTest : RobolectricTest() { loginViewModel = LoginViewModel( + fhirEngine = mockk(), accountAuthenticator = accountAuthenticatorSpy, dispatcher = coroutineTestRule.testDispatcherProvider, sharedPreferences = sharedPreferencesHelper, - fhirResourceDataSource = fhirResourceDataSource, configurationRegistry = configurationRegistry ) } @@ -245,74 +258,204 @@ internal class LoginViewModelTest : RobolectricTest() { } @Test - fun testFetchLoggedInPractitionerShouldRetrieveAndSavePractitioner() { - coroutineTestRule.runBlockingTest { - val userInfo = - UserInfo( - questionnairePublisher = "quesP1", - keycloakUuid = "keyck1", - organization = "org", - location = "Nairobi" + fun savePractitionerDetailsWithProperPayload() { + val fhirEngine = mockk() + val configurationRegistry = mockk() + val accountAuthenticator = mockk() + val dispatcher = DefaultDispatcherProvider() + val sharedPreferences = SharedPreferencesHelper(application, gson) + + val viewModel = + LoginViewModel( + fhirEngine = fhirEngine, + configurationRegistry = configurationRegistry, + accountAuthenticator = accountAuthenticator, + dispatcher = dispatcher, + sharedPreferences = sharedPreferences + ) + + val sampleKeycloakUserDetails = + KeycloakUserDetails().apply { + id = "12345" + userBioData = UserBioData().apply { givenName = StringType("John") } + } + val sampleCareTeam = CareTeam().apply { id = "1" } + val sampleOrganization = Organization().apply { id = "12" } + val sampleLocation = Location().apply { id = "123" } + val sampleLocationHierarchy = LocationHierarchy().apply { id = "1234" } + + val samplePractitionerDetails = + PractitionerDetails().apply { + id = "94859" + userDetail = sampleKeycloakUserDetails + fhirPractitionerDetails = + FhirPractitionerDetails().apply { + careTeams = listOf(sampleCareTeam) + organizations = listOf(sampleOrganization) + locations = listOf(sampleLocation) + locationHierarchyList = listOf(sampleLocationHierarchy) + } + } + + val bundle = + Bundle().apply { + entry = listOf(Bundle.BundleEntryComponent().apply { resource = samplePractitionerDetails }) + } + + coEvery { + fhirEngine.create(*samplePractitionerDetails.fhirPractitionerDetails.careTeams.toTypedArray()) + } returns listOf("1") + + coEvery { + fhirEngine.create( + *samplePractitionerDetails.fhirPractitionerDetails.organizations.toTypedArray() + ) + } returns listOf("12") + + coEvery { + fhirEngine.create(*samplePractitionerDetails.fhirPractitionerDetails.locations.toTypedArray()) + } returns listOf("123") + + runBlocking { viewModel.savePractitionerDetails(bundle) } + + Assert.assertEquals( + "John", + sharedPreferences.read( + SharedPreferenceKey.PRACTITIONER_DETAILS_USER_DETAIL.name ) + ?.userBioData + ?.givenName + ?.value + ) - val practitionerId = "12123" + Assert.assertEquals( + 1, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_CARE_TEAM_IDS.name + ) + ?.size + ) - coEvery { resourceService.searchResource(ResourceType.Practitioner.name, any()) } returns - Bundle().apply { - entry.add( - Bundle.BundleEntryComponent().apply { - resource = Practitioner().apply { id = practitionerId } - } - ) - } + Assert.assertEquals( + 1, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name + ) + ?.size + ) - loginViewModel.fetchLoggedInPractitioner(userInfo) + Assert.assertEquals( + 1, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_LOCATION_IDS.name + ) + ?.size + ) - // Shared preference contains practitioner details - val practitioner = - sharedPreferencesHelper.read( - LOGGED_IN_PRACTITIONER, - decodeFhirResource = true + Assert.assertEquals( + 1, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_LOCATION_HIERARCHIES.name ) - Assert.assertNotNull(practitioner) - Assert.assertEquals(practitionerId, practitioner!!.logicalId) - - // Eventually dismisses the progress dialog and navigates home - Assert.assertNotNull(loginViewModel.showProgressBar.value) - Assert.assertFalse(loginViewModel.showProgressBar.value!!) - Assert.assertNotNull(loginViewModel.navigateToHome.value) - Assert.assertTrue(loginViewModel.navigateToHome.value!!) + ?.size + ) + + coVerify { + fhirEngine.create(*samplePractitionerDetails.fhirPractitionerDetails.careTeams.toTypedArray()) + } + + coVerify { + fhirEngine.create( + *samplePractitionerDetails.fhirPractitionerDetails.organizations.toTypedArray() + ) + } + + coVerify { + fhirEngine.create(*samplePractitionerDetails.fhirPractitionerDetails.locations.toTypedArray()) } } + @Test - fun testFetchLoggedInPractitionerWithNullKeycloakUuid() { - coroutineTestRule.runBlockingTest { - val userInfo = - UserInfo( - questionnairePublisher = "quesP1", - keycloakUuid = null, - organization = "org", - location = "Nairobi" + fun savePractitionerDetailsWhenFhirPractitionerDetailsIsNull() { + val fhirEngine = mockk() + val configurationRegistry = mockk() + val accountAuthenticator = mockk() + val dispatcher = DefaultDispatcherProvider() + val sharedPreferences = SharedPreferencesHelper(application, gson) + + val viewModel = + LoginViewModel( + fhirEngine = fhirEngine, + configurationRegistry = configurationRegistry, + accountAuthenticator = accountAuthenticator, + dispatcher = dispatcher, + sharedPreferences = sharedPreferences + ) + + val sampleKeycloakUserDetails = + KeycloakUserDetails().apply { + id = "12345" + userBioData = UserBioData().apply { givenName = StringType("John") } + } + + val samplePractitionerDetails = + PractitionerDetails().apply { + id = "94859" + userDetail = sampleKeycloakUserDetails + fhirPractitionerDetails = FhirPractitionerDetails() + } + + val bundle = + Bundle().apply { + entry = listOf(Bundle.BundleEntryComponent().apply { resource = samplePractitionerDetails }) + } + + coEvery { fhirEngine.create(*emptyArray()) } returns listOf() + + runBlocking { viewModel.savePractitionerDetails(bundle) } + + Assert.assertEquals( + "John", + sharedPreferences.read( + SharedPreferenceKey.PRACTITIONER_DETAILS_USER_DETAIL.name ) + ?.userBioData + ?.givenName + ?.value + ) - val practitionerId = "12123" + Assert.assertEquals( + 0, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_CARE_TEAM_IDS.name + ) + ?.size + ) + + Assert.assertEquals( + 0, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name + ) + ?.size + ) - coEvery { resourceService.searchResource(ResourceType.Practitioner.name, any()) } returns - Bundle().apply { - entry.add( - Bundle.BundleEntryComponent().apply { - resource = Practitioner().apply { id = practitionerId } - } - ) - } + Assert.assertEquals( + 0, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_LOCATION_IDS.name + ) + ?.size + ) - loginViewModel.fetchLoggedInPractitioner(userInfo) + Assert.assertEquals( + 0, + sharedPreferences.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_LOCATION_HIERARCHIES.name + ) + ?.size + ) - // Eventually dismisses the progress dialog and navigates home - Assert.assertNotNull(loginViewModel.showProgressBar.value) - Assert.assertFalse(loginViewModel.showProgressBar.value!!) - Assert.assertNotNull(loginViewModel.navigateToHome.value) - Assert.assertTrue(loginViewModel.navigateToHome.value!!) - } + coVerify(exactly = 3) { fhirEngine.create(*emptyArray()) } } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt index ba2206ae9e..6c800b0d95 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt @@ -49,6 +49,7 @@ import org.hl7.fhir.r4.model.StringType import org.junit.After import org.junit.Assert import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.robolectric.Robolectric @@ -318,6 +319,7 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { } } + @Ignore("Flaky test") @Test fun testOnClickSaveButtonShouldShowSubmitConfirmationAlert() { ReflectionHelpers.setField( @@ -337,6 +339,7 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { ) } + @Ignore("Flaky test") @Test fun testOnClickSaveWithExperimentalButtonShouldShowTestOnlyConfirmationAlert() { ReflectionHelpers.setField( diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt index 4e72610e89..78277f99d2 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -58,7 +58,6 @@ import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.Patient -import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Reference @@ -77,17 +76,13 @@ import org.robolectric.util.ReflectionHelpers import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.cql.LibraryEvaluator import org.smartregister.fhircore.engine.data.local.DefaultRepository -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo import org.smartregister.fhircore.engine.domain.model.QuestionnaireType import org.smartregister.fhircore.engine.robolectric.RobolectricTest import org.smartregister.fhircore.engine.rule.CoroutineTestRule -import org.smartregister.fhircore.engine.util.APP_ID_KEY -import org.smartregister.fhircore.engine.util.LOGGED_IN_PRACTITIONER +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper -import org.smartregister.fhircore.engine.util.USER_INFO_SHARED_PREFERENCE_KEY -import org.smartregister.fhircore.engine.util.extension.encodeJson -import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.retainMetadata +import org.smartregister.model.practitioner.KeycloakUserDetails @HiltAndroidTest class QuestionnaireViewModelTest : RobolectricTest() { @@ -116,16 +111,22 @@ class QuestionnaireViewModelTest : RobolectricTest() { fun setUp() { hiltRule.inject() - every { sharedPreferencesHelper.read(USER_INFO_SHARED_PREFERENCE_KEY, null) } returns - getUserInfo().encodeJson() + every { + sharedPreferencesHelper.read( + key = SharedPreferenceKey.PRACTITIONER_DETAILS_USER_DETAIL.name + ) + } returns getKeycloakUserDetails() - every { sharedPreferencesHelper.read(LOGGED_IN_PRACTITIONER, null) } returns - Practitioner().apply { id = "123" }.encodeResourceToString() + every { + sharedPreferencesHelper.read>( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name + ) + } returns listOf("105") defaultRepo = spyk(DefaultRepository(fhirEngine, coroutineRule.testDispatcherProvider)) val configurationRegistry = mockk() - sharedPreferencesHelper.write(APP_ID_KEY, "appId") + sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, "appId") questionnaireViewModel = spyk( @@ -998,17 +999,11 @@ class QuestionnaireViewModelTest : RobolectricTest() { questionnaireResponse ) - Assert.assertEquals("Organization/1111", questionnaireResponse.subject.reference) + Assert.assertEquals("Organization/105", questionnaireResponse.subject.reference) } - private fun getUserInfo(): UserInfo { - val userInfo = - UserInfo().apply { - questionnairePublisher = "ab" - organization = "1111" - keycloakUuid = "123" - } - return userInfo + private fun getKeycloakUserDetails(): KeycloakUserDetails { + return KeycloakUserDetails().apply { id = "12345" } } private fun samplePatient() = @@ -1023,7 +1018,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { questionnaireViewModel.appendPractitionerInfo(patient) - Assert.assertEquals("Practitioner/123", patient.generalPractitioner[0].reference) + Assert.assertEquals("12345", patient.generalPractitioner[0].reference) } @Test @@ -1031,19 +1026,19 @@ class QuestionnaireViewModelTest : RobolectricTest() { // For patient val patient = samplePatient() questionnaireViewModel.appendOrganizationInfo(patient) - Assert.assertNotNull("Organization/1111", patient.managingOrganization.reference) + Assert.assertNotNull("Organization/105", patient.managingOrganization.reference) // For group val group = Group().apply { id = "123" } questionnaireViewModel.appendOrganizationInfo(group) - Assert.assertEquals("Organization/1111", group.managingEntity.reference) + Assert.assertEquals("Organization/105", group.managingEntity.reference) } @Test fun testAddPractitionerInfoShouldSetIndividualPractitionerReferenceToEncounterResource() { val encounter = Encounter().apply { this.id = "123456" } questionnaireViewModel.appendPractitionerInfo(encounter) - Assert.assertEquals("Practitioner/123", encounter.participant[0].individual.reference) + Assert.assertEquals("12345", encounter.participant[0].individual.reference) } @Test diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModelTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModelTest.kt index 87531eafa2..cb6d8b2b08 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModelTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/userprofile/UserProfileViewModelTest.kt @@ -48,6 +48,7 @@ import org.smartregister.fhircore.engine.robolectric.RobolectricTest import org.smartregister.fhircore.engine.rule.CoroutineTestRule import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.util.SecureSharedPreference +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper @HiltAndroidTest @@ -74,7 +75,7 @@ class UserProfileViewModelTest : RobolectricTest() { private var fhirResourceDataSource: FhirResourceDataSource init { - sharedPreferencesHelper = SharedPreferencesHelper(context) + sharedPreferencesHelper = SharedPreferencesHelper(context = context, gson = mockk()) configService = AppConfigService(context = context) fhirResourceDataSource = spyk(FhirResourceDataSource(resourceService)) syncBroadcaster = @@ -151,10 +152,10 @@ class UserProfileViewModelTest : RobolectricTest() { @Test fun loadSelectedLanguage() { - every { sharedPreferencesHelper.read(SharedPreferencesHelper.LANG, "en") } returns "fr" + every { sharedPreferencesHelper.read(SharedPreferenceKey.LANG.name, "en") } returns "fr" Assert.assertEquals("French", userProfileViewModel.loadSelectedLanguage()) - verify { sharedPreferencesHelper.read(SharedPreferencesHelper.LANG, "en") } + verify { sharedPreferencesHelper.read(SharedPreferenceKey.LANG.name, "en") } } @Test @@ -170,7 +171,7 @@ class UserProfileViewModelTest : RobolectricTest() { Shadows.shadowOf(Looper.getMainLooper()).idle() - verify { sharedPreferencesHelper.write(SharedPreferencesHelper.LANG, "es") } + verify { sharedPreferencesHelper.write(SharedPreferenceKey.LANG.name, "es") } Assert.assertEquals(language, postedValue!!) } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelperTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelperTest.kt index 5e4c4c698b..6de2bb1da7 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelperTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelperTest.kt @@ -19,18 +19,17 @@ package org.smartregister.fhircore.engine.util import android.app.Application import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider -import com.google.android.fhir.logicalId +import com.google.gson.Gson import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import org.hl7.fhir.r4.model.Practitioner +import javax.inject.Inject import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo +import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.robolectric.RobolectricTest -import org.smartregister.fhircore.engine.util.extension.encodeJson -import org.smartregister.fhircore.engine.util.extension.encodeResourceToString +import org.smartregister.model.practitioner.KeycloakUserDetails @HiltAndroidTest internal class SharedPreferencesHelperTest : RobolectricTest() { @@ -43,9 +42,12 @@ internal class SharedPreferencesHelperTest : RobolectricTest() { private lateinit var sharedPreferencesHelper: SharedPreferencesHelper + @Inject lateinit var gson: Gson + @Before fun setUp() { - sharedPreferencesHelper = SharedPreferencesHelper(application) + hiltRule.inject() + sharedPreferencesHelper = SharedPreferencesHelper(context = application, gson = gson) } @Test @@ -82,19 +84,22 @@ internal class SharedPreferencesHelperTest : RobolectricTest() { } @Test - fun testReadObject() { - val practitioner = Practitioner().apply { id = "1234" } - sharedPreferencesHelper.write(LOGGED_IN_PRACTITIONER, practitioner.encodeResourceToString()) - - val readPractitioner = - sharedPreferencesHelper.read(LOGGED_IN_PRACTITIONER, decodeFhirResource = true) - Assert.assertNotNull(readPractitioner!!.logicalId) - Assert.assertEquals(practitioner.logicalId, readPractitioner.logicalId) - - sharedPreferencesHelper.write( - USER_INFO_SHARED_PREFERENCE_KEY, - UserInfo(keycloakUuid = "1244").encodeJson() + fun writeObjectUsingSerialized() { + val questionnaireConfig = QuestionnaireConfig(id = "123", title = "my-questionnaire") + sharedPreferencesHelper.write("object", questionnaireConfig) + Assert.assertEquals( + questionnaireConfig.id, + sharedPreferencesHelper.read("object", isSerialized = true)?.id + ) + } + + @Test + fun writeObjectUsingGson() { + val keycloakUserDetails = KeycloakUserDetails().apply { id = "12345" } + sharedPreferencesHelper.write("object", keycloakUserDetails) + Assert.assertEquals( + keycloakUserDetails.id, + sharedPreferencesHelper.read("object")?.id ) - Assert.assertNotNull(sharedPreferencesHelper.read(USER_INFO_SHARED_PREFERENCE_KEY)) } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtensionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtensionTest.kt index 94b8db1731..a1fd47c9d1 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtensionTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtensionTest.kt @@ -37,6 +37,7 @@ import org.hl7.fhir.r4.model.ResourceType import org.junit.Assert import org.junit.Test import org.smartregister.fhircore.engine.robolectric.RobolectricTest +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper class ApplicationExtensionTest : RobolectricTest() { @@ -125,7 +126,7 @@ class ApplicationExtensionTest : RobolectricTest() { val sharedPreferencesHelper: SharedPreferencesHelper = mockk() val measureResourceBundleUrl = "measure/ANCIND01-bundle.json" - val prefsDataKey = SharedPreferencesHelper.MEASURE_RESOURCES_LOADED + val prefsDataKey = SharedPreferenceKey.MEASURE_RESOURCES_LOADED.name every { sharedPreferencesHelper.read(prefsDataKey, any()) } returns "" every { sharedPreferencesHelper.write(prefsDataKey, any()) } returns Unit coEvery { fhirOperator.loadLib(any()) } returns Unit diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt new file mode 100644 index 0000000000..ff8980f033 --- /dev/null +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.util.extension + +import org.junit.Assert +import org.junit.Test + +class StringExtensionTest { + + @Test + fun practitionerEndpointUrlShouldMatch() { + Assert.assertEquals( + "practitioner-details?keycloak-uuid=my-keycloak-id", + "my-keycloak-id".practitionerEndpointUrl() + ) + } +} diff --git a/android/quest/build.gradle b/android/quest/build.gradle index eebf15ed55..bb8200438b 100644 --- a/android/quest/build.gradle +++ b/android/quest/build.gradle @@ -177,6 +177,7 @@ dependencies { implementation 'androidx.ui:ui-foundation:0.1.0-dev03' implementation deps.lifecycle.viewmodel implementation('org.smartregister:p2p-lib:0.3.0-SNAPSHOT') + implementation 'org.smartregister:fhir-common-utils:0.0.2-SNAPSHOT' //Hilt - Dependency Injection implementation "com.google.dagger:hilt-android:$hiltVersion" diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index 96e079b191..a37ce0fb2b 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -46,10 +46,9 @@ import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.ui.bottomsheet.RegisterBottomSheetFragment import org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireActivity -import org.smartregister.fhircore.engine.util.APP_ID_KEY import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider -import org.smartregister.fhircore.engine.util.LAST_SYNC_TIMESTAMP import org.smartregister.fhircore.engine.util.SecureSharedPreference +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.fetchLanguages import org.smartregister.fhircore.engine.util.extension.launchQuestionnaire @@ -78,7 +77,9 @@ constructor( mutableStateOf( appMainUiStateOf( navigationConfiguration = - NavigationConfiguration(sharedPreferencesHelper.read(APP_ID_KEY) ?: "") + NavigationConfiguration( + sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, "")!! + ) ) ) @@ -111,7 +112,7 @@ constructor( when (event) { AppMainEvent.Logout -> accountAuthenticator.logout() is AppMainEvent.SwitchLanguage -> { - sharedPreferencesHelper.write(SharedPreferencesHelper.LANG, event.language.tag) + sharedPreferencesHelper.write(SharedPreferenceKey.LANG.name, event.language.tag) event.context.run { setAppLocale(event.language.tag) (this as Activity).refresh() @@ -210,7 +211,7 @@ constructor( private fun loadCurrentLanguage() = Locale.forLanguageTag( - sharedPreferencesHelper.read(SharedPreferencesHelper.LANG, Locale.ENGLISH.toLanguageTag()) + sharedPreferencesHelper.read(SharedPreferenceKey.LANG.name, Locale.ENGLISH.toLanguageTag()) ?: Locale.ENGLISH.toLanguageTag() ) .displayName @@ -225,10 +226,14 @@ constructor( return if (parse == null) "" else simpleDateFormat.format(parse) } - fun retrieveLastSyncTimestamp(): String? = sharedPreferencesHelper.read(LAST_SYNC_TIMESTAMP, null) + fun retrieveLastSyncTimestamp(): String? = + sharedPreferencesHelper.read(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null) fun updateLastSyncTimestamp(timestamp: OffsetDateTime) { - sharedPreferencesHelper.write(LAST_SYNC_TIMESTAMP, formatLastSyncTimestamp(timestamp)) + sharedPreferencesHelper.write( + SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, + formatLastSyncTimestamp(timestamp) + ) } companion object { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt index 170786a746..7b0f05b071 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt @@ -39,7 +39,7 @@ import org.smartregister.fhircore.engine.configuration.register.RegisterConfigur import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireActivity -import org.smartregister.fhircore.engine.util.LAST_SYNC_TIMESTAMP +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.launchQuestionnaire import org.smartregister.fhircore.quest.data.register.RegisterPagingSource @@ -167,5 +167,6 @@ constructor( return false } - fun isFirstTimeSync() = sharedPreferencesHelper.read(LAST_SYNC_TIMESTAMP, null).isNullOrEmpty() + fun isFirstTimeSync() = + sharedPreferencesHelper.read(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null).isNullOrEmpty() } 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 7898e254aa..57e64a03e4 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 @@ -43,11 +43,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.MeasureReport import org.hl7.fhir.r4.model.Observation -import org.hl7.fhir.r4.model.Practitioner import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfig import org.smartregister.fhircore.engine.domain.util.PaginationConstant import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider -import org.smartregister.fhircore.engine.util.LOGGED_IN_PRACTITIONER +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.loadCqlLibraryBundle @@ -60,6 +59,7 @@ import org.smartregister.fhircore.quest.ui.report.measure.models.MeasureReportIn import org.smartregister.fhircore.quest.ui.report.measure.models.MeasureReportPopulationResult import org.smartregister.fhircore.quest.ui.shared.models.MeasureReportPatientViewData import org.smartregister.fhircore.quest.util.mappers.MeasureReportPatientViewDataMapper +import org.smartregister.model.practitioner.KeycloakUserDetails import timber.log.Timber @HiltViewModel @@ -102,10 +102,9 @@ constructor( MutableStateFlow(retrieveAncPatients()) } - private val loggedInPractitioner by lazy { - sharedPreferencesHelper.read( - key = LOGGED_IN_PRACTITIONER, - decodeFhirResource = true + private val loggedInUserDetail by lazy { + sharedPreferencesHelper.read( + key = SharedPreferenceKey.PRACTITIONER_DETAILS_USER_DETAIL.name, ) } @@ -212,7 +211,7 @@ constructor( end = endDateFormatted, reportType = SUBJECT, subject = reportTypeSelectorUiState.value.patientViewData!!.logicalId, - practitioner = loggedInPractitioner?.id, + practitioner = loggedInUserDetail?.id, lastReceivedOn = null // Non-null value not supported yet ) } @@ -256,7 +255,7 @@ constructor( end = endDateFormatted, reportType = POPULATION, subject = null, - practitioner = loggedInPractitioner?.id, + practitioner = loggedInUserDetail?.id, lastReceivedOn = null // Non-null value not supported yet ) } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestConfigServiceTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestConfigServiceTest.kt index 04982120b1..fb60410f63 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestConfigServiceTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestConfigServiceTest.kt @@ -33,7 +33,7 @@ import org.junit.Test import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.local.DefaultRepository -import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo +import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.extension.isIn import org.smartregister.fhircore.quest.robolectric.RobolectricTest @@ -65,10 +65,19 @@ class QuestConfigServiceTest : RobolectricTest() { @Test fun testResourceSyncParam_shouldHaveResourceTypes() { + + val paramsMap = + mutableMapOf>().apply { + put( + SharedPreferenceKey.PRACTITIONER_DETAILS_ORGANIZATION_IDS.name, + listOf("Organization/105") + ) + } + val syncParam = configService.loadRegistrySyncParams( configurationRegistry = configurationRegistry, - authenticatedUserInfo = UserInfo("ONA-Systems", "105", "Nairobi") + paramsMap = paramsMap ) Assert.assertTrue(syncParam.isNotEmpty()) @@ -119,7 +128,6 @@ class QuestConfigServiceTest : RobolectricTest() { } syncParam.keys.filter { it.isIn(ResourceType.Questionnaire) }.forEach { - Assert.assertTrue(syncParam[it]!!.containsKey("publisher")) Assert.assertTrue(syncParam[it]!!.containsKey("_count")) } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt new file mode 100644 index 0000000000..2b7ce6318d --- /dev/null +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.quest.ui.main + +import android.app.Activity +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.gson.Gson +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkClass +import io.mockk.verify +import javax.inject.Inject +import kotlinx.coroutines.runBlocking +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.smartregister.fhircore.engine.auth.AccountAuthenticator +import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry +import org.smartregister.fhircore.engine.configuration.app.ConfigService +import org.smartregister.fhircore.engine.data.local.register.RegisterRepository +import org.smartregister.fhircore.engine.domain.model.Language +import org.smartregister.fhircore.engine.sync.SyncBroadcaster +import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider +import org.smartregister.fhircore.engine.util.SecureSharedPreference +import org.smartregister.fhircore.engine.util.SharedPreferenceKey +import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.fhircore.quest.robolectric.RobolectricTest + +@HiltAndroidTest +class AppMainViewModelTest : RobolectricTest() { + + @get:Rule val hiltRule = HiltAndroidRule(this) + + lateinit var accountAuthenticator: AccountAuthenticator + lateinit var syncBroadcaster: SyncBroadcaster + lateinit var secureSharedPreference: SecureSharedPreference + lateinit var sharedPreferencesHelper: SharedPreferencesHelper + @Inject lateinit var configurationRegistry: ConfigurationRegistry + lateinit var configService: ConfigService + lateinit var registerRepository: RegisterRepository + lateinit var dispatcherProvider: DefaultDispatcherProvider + + val application: Context = ApplicationProvider.getApplicationContext() + + @Inject lateinit var gson: Gson + + lateinit var appMainViewModel: AppMainViewModel + + @Before + fun setUp() { + hiltRule.inject() + + accountAuthenticator = mockk(relaxed = true) + syncBroadcaster = mockk(relaxed = true) + secureSharedPreference = mockk() + sharedPreferencesHelper = SharedPreferencesHelper(application, gson) + configService = mockk() + registerRepository = mockk() + dispatcherProvider = DefaultDispatcherProvider() + + appMainViewModel = + AppMainViewModel( + accountAuthenticator, + syncBroadcaster, + secureSharedPreference, + sharedPreferencesHelper, + configurationRegistry, + configService, + registerRepository, + dispatcherProvider + ) + } + + @Test + fun onEventLogout() { + val appMainEvent = AppMainEvent.Logout + + appMainViewModel.onEvent(appMainEvent) + + verify { accountAuthenticator.logout() } + } + + @Test + fun onEventSwitchLanguage() { + val appMainEvent = + AppMainEvent.SwitchLanguage( + Language("en", "English"), + mockkClass(Activity::class, relaxed = true) + ) + + appMainViewModel.onEvent(appMainEvent) + + Assert.assertEquals("en", sharedPreferencesHelper.read(SharedPreferenceKey.LANG.name, "")) + } + + @Test + fun onEventSyncData() { + val appMainEvent = AppMainEvent.SyncData + + every { secureSharedPreference.retrieveSessionUsername() } returns "demo" + + runBlocking { + configurationRegistry.loadConfigurations("app/debug", application) + appMainViewModel.onEvent(appMainEvent) + } + + verify { syncBroadcaster.runSync() } + verify { appMainViewModel.retrieveAppMainUiState() } + } + + @Test + fun onEventRegisterNewClient() { + val context = mockkClass(Activity::class, relaxed = true) + val appMainEvent = AppMainEvent.RegisterNewClient(context, "123") + + runBlocking { appMainViewModel.onEvent(appMainEvent) } + + verify { context.startActivity(any()) } + } +}