Skip to content

Commit

Permalink
Quest questionnaire sync (#590)
Browse files Browse the repository at this point in the history
* Filter Questionnaires by publisher g6pd (#571)

* SharedPreferencesHelper.read default value null

* Make SharedPreferencesHelper.read default value nullable in line with
  Android API SharedPrefereneces.getString.

(#571)

* Add UserResponse to engine

* Expose keycloak GET /userInfo response as data class to simplify
  property access.

(#571)

* Store User Questionnaire Publisher as Preference

* Store User Questionnaire Publisher in SharedPreferences

(#571)

* Filter synchronized Questionnaires by cached publisher.

(#571)

* Make SharedPreferencesHelper.write value nullable

* Make SharedPreferencesHelper.write value nullable in line with
  Android API SharedPrefereneces.Editor.putString.

(#607)

* Add UserResponse.questionnairePublisher

* Replace UserResponse.realmAccess.roles with
  UserResponse.questionnairePublisher.
* Enable key/value rather than array access therefore enabling a more
  generic solution.

(#607)

* Store User Questionnaire Publisher as Preference

* Store User Questionnaire Publisher in SharedPreferences

(#607)

* Log Quest Questionnaire filtering Change

(#571) (#607)

* Setup engine module test application class

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Add more tests for LoginViewModel

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Run eir:spotlessApply

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* add refresh token

* Fix failing LoginViewModel tests

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

Co-authored-by: Elly Kitoto <junkmailstoelly@gmail.com>
Co-authored-by: Peter Lubell-Doughtie <peter@ona.io>
  • Loading branch information
3 people authored Oct 9, 2021
1 parent 8c5dfd3 commit 70c8a14
Show file tree
Hide file tree
Showing 16 changed files with 664 additions and 119 deletions.
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added class for Measure report evaluation which will be used in ANC application
- ANC | Added Condition resource to sync params list
- Moved Token to secure storage from AccountManager
- QUEST | Patient List, Load Config from server
- QUEST | Added Patient Profile View
- QUEST | Patient Registration Questionnaire
- Expose [custom user attribute](https://www.keycloak.org/docs/latest/server_admin/index.html#_user-attributes) `questionnaire_publisher` available in SharedPreferences with key `USER_QUESTIONNAIRE_PUBLISHER` (#607)
- Quest | Filter [Questionnaires](http://hl7.org/fhir/questionnaire.html) by [publisher](http://hl7.org/fhir/questionnaire-definitions.html#Questionnaire.publisher) using user attribute as per above. (#571)
- Quest | Patient List, Load Config from server
- Quest | Added Patient Profile View
- Quest | Patient Registration Questionnaire

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@ class EirApplication : Application(), ConfigurableApplication {
private lateinit var eirApplication: EirApplication

fun getContext() = eirApplication

fun getSyncJob() = Sync.basicSyncJob(eirApplication)
}

override val syncJob: SyncJob
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class EirApplicationTest : RobolectricTest() {

@Test
fun testSyncJobShouldReturnNonNull() {
Assert.assertNotNull(EirApplication.getSyncJob())
Assert.assertNotNull(EirApplication.getContext().syncJob)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.data.remote.model.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UserResponse(
@SerialName("questionnaire_publisher") val questionnairePublisher: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ import okhttp3.ResponseBody
import org.smartregister.fhircore.engine.auth.AuthenticationService
import org.smartregister.fhircore.engine.configuration.view.LoginViewConfiguration
import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse
import org.smartregister.fhircore.engine.data.remote.model.response.UserResponse
import org.smartregister.fhircore.engine.data.remote.shared.ResponseCallback
import org.smartregister.fhircore.engine.data.remote.shared.ResponseHandler
import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.USER_QUESTIONNAIRE_PUBLISHER_SHARED_PREFERENCE_KEY
import org.smartregister.fhircore.engine.util.extension.decodeJson
import retrofit2.Call
import retrofit2.Response
Expand All @@ -48,12 +50,14 @@ class LoginViewModel(
private val dispatcher: DispatcherProvider = DefaultDispatcherProvider
) : AndroidViewModel(application), AccountManagerCallback<Bundle> {

val sharedPreferences =
SharedPreferencesHelper.init(getApplication<Application>().applicationContext)

val responseBodyHandler =
object : ResponseHandler<ResponseBody> {
override fun handleResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
response.body()?.run {
Timber.i(this.string())
SharedPreferencesHelper.init(application.applicationContext).write("USER", this.string())
storeUserPreferences(this)
_showProgressBar.postValue(false)
}
}
Expand All @@ -65,6 +69,16 @@ class LoginViewModel(
}
}

private fun storeUserPreferences(responseBody: ResponseBody) {
val responseBodyString = responseBody.string()
Timber.d(responseBodyString)
val userResponse = responseBodyString.decodeJson<UserResponse>()
sharedPreferences.write(
USER_QUESTIONNAIRE_PUBLISHER_SHARED_PREFERENCE_KEY,
userResponse.questionnairePublisher
)
}

private val userInfoResponseCallback: ResponseCallback<ResponseBody> by lazy {
object : ResponseCallback<ResponseBody>(responseBodyHandler) {}
}
Expand Down Expand Up @@ -150,13 +164,13 @@ class LoginViewModel(
}

fun onUsernameUpdated(username: String) {
_loginError.value = ""
_username.value = username
_loginError.postValue("")
_username.postValue(username)
}

fun onPasswordUpdated(password: String) {
_loginError.value = ""
_password.value = password
_loginError.postValue("")
_password.postValue(password)
}

override fun run(future: AccountManagerFuture<Bundle>?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@
package org.smartregister.fhircore.engine.util

const val LAST_SYNC_TIMESTAMP = "last_sync_timestamp"
const val USER_SHARED_PREFERENCE_KEY = "USER"
const val USER_QUESTIONNAIRE_PUBLISHER_SHARED_PREFERENCE_KEY = "USER_QUESTIONNAIRE_PUBLISHER"
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ object SharedPreferencesHelper {
return this
}

fun read(key: String, value: String) = prefs.getString(key, value)
/** @see [SharedPreferences.getString] */
fun read(key: String, defaultValue: String?) = prefs.getString(key, defaultValue)

fun write(key: String, value: String) {
/** @see [SharedPreferences.Editor.putString] */
fun write(key: String, value: String?) {
with(prefs.edit()) {
putString(key, value)
commit()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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.impl

import android.app.Application
import androidx.test.core.app.ApplicationProvider
import ca.uhn.fhir.rest.gclient.TokenClientParam
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.SyncDownloadContext
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.dao.SquashedLocalChange
import com.google.android.fhir.search.Search
import com.google.android.fhir.sync.Sync
import com.google.android.fhir.sync.SyncJob
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import java.time.OffsetDateTime
import org.hl7.fhir.r4.context.SimpleWorkerContext
import org.hl7.fhir.r4.model.Identifier
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.robolectric.annotation.Config
import org.smartregister.fhircore.engine.auth.AuthCredentials
import org.smartregister.fhircore.engine.auth.AuthenticationService
import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration
import org.smartregister.fhircore.engine.configuration.app.ConfigurableApplication
import org.smartregister.fhircore.engine.configuration.app.applicationConfigurationOf
import org.smartregister.fhircore.engine.shadow.ShadowNpmPackageProvider
import org.smartregister.fhircore.engine.shadow.activity.ShadowLoginActivity
import org.smartregister.fhircore.engine.util.SecureSharedPreference

@Config(shadows = [ShadowNpmPackageProvider::class])
class FhirApplication : Application(), ConfigurableApplication {

override val syncJob: SyncJob
get() = spyk(Sync.basicSyncJob(ApplicationProvider.getApplicationContext()))

override var applicationConfiguration: ApplicationConfiguration = applicationConfigurationOf()

override val authenticationService: AuthenticationService
get() = spyk(FhirAuthenticationService())

override val fhirEngine: FhirEngine
get() = spyk(FhirEngineImpl())

override val secureSharedPreference: SecureSharedPreference by lazy {
val secureSharedPreferenceSpy =
spyk(SecureSharedPreference(ApplicationProvider.getApplicationContext()))
every { secureSharedPreferenceSpy.retrieveCredentials() } returns
AuthCredentials(
username = "demo",
password = "Amani123",
refreshToken = "",
sessionToken = "same-gibberish-string-as-token"
)
secureSharedPreferenceSpy
}

override val resourceSyncParams: Map<ResourceType, Map<String, String>>
get() = mapOf()

override val workerContextProvider: SimpleWorkerContext
get() = mockk(relaxed = true) { SimpleWorkerContext() }

override fun configureApplication(applicationConfiguration: ApplicationConfiguration) {
this.applicationConfiguration = applicationConfiguration
}

override fun schedulePeriodicSync() {
// Do nothing
}

inner class FhirEngineImpl : FhirEngine {

val mockedResourcesStore = mutableListOf<Resource>()

override suspend fun count(search: Search): Long = 1

override suspend fun getLastSyncTimeStamp(): OffsetDateTime? = OffsetDateTime.now()

override suspend fun <R : Resource> load(clazz: Class<R>, id: String): R {
val existingResource =
mockedResourcesStore.find { it.hasId() && it.id == id }
?: throw ResourceNotFoundException(id)
return existingResource as R
}

override suspend fun <R : Resource> remove(clazz: Class<R>, id: String) {
mockedResourcesStore.removeIf { it.id == id }
}

override suspend fun <R : Resource> save(vararg resource: R) {
mockedResourcesStore.addAll(resource)
}

@Suppress("UNCHECKED_CAST")
override suspend fun <R : Resource> search(search: Search): List<R> =
mockedResourcesStore.filter { search.filter(TokenClientParam(it.id), Identifier()) } as
List<R>

override suspend fun syncDownload(download: suspend (SyncDownloadContext) -> List<Resource>) {
// Do nothing
}

override suspend fun syncUpload(
upload: suspend (List<SquashedLocalChange>) -> List<LocalChangeToken>
) {
// Do nothing
}

override suspend fun <R : Resource> update(resource: R) {
// Replace old resource
mockedResourcesStore.removeIf { it.hasId() && it.id == resource.id }
mockedResourcesStore.add(resource)
}
}

inner class FhirAuthenticationService :
AuthenticationService(ApplicationProvider.getApplicationContext()) {
override fun skipLogin(): Boolean = false

override fun getLoginActivityClass(): Class<*> = ShadowLoginActivity::class.java

override fun getAccountType(): String = "test.account.type"

override fun clientSecret(): String = "test.client.secret"

override fun clientId(): String = "test.client.id"

override fun providerScope(): String = "openid"

override fun getApplicationConfigurations(): ApplicationConfiguration = applicationConfiguration
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ package org.smartregister.fhircore.engine.robolectric
import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.mockk.clearAllMocks
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.AfterClass
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.smartregister.fhircore.engine.impl.FhirApplication
import org.smartregister.fhircore.engine.shadow.ShadowNpmPackageProvider

@RunWith(FhircoreTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1])
@Config(
sdk = [Build.VERSION_CODES.O_MR1],
application = FhirApplication::class,
shadows = [ShadowNpmPackageProvider::class]
)
abstract class RobolectricTest {
/** Get the liveData value by observing but wait for 3 seconds if not ready then stop observing */
@Throws(InterruptedException::class)
Expand All @@ -44,4 +52,12 @@ abstract class RobolectricTest {
latch.await(3, TimeUnit.SECONDS)
return data[0] as T?
}

companion object {
@JvmStatic
@AfterClass
fun resetMocks() {
clearAllMocks()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.rule

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.smartregister.fhircore.engine.util.DispatcherProvider

@ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) :
TestRule, TestCoroutineScope by TestCoroutineScope(testDispatcher) {

val testDispatcherProvider =
object : DispatcherProvider {
override fun default() = testDispatcher
override fun io() = testDispatcher
override fun main() = testDispatcher
override fun unconfined() = testDispatcher
}

override fun apply(base: Statement?, description: Description?) =
object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testDispatcher)
base?.evaluate()
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
}
Loading

0 comments on commit 70c8a14

Please sign in to comment.