Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.simprints.core.tools.utils
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import androidx.core.content.edit
import com.simprints.core.ExcludedFromGeneratedTestCoverageReports
import java.util.Locale

Expand All @@ -15,11 +16,9 @@ object LanguageHelper {

lateinit var prefs: SharedPreferences
var language: String
get() {
return prefs.getString(SHARED_PREFS_LANGUAGE_KEY, SHARED_PREFS_LANGUAGE_DEFAULT)!!
}
get() = prefs.getString(SHARED_PREFS_LANGUAGE_KEY, SHARED_PREFS_LANGUAGE_DEFAULT)!!
set(value) {
prefs.edit().putString(SHARED_PREFS_LANGUAGE_KEY, value).apply()
prefs.edit { putString(SHARED_PREFS_LANGUAGE_KEY, value) }
}

fun init(ctx: Context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.simprints.infra.enrolment.records.repository

import android.content.Context
import androidx.core.content.edit
import com.simprints.core.DispatcherIO
import com.simprints.core.domain.tokenization.TokenizableString
Expand All @@ -16,23 +15,23 @@ import com.simprints.infra.enrolment.records.repository.local.SelectEnrolmentRec
import com.simprints.infra.enrolment.records.repository.local.migration.InsertRecordsInRoomDuringMigrationUseCase
import com.simprints.infra.enrolment.records.repository.remote.EnrolmentRecordRemoteDataSource
import com.simprints.infra.logging.Simber
import dagger.hilt.android.qualifiers.ApplicationContext
import com.simprints.infra.security.SecurityManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import javax.inject.Inject

internal class EnrolmentRecordRepositoryImpl @Inject constructor(
@ApplicationContext context: Context,
private val remoteDataSource: EnrolmentRecordRemoteDataSource,
@CommCareDataSource private val commCareDataSource: IdentityDataSource,
private val tokenizationProcessor: TokenizationProcessor,
private val selectEnrolmentRecordLocalDataSource: SelectEnrolmentRecordLocalDataSourceUseCase,
@DispatcherIO private val dispatcher: CoroutineDispatcher,
@EnrolmentBatchSize private val batchSize: Int,
private val insertRecordsInRoomDuringMigration: InsertRecordsInRoomDuringMigrationUseCase,
securityManager: SecurityManager,
) : EnrolmentRecordRepository {
private val prefs = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE)
private val prefs = securityManager.buildEncryptedSharedPreferences(PREF_FILE_NAME)

companion object {
private const val PREF_FILE_NAME = "UPLOAD_ENROLMENT_RECORDS_PROGRESS"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.simprints.infra.enrolment.records.repository

import android.content.Context
import android.content.SharedPreferences
import com.simprints.core.domain.tokenization.asTokenizableEncrypted
import com.simprints.core.domain.tokenization.asTokenizableRaw
Expand All @@ -17,6 +16,7 @@ import com.simprints.infra.enrolment.records.repository.local.EnrolmentRecordLoc
import com.simprints.infra.enrolment.records.repository.local.SelectEnrolmentRecordLocalDataSourceUseCase
import com.simprints.infra.enrolment.records.repository.local.migration.InsertRecordsInRoomDuringMigrationUseCase
import com.simprints.infra.enrolment.records.repository.remote.EnrolmentRecordRemoteDataSource
import com.simprints.infra.security.SecurityManager
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
Expand Down Expand Up @@ -64,8 +64,8 @@ class EnrolmentRecordRepositoryImplTest {
private val prefs = mockk<SharedPreferences> {
every { edit() } returns prefsEditor
}
private val ctx = mockk<Context> {
every { getSharedPreferences(any(), any()) } returns prefs
private val securityManager = mockk<SecurityManager> {
every { buildEncryptedSharedPreferences(any()) } returns prefs
}
private lateinit var repository: EnrolmentRecordRepositoryImpl
private val project = mockk<Project>()
Expand All @@ -78,14 +78,14 @@ class EnrolmentRecordRepositoryImplTest {
every { prefsEditor.remove(any()) } returns prefsEditor
coEvery { selectEnrolmentRecordLocalDataSource() } returns localDataSource
repository = EnrolmentRecordRepositoryImpl(
context = ctx,
remoteDataSource = remoteDataSource,
selectEnrolmentRecordLocalDataSource = selectEnrolmentRecordLocalDataSource,
commCareDataSource = commCareDataSource,
tokenizationProcessor = tokenizationProcessor,
dispatcher = UnconfinedTestDispatcher(),
batchSize = BATCH_SIZE,
insertRecordsInRoomDuringMigration = insertRecordsDuringMigration,
securityManager = securityManager,
)
}

Expand Down
2 changes: 1 addition & 1 deletion infra/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ android {
dependencies {
implementation(project(":infra:logging"))
implementation(project(":infra:logging-persistent"))
implementation(project(":infra:security"))

debugImplementation(libs.chuck.debug) {
exclude("androidx.lifecycle", "lifecycle-viewmodel-ktx")
Expand Down Expand Up @@ -47,7 +48,6 @@ dependencies {

testImplementation(libs.testing.mockwebserver)
testImplementation(libs.chuck.release)

}

configurations {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ import androidx.annotation.VisibleForTesting
import androidx.core.content.edit
import com.simprints.infra.logging.Simber
import com.simprints.infra.network.BuildConfig
import com.simprints.infra.security.SecurityManager
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class BaseUrlProviderImpl @Inject constructor(
@ApplicationContext context: Context,
@ApplicationContext private val context: Context,
securityManager: SecurityManager,
) : BaseUrlProvider {
companion object {
private const val PREF_FILE_NAME = "b3f0cf9b-4f3f-4c5b-bf85-7b1f44eddd7a"
private const val PREF_MODE = Context.MODE_PRIVATE
private const val LEGACY_PREF_FILE_NAME = "b3f0cf9b-4f3f-4c5b-bf85-7b1f44eddd7a"
private const val SECURE_PREF_FILE_NAME = "97285f18-9742-469e-accf-3ed54def7a7e"
private const val API_BASE_URL_KEY = "ApiBaseUrl"
private const val API_VERSION = "v2"
private const val BASE_URL_SUFFIX = "/androidapi/$API_VERSION/"
Expand All @@ -26,13 +28,27 @@ internal class BaseUrlProviderImpl @Inject constructor(
"https://${BuildConfig.BASE_URL_PREFIX}.simprints-apis.com$BASE_URL_SUFFIX"
}

val prefs: SharedPreferences = context.getSharedPreferences(PREF_FILE_NAME, PREF_MODE)
private val securePrefs = securityManager.buildEncryptedSharedPreferences(SECURE_PREF_FILE_NAME)

override fun getApiBaseUrl(): String = prefs
/**
* Ensures that data has been migrated to secure prefs before accessing it.
*/
private fun getSecurePrefs(): SharedPreferences {
// Data has been migrated to secure prefs.
// TODO Delete after there are no users below 2025.3.0
if (!securePrefs.contains(API_BASE_URL_KEY)) {
val prefs = context.getSharedPreferences(LEGACY_PREF_FILE_NAME, Context.MODE_PRIVATE)
securePrefs.edit(commit = true) { putString(API_BASE_URL_KEY, prefs.getString(API_BASE_URL_KEY, "")) }
prefs.edit(commit = true) { clear() }
}
return securePrefs
}

override fun getApiBaseUrl(): String = getSecurePrefs()
.getString(API_BASE_URL_KEY, DEFAULT_BASE_URL)!!
.also { Simber.d("API base URL is $it") }

override fun getApiBaseUrlPrefix(): String = prefs
override fun getApiBaseUrlPrefix(): String = getSecurePrefs()
.getString(API_BASE_URL_KEY, DEFAULT_BASE_URL)
?.removeSuffix(BASE_URL_SUFFIX)
?.also { Simber.d("API base URL prefix is $it") }!!
Expand All @@ -51,11 +67,11 @@ internal class BaseUrlProviderImpl @Inject constructor(

Simber.d("Setting API base URL to $newValue")

prefs.edit(commit = true) { putString(API_BASE_URL_KEY, newValue) }
getSecurePrefs().edit(commit = true) { putString(API_BASE_URL_KEY, newValue) }
}

override fun resetApiBaseUrl() {
Simber.d("Resetting API base")
prefs.edit(commit = true) { putString(API_BASE_URL_KEY, DEFAULT_BASE_URL) }
getSecurePrefs().edit(commit = true) { putString(API_BASE_URL_KEY, DEFAULT_BASE_URL) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package com.simprints.infra.network.url

import android.content.Context
import android.content.SharedPreferences
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.*
import com.simprints.infra.network.url.BaseUrlProviderImpl.Companion.DEFAULT_BASE_URL
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.verify
import com.simprints.infra.security.SecurityManager
import io.mockk.*
import io.mockk.impl.annotations.*
import io.mockk.impl.annotations.MockK
import org.junit.Before
import org.junit.Test

Expand All @@ -22,27 +22,54 @@ class BaseUrlProviderImplTest {
@RelaxedMockK
lateinit var ctx: Context

@RelaxedMockK
lateinit var sharedPreferences: SharedPreferences
@MockK
private lateinit var securityManager: SecurityManager

@RelaxedMockK
lateinit var editor: SharedPreferences.Editor
@MockK
private lateinit var legacySharedPreferences: SharedPreferences

@MockK
private lateinit var legacyEditor: SharedPreferences.Editor

@MockK
private lateinit var secureSharedPreferences: SharedPreferences

@MockK
private lateinit var secureEditor: SharedPreferences.Editor

private lateinit var baseUrlProviderImpl: BaseUrlProviderImpl

@Before
fun setup() {
MockKAnnotations.init(this)
every { ctx.getSharedPreferences(any(), any()) } returns sharedPreferences
every { sharedPreferences.edit() } returns editor
every { editor.putString(any(), any()) } returns editor
MockKAnnotations.init(this, relaxed = true)
every { ctx.getSharedPreferences(any(), any()) } returns legacySharedPreferences
every { legacySharedPreferences.edit() } returns legacyEditor

baseUrlProviderImpl = BaseUrlProviderImpl(ctx)
every { securityManager.buildEncryptedSharedPreferences(any()) } returns secureSharedPreferences
every { secureSharedPreferences.edit() } returns secureEditor
every { secureEditor.putString(any(), any()) } returns secureEditor

baseUrlProviderImpl = BaseUrlProviderImpl(ctx, securityManager)
}

@Test
fun `should migrate data from legacy prefs to secure prefs`() {
every { legacySharedPreferences.contains(any()) } returns true
every { legacySharedPreferences.getString(any(), any()) } returns "old-value"

val result = baseUrlProviderImpl.getApiBaseUrl()

verify { secureEditor.putString(any(), any()) }
verify(exactly = 1) {
legacyEditor.clear()
legacyEditor.commit()
secureEditor.commit()
}
}

@Test
fun `get api base url should return the actual url`() {
every { sharedPreferences.getString(any(), any()) } returns URL
every { secureSharedPreferences.getString(any(), any()) } returns URL

val url = baseUrlProviderImpl.getApiBaseUrl()

Expand All @@ -51,7 +78,7 @@ class BaseUrlProviderImplTest {

@Test
fun `get api base url prefix should return the actual url`() {
every { sharedPreferences.getString(any(), any()) } returns URL_WITH_SUFFIX
every { secureSharedPreferences.getString(any(), any()) } returns URL_WITH_SUFFIX

val url = baseUrlProviderImpl.getApiBaseUrlPrefix()

Expand All @@ -62,29 +89,29 @@ class BaseUrlProviderImplTest {
fun `set api base url should set the url to the default one when the one passed is null`() {
baseUrlProviderImpl.setApiBaseUrl(null)

verify(exactly = 1) { editor.putString(any(), DEFAULT_BASE_URL) }
verify(exactly = 1) { secureEditor.putString(any(), DEFAULT_BASE_URL) }
}

@Test
fun `set api base url should set the url and adds the the base url`() {
val url = "https://url.com"
baseUrlProviderImpl.setApiBaseUrl(url)

verify(exactly = 1) { editor.putString(any(), "$url$URL_SUFFIX") }
verify(exactly = 1) { secureEditor.putString(any(), "$url$URL_SUFFIX") }
}

@Test
fun `set api base url should set the url and adds the https prefix if missing`() {
val url = "url.com"
baseUrlProviderImpl.setApiBaseUrl(url)

verify(exactly = 1) { editor.putString(any(), "https://$url$URL_SUFFIX") }
verify(exactly = 1) { secureEditor.putString(any(), "https://$url$URL_SUFFIX") }
}

@Test
fun `reset api base url should set the url to the default one`() {
baseUrlProviderImpl.resetApiBaseUrl()

verify(exactly = 1) { editor.putString(any(), DEFAULT_BASE_URL) }
verify(exactly = 1) { secureEditor.putString(any(), DEFAULT_BASE_URL) }
}
}