From a772d8fd970900340c9d5d2a463e22628c368d9a Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 7 Jan 2026 10:34:39 +0200 Subject: [PATCH 1/4] MS-448 Cleanup ad-hoc tokenization function --- .../EnrolmentRecordRepositoryImpl.kt | 33 +++++-------------- .../EnrolmentRecordRepositoryImplTest.kt | 8 ++--- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt index 0dad1aeea5..dcf24af111 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt @@ -2,7 +2,6 @@ package com.simprints.infra.enrolment.records.repository import androidx.core.content.edit import com.simprints.core.DispatcherIO -import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.tokenization.TokenizationProcessor @@ -63,23 +62,23 @@ internal class EnrolmentRecordRepositoryImpl @Inject constructor( override suspend fun tokenizeExistingRecords(project: Project) { try { val query = EnrolmentRecordQuery(projectId = project.id, hasUntokenizedFields = true) - val tokenizedSubjectsCreateAction = selectEnrolmentRecordLocalDataSource() + val tokenizedRecordsCreateAction = selectEnrolmentRecordLocalDataSource() .load(query) - .mapNotNull { subject -> - if (subject.projectId != project.id) return@mapNotNull null - val moduleId = tokenizeIfNecessary( - value = subject.moduleId, + .mapNotNull { record -> + if (record.projectId != project.id) return@mapNotNull null + val moduleId = tokenizationProcessor.tokenizeIfNecessary( + tokenizableString = record.moduleId, tokenKeyType = TokenKeyType.ModuleId, project = project, ) - val attendantId = tokenizeIfNecessary( - value = subject.attendantId, + val attendantId = tokenizationProcessor.tokenizeIfNecessary( + tokenizableString = record.attendantId, tokenKeyType = TokenKeyType.AttendantId, project = project, ) - return@mapNotNull subject.copy(moduleId = moduleId, attendantId = attendantId) + record.copy(moduleId = moduleId, attendantId = attendantId) }.map(EnrolmentRecordAction::Creation) - selectEnrolmentRecordLocalDataSource().performActions(tokenizedSubjectsCreateAction, project) + selectEnrolmentRecordLocalDataSource().performActions(tokenizedRecordsCreateAction, project) } catch (e: Exception) { when (e) { is RealmUninitialisedException -> Unit @@ -90,20 +89,6 @@ internal class EnrolmentRecordRepositoryImpl @Inject constructor( } } - private fun tokenizeIfNecessary( - value: TokenizableString, - tokenKeyType: TokenKeyType, - project: Project, - ) = when (value) { - is TokenizableString.Tokenized -> value - - is TokenizableString.Raw -> tokenizationProcessor.encrypt( - decrypted = value, - tokenKeyType = tokenKeyType, - project = project, - ) - } - override suspend fun count( query: EnrolmentRecordQuery, dataSource: BiometricDataSource, diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt index 3bd04938bc..b50f7bb876 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt @@ -203,15 +203,15 @@ class EnrolmentRecordRepositoryImplTest { every { project.id } returns projectId coEvery { localDataSource.load(any()) } returns listOf(enrolmentRecord) every { - tokenizationProcessor.encrypt( - decrypted = attendantIdRaw, + tokenizationProcessor.tokenizeIfNecessary( + tokenizableString = attendantIdRaw, tokenKeyType = TokenKeyType.AttendantId, project = project, ) } returns attendantIdTokenized every { - tokenizationProcessor.encrypt( - decrypted = moduleIdRaw, + tokenizationProcessor.tokenizeIfNecessary( + tokenizableString = moduleIdRaw, tokenKeyType = TokenKeyType.ModuleId, project = project, ) From 27ef2ca167e30d9a350db61ff4006927df6bd42f Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 7 Jan 2026 11:11:47 +0200 Subject: [PATCH 2/4] MS-448 Move the ad-hoc tokenization from every project refresh call to the worker --- .../infra/config/sync/ConfigManager.kt | 3 - .../infra/config/sync/ConfigManagerTest.kt | 5 -- .../TokenizeRecordsIfKeysChangedUseCase.kt | 18 +++++ .../worker/ProjectConfigDownSyncWorker.kt | 7 ++ ...TokenizeRecordsIfKeysChangedUseCaseTest.kt | 74 +++++++++++++++++++ .../worker/ProjectConfigDownSyncWorkerTest.kt | 14 ++-- 6 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCase.kt create mode 100644 infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCaseTest.kt diff --git a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt index f3e00c2bcb..24873c6c2b 100644 --- a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt +++ b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt @@ -8,7 +8,6 @@ import com.simprints.infra.config.store.models.PrivacyNoticeResult import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectWithConfig -import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -19,7 +18,6 @@ import javax.inject.Singleton @Singleton class ConfigManager @Inject constructor( private val configRepository: ConfigRepository, - private val enrolmentRecordRepository: EnrolmentRecordRepository, private val configSyncCache: ConfigSyncCache, private val authStore: AuthStore, ) { @@ -29,7 +27,6 @@ class ConfigManager @Inject constructor( isProjectRefreshingFlow.tryEmit(true) try { return configRepository.refreshProject(projectId).also { - enrolmentRecordRepository.tokenizeExistingRecords(it.project) configSyncCache.saveUpdateTime() } } finally { diff --git a/infra/config-sync/src/test/java/com/simprints/infra/config/sync/ConfigManagerTest.kt b/infra/config-sync/src/test/java/com/simprints/infra/config/sync/ConfigManagerTest.kt index d041e68989..b7a477e98e 100644 --- a/infra/config-sync/src/test/java/com/simprints/infra/config/sync/ConfigManagerTest.kt +++ b/infra/config-sync/src/test/java/com/simprints/infra/config/sync/ConfigManagerTest.kt @@ -7,7 +7,6 @@ import com.simprints.infra.config.store.models.DeviceConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectWithConfig -import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,9 +31,6 @@ class ConfigManagerTest { @MockK private lateinit var configRepository: ConfigRepository - @MockK - private lateinit var enrolmentRecordRepository: EnrolmentRecordRepository - @MockK private lateinit var configSyncCache: ConfigSyncCache @@ -58,7 +54,6 @@ class ConfigManagerTest { MockKAnnotations.init(this, relaxed = true) configManager = ConfigManager( configRepository = configRepository, - enrolmentRecordRepository = enrolmentRecordRepository, configSyncCache = configSyncCache, authStore = authStore, ) diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCase.kt b/infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCase.kt new file mode 100644 index 0000000000..148094a02e --- /dev/null +++ b/infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCase.kt @@ -0,0 +1,18 @@ +package com.simprints.infra.sync.config.usecase + +import com.simprints.infra.config.store.models.Project +import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository +import javax.inject.Inject + +class TokenizeRecordsIfKeysChangedUseCase @Inject constructor( + private val enrolmentRecordRepository: EnrolmentRecordRepository, +) { + suspend operator fun invoke( + oldProject: Project?, + newProject: Project, + ) { + if ((oldProject == null || oldProject.tokenizationKeys.isEmpty()) && newProject.tokenizationKeys.isNotEmpty()) { + enrolmentRecordRepository.tokenizeExistingRecords(newProject) + } + } +} diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorker.kt b/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorker.kt index 4337a60654..4cfd2b2f40 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorker.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorker.kt @@ -11,6 +11,7 @@ import com.simprints.infra.enrolment.records.repository.local.migration.RealmToR import com.simprints.infra.sync.config.usecase.HandleProjectStateUseCase import com.simprints.infra.sync.config.usecase.RescheduleWorkersIfConfigChangedUseCase import com.simprints.infra.sync.config.usecase.ResetLocalRecordsIfConfigChangedUseCase +import com.simprints.infra.sync.config.usecase.TokenizeRecordsIfKeysChangedUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher @@ -25,6 +26,7 @@ internal class ProjectConfigDownSyncWorker @AssistedInject constructor( private val handleProjectState: HandleProjectStateUseCase, private val rescheduleWorkersIfConfigChanged: RescheduleWorkersIfConfigChangedUseCase, private val resetLocalRecordsIfConfigChanged: ResetLocalRecordsIfConfigChangedUseCase, + private val tokenizeRecordsIfProjectChanged: TokenizeRecordsIfKeysChangedUseCase, private val realmToRoomMigrationScheduler: RealmToRoomMigrationScheduler, @param:DispatcherBG private val dispatcher: CoroutineDispatcher, ) : SimCoroutineWorker(context, params) { @@ -35,6 +37,7 @@ internal class ProjectConfigDownSyncWorker @AssistedInject constructor( crashlyticsLog("Started") try { val projectId = authStore.signedInProjectId + val oldProject = configManager.getProject() val oldConfig = configManager.getProjectConfiguration() // if the user is not signed in, we shouldn't try again @@ -45,6 +48,10 @@ internal class ProjectConfigDownSyncWorker @AssistedInject constructor( handleProjectState(project.state) resetLocalRecordsIfConfigChanged(oldConfig, config) realmToRoomMigrationScheduler.scheduleMigrationWorkerIfNeeded() + + // Running potential tokenization after potential reset and room migration to avoid unnecessary work + tokenizeRecordsIfProjectChanged(oldProject, project) + rescheduleWorkersIfConfigChanged(oldConfig, config) success() } diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCaseTest.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCaseTest.kt new file mode 100644 index 0000000000..18a57c16b8 --- /dev/null +++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/TokenizeRecordsIfKeysChangedUseCaseTest.kt @@ -0,0 +1,74 @@ +package com.simprints.infra.sync.config.usecase + +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository +import com.simprints.infra.sync.config.testtools.project +import io.mockk.* +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class TokenizeRecordsIfKeysChangedUseCaseTest { + @MockK + private lateinit var enrolmentRecordRepository: EnrolmentRecordRepository + + private lateinit var useCase: TokenizeRecordsIfKeysChangedUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxed = true) + + useCase = TokenizeRecordsIfKeysChangedUseCase( + enrolmentRecordRepository = enrolmentRecordRepository, + ) + } + + @Test + fun `should not reset when project already had tokenization keys`() = runTest { + useCase( + project.copy(tokenizationKeys = mapOf(TokenKeyType.ModuleId to "token")), + project.copy(tokenizationKeys = mapOf(TokenKeyType.ModuleId to "token")), + ) + + coVerify(exactly = 0) { + enrolmentRecordRepository.tokenizeExistingRecords(any()) + } + } + + @Test + fun `should not reset when new project does not have tokenization keys`() = runTest { + useCase( + project.copy(tokenizationKeys = emptyMap()), + project.copy(tokenizationKeys = emptyMap()), + ) + + coVerify(exactly = 0) { + enrolmentRecordRepository.tokenizeExistingRecords(any()) + } + } + + @Test + fun `should reset when old project is not present`() = runTest { + useCase( + null, + project.copy(tokenizationKeys = mapOf(TokenKeyType.ModuleId to "token")), + ) + + coVerify(exactly = 1) { + enrolmentRecordRepository.tokenizeExistingRecords(any()) + } + } + + @Test + fun `should reset when project did not have tokenization keys`() = runTest { + useCase( + project.copy(tokenizationKeys = emptyMap()), + project.copy(tokenizationKeys = mapOf(TokenKeyType.ModuleId to "token")), + ) + + coVerify(exactly = 1) { + enrolmentRecordRepository.tokenizeExistingRecords(any()) + } + } +} diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorkerTest.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorkerTest.kt index d21fbeb997..6c661d5eda 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorkerTest.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/worker/ProjectConfigDownSyncWorkerTest.kt @@ -2,7 +2,7 @@ package com.simprints.infra.sync.config.worker import android.os.PowerManager import androidx.work.ListenableWorker -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.models.ProjectWithConfig import com.simprints.infra.config.sync.ConfigManager @@ -12,13 +12,10 @@ import com.simprints.infra.sync.config.testtools.projectConfiguration import com.simprints.infra.sync.config.usecase.HandleProjectStateUseCase import com.simprints.infra.sync.config.usecase.RescheduleWorkersIfConfigChangedUseCase import com.simprints.infra.sync.config.usecase.ResetLocalRecordsIfConfigChangedUseCase +import com.simprints.infra.sync.config.usecase.TokenizeRecordsIfKeysChangedUseCase import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -43,6 +40,9 @@ class ProjectConfigDownSyncWorkerTest { @MockK private lateinit var resetLocalRecordsIfConfigChangedUseCase: ResetLocalRecordsIfConfigChangedUseCase + @MockK + private lateinit var tokenizeRecordsIfProjectChanged: TokenizeRecordsIfKeysChangedUseCase + @MockK private lateinit var realmToRoomMigrationScheduler: RealmToRoomMigrationScheduler @@ -64,6 +64,7 @@ class ProjectConfigDownSyncWorkerTest { handleProjectState = handleProjectStateUseCase, rescheduleWorkersIfConfigChanged = rescheduleWorkersIfConfigChangedUseCase, resetLocalRecordsIfConfigChanged = resetLocalRecordsIfConfigChangedUseCase, + tokenizeRecordsIfProjectChanged = tokenizeRecordsIfProjectChanged, realmToRoomMigrationScheduler = realmToRoomMigrationScheduler, dispatcher = testCoroutineRule.testCoroutineDispatcher, ) @@ -102,6 +103,7 @@ class ProjectConfigDownSyncWorkerTest { resetLocalRecordsIfConfigChangedUseCase.invoke(any(), any()) realmToRoomMigrationScheduler.scheduleMigrationWorkerIfNeeded() rescheduleWorkersIfConfigChangedUseCase.invoke(any(), any()) + tokenizeRecordsIfProjectChanged.invoke(any(), any()) } } From 90597c4df6220a1177ccfb849539990a06e647d7 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 7 Jan 2026 11:12:30 +0200 Subject: [PATCH 3/4] MS-448 Schedule configuration sync immediately after app update --- infra/sync/src/main/AndroidManifest.xml | 11 +++++++- .../simprints/infra/sync/SyncOrchestrator.kt | 2 ++ .../infra/sync/SyncOrchestratorImpl.kt | 5 +++- .../receivers/SyncConfigScheduleReceiver.kt | 26 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 infra/sync/src/main/java/com/simprints/infra/sync/config/receivers/SyncConfigScheduleReceiver.kt diff --git a/infra/sync/src/main/AndroidManifest.xml b/infra/sync/src/main/AndroidManifest.xml index e100076157..7e5d7bf13b 100644 --- a/infra/sync/src/main/AndroidManifest.xml +++ b/infra/sync/src/main/AndroidManifest.xml @@ -1,4 +1,13 @@ - + + + + + + + + diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt index 3f127229b5..494525dbb4 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt @@ -7,6 +7,8 @@ interface SyncOrchestrator { suspend fun cancelBackgroundWork() + fun startConfigSync() + /** * Trigger project and device configuration sync workers. * Emits value when both sync workers are done. diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt index 77d58f9374..ed6c3a22b9 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt @@ -98,10 +98,13 @@ internal class SyncOrchestratorImpl @Inject constructor( stopEventSync() } - override fun refreshConfiguration(): Flow { + override fun startConfigSync() { workManager.startWorker(SyncConstants.PROJECT_SYNC_WORK_NAME_ONE_TIME) workManager.startWorker(SyncConstants.DEVICE_SYNC_WORK_NAME_ONE_TIME) + } + override fun refreshConfiguration(): Flow { + startConfigSync() return workManager .getWorkInfosFlow( WorkQuery.fromUniqueWorkNames( diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/config/receivers/SyncConfigScheduleReceiver.kt b/infra/sync/src/main/java/com/simprints/infra/sync/config/receivers/SyncConfigScheduleReceiver.kt new file mode 100644 index 0000000000..b3e350444a --- /dev/null +++ b/infra/sync/src/main/java/com/simprints/infra/sync/config/receivers/SyncConfigScheduleReceiver.kt @@ -0,0 +1,26 @@ +package com.simprints.infra.sync.config.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.simprints.infra.sync.SyncOrchestrator +import javax.inject.Inject + +/** + * Updating to a new version might enable features that require specific configuration + * that has not been saved in older versions (e.g. tokenization keys). + * Therefore it makes sense to refresh the configuration ASAP. + */ +class SyncConfigScheduleReceiver : BroadcastReceiver() { + @Inject + lateinit var syncOrchestrator: SyncOrchestrator + + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (Intent.ACTION_MY_PACKAGE_REPLACED == intent.action) { + syncOrchestrator.startConfigSync() + } + } +} From d2cd3e71f77dcb27fcebeb5353ed333e932cea81 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 7 Jan 2026 12:14:16 +0200 Subject: [PATCH 4/4] MS-448 Double check that values in action request are tokenised after login --- .../feature/logincheck/LoginCheckViewModel.kt | 4 +- .../EnsureActionFieldsTokenizedUseCase.kt | 43 ++++++ .../logincheck/LoginCheckViewModelTest.kt | 34 +++-- .../EnsureActionFieldsTokenisedUseCaseTest.kt | 134 ++++++++++++++++++ 4 files changed, 201 insertions(+), 14 deletions(-) create mode 100644 feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenizedUseCase.kt create mode 100644 feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenisedUseCaseTest.kt diff --git a/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt b/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt index 090dfc0d1e..38b649be3f 100644 --- a/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt +++ b/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt @@ -10,6 +10,7 @@ import com.simprints.core.livedata.send import com.simprints.feature.login.LoginError import com.simprints.feature.login.LoginResult import com.simprints.feature.logincheck.usecases.AddAuthorizationEventUseCase +import com.simprints.feature.logincheck.usecases.EnsureActionFieldsTokenizedUseCase import com.simprints.feature.logincheck.usecases.ExtractCrashKeysUseCase import com.simprints.feature.logincheck.usecases.ExtractParametersForAnalyticsUseCase import com.simprints.feature.logincheck.usecases.IsUserSignedInUseCase @@ -52,6 +53,7 @@ class LoginCheckViewModel @Inject internal constructor( private val updateProjectInCurrentSession: UpdateProjectInCurrentSessionUseCase, private val updateStoredUserId: UpdateStoredUserIdUseCase, private val realmToRoomMigrationScheduler: RealmToRoomMigrationScheduler, + private val ensureActionFieldsTokenizedUseCase: EnsureActionFieldsTokenizedUseCase, ) : ViewModel() { private var cachedRequest: ActionRequest? = null private val loginAlreadyTried: AtomicBoolean = AtomicBoolean(false) @@ -138,7 +140,7 @@ class LoginCheckViewModel @Inject internal constructor( null, ProjectState.PROJECT_ENDING -> _showAlert.send(LoginCheckError.PROJECT_ENDING) ProjectState.PROJECT_PAUSED -> _showAlert.send(LoginCheckError.PROJECT_PAUSED) ProjectState.PROJECT_ENDED -> startSignInAttempt(actionRequest) - ProjectState.RUNNING -> proceedWithAction(actionRequest) + ProjectState.RUNNING -> proceedWithAction(ensureActionFieldsTokenizedUseCase(actionRequest)) } } diff --git a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenizedUseCase.kt b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenizedUseCase.kt new file mode 100644 index 0000000000..18fdd88fd3 --- /dev/null +++ b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenizedUseCase.kt @@ -0,0 +1,43 @@ +package com.simprints.feature.logincheck.usecases + +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.config.store.tokenization.TokenizationProcessor +import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.orchestration.data.ActionRequest +import javax.inject.Inject + +class EnsureActionFieldsTokenizedUseCase @Inject constructor( + private val configManager: ConfigManager, + private val tokenizationProcessor: TokenizationProcessor, +) { + suspend operator fun invoke(action: ActionRequest): ActionRequest { + val project = configManager.getProject() ?: return action + + // There is no automatic `.copy()` on the the interfaces, so we have to enumerate all sub-classes separately + return when (action) { + is ActionRequest.EnrolActionRequest -> action.copy( + userId = tokenizationProcessor.tokenizeIfNecessary(action.userId, TokenKeyType.AttendantId, project), + moduleId = tokenizationProcessor.tokenizeIfNecessary(action.moduleId, TokenKeyType.ModuleId, project), + ) + + is ActionRequest.IdentifyActionRequest -> action.copy( + userId = tokenizationProcessor.tokenizeIfNecessary(action.userId, TokenKeyType.AttendantId, project), + moduleId = tokenizationProcessor.tokenizeIfNecessary(action.moduleId, TokenKeyType.ModuleId, project), + ) + + is ActionRequest.VerifyActionRequest -> action.copy( + userId = tokenizationProcessor.tokenizeIfNecessary(action.userId, TokenKeyType.AttendantId, project), + moduleId = tokenizationProcessor.tokenizeIfNecessary(action.moduleId, TokenKeyType.ModuleId, project), + ) + + is ActionRequest.EnrolLastBiometricActionRequest -> action.copy( + userId = tokenizationProcessor.tokenizeIfNecessary(action.userId, TokenKeyType.AttendantId, project), + moduleId = tokenizationProcessor.tokenizeIfNecessary(action.moduleId, TokenKeyType.ModuleId, project), + ) + + is ActionRequest.ConfirmIdentityActionRequest -> action.copy( + userId = tokenizationProcessor.tokenizeIfNecessary(action.userId, TokenKeyType.AttendantId, project), + ) + } + } +} diff --git a/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt b/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt index f7ddc7a49b..a86fb61470 100644 --- a/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt +++ b/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt @@ -7,6 +7,7 @@ import com.simprints.feature.login.LoginError import com.simprints.feature.login.LoginResult import com.simprints.feature.logincheck.usecases.ActionFactory import com.simprints.feature.logincheck.usecases.AddAuthorizationEventUseCase +import com.simprints.feature.logincheck.usecases.EnsureActionFieldsTokenizedUseCase import com.simprints.feature.logincheck.usecases.ExtractCrashKeysUseCase import com.simprints.feature.logincheck.usecases.ExtractParametersForAnalyticsUseCase import com.simprints.feature.logincheck.usecases.IsUserSignedInUseCase @@ -75,6 +76,9 @@ internal class LoginCheckViewModelTest { @MockK lateinit var realmToRoomMigrationScheduler: RealmToRoomMigrationScheduler + @MockK + lateinit var ensureActionFieldsTokenizedUseCase: EnsureActionFieldsTokenizedUseCase + private lateinit var viewModel: LoginCheckViewModel @Before @@ -82,20 +86,23 @@ internal class LoginCheckViewModelTest { MockKAnnotations.init(this, relaxed = true) viewModel = LoginCheckViewModel( - rootMatchers, - reportActionRequestEventsUseCase, - extractParametersForAnalyticsUseCase, - extractCrashKeysUseCase, - addAuthorizationEventUseCase, - isUserSignedInUseCase, - configManager, - startBackgroundSync, - syncOrchestrator, - updateSessionScopePayloadUseCase, - updateProjectStateUseCase, - updateStoredUserIdUseCase, - realmToRoomMigrationScheduler, + rootManager = rootMatchers, + reportActionRequestEvents = reportActionRequestEventsUseCase, + extractParametersForAnalytics = extractParametersForAnalyticsUseCase, + extractParametersForCrashReport = extractCrashKeysUseCase, + addAuthorizationEvent = addAuthorizationEventUseCase, + isUserSignedIn = isUserSignedInUseCase, + configManager = configManager, + startBackgroundSync = startBackgroundSync, + syncOrchestrator = syncOrchestrator, + updateDatabaseCountsInCurrentSession = updateSessionScopePayloadUseCase, + updateProjectInCurrentSession = updateProjectStateUseCase, + updateStoredUserId = updateStoredUserIdUseCase, + realmToRoomMigrationScheduler = realmToRoomMigrationScheduler, + ensureActionFieldsTokenizedUseCase = ensureActionFieldsTokenizedUseCase, ) + + coEvery { ensureActionFieldsTokenizedUseCase.invoke(any()) } answers { firstArg() } } @Test @@ -337,6 +344,7 @@ internal class LoginCheckViewModelTest { addAuthorizationEventUseCase.invoke(any(), eq(true)) extractCrashKeysUseCase.invoke(any()) startBackgroundSync.invoke() + ensureActionFieldsTokenizedUseCase.invoke(any()) } viewModel.proceedWithAction diff --git a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenisedUseCaseTest.kt b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenisedUseCaseTest.kt new file mode 100644 index 0000000000..7e307b5136 --- /dev/null +++ b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/EnsureActionFieldsTokenisedUseCaseTest.kt @@ -0,0 +1,134 @@ +package com.simprints.feature.logincheck.usecases + +import com.google.common.truth.Truth.* +import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.core.domain.tokenization.asTokenizableRaw +import com.simprints.infra.config.store.models.Project +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.config.store.tokenization.TokenizationProcessor +import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.orchestration.data.ActionRequest +import io.mockk.* +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class EnsureActionFieldsTokenisedUseCaseTest { + @MockK + private lateinit var configManager: ConfigManager + + @MockK + private lateinit var tokenizationProcessor: TokenizationProcessor + + @MockK + private lateinit var project: Project + + private lateinit var useCase: EnsureActionFieldsTokenizedUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxed = true) + useCase = EnsureActionFieldsTokenizedUseCase(configManager, tokenizationProcessor) + + every { tokenizationProcessor.tokenizeIfNecessary(any(), any(), any()) } returns "value".asTokenizableEncrypted() + } + + @Test + fun `when project is null, return original action`() = runTest { + coEvery { configManager.getProject() } returns null + + val expected = mockk() + val result = useCase(expected) + + assertThat(result).isSameInstanceAs(expected) + } + + @Test + fun `when invoking with EnrolActionRequest, attempt tokenizing fields`() = runTest { + coEvery { configManager.getProject() } returns project + + val action = mockk { + every { userId } returns "user".asTokenizableRaw() + every { moduleId } returns "module".asTokenizableRaw() + } + every { action.copy(userId = any(), moduleId = any()) } returns action + val result = useCase(action) + + assertThat(result).isInstanceOf(ActionRequest.EnrolActionRequest::class.java) + verify { + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.ModuleId, any()) + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.AttendantId, any()) + } + } + + @Test + fun `when invoking with VerifyActionRequest, attempt tokenizing fields`() = runTest { + coEvery { configManager.getProject() } returns project + + val action = mockk { + every { userId } returns "user".asTokenizableRaw() + every { moduleId } returns "module".asTokenizableRaw() + } + every { action.copy(userId = any(), moduleId = any()) } returns action + val result = useCase(action) + + assertThat(result).isInstanceOf(ActionRequest.VerifyActionRequest::class.java) + verify { + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.ModuleId, any()) + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.AttendantId, any()) + } + } + + @Test + fun `when invoking with IdentifyActionRequest, attempt tokenizing fields`() = runTest { + coEvery { configManager.getProject() } returns project + + val action = mockk { + every { userId } returns "user".asTokenizableRaw() + every { moduleId } returns "module".asTokenizableRaw() + } + every { action.copy(userId = any(), moduleId = any()) } returns action + val result = useCase(action) + + assertThat(result).isInstanceOf(ActionRequest.IdentifyActionRequest::class.java) + verify { + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.ModuleId, any()) + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.AttendantId, any()) + } + } + + @Test + fun `when invoking with ConfirmIdentityActionRequest, attempt tokenizing fields`() = runTest { + coEvery { configManager.getProject() } returns project + + val action = mockk { + every { userId } returns "user".asTokenizableRaw() + } + every { action.copy(userId = any()) } returns action + val result = useCase(action) + + assertThat(result).isInstanceOf(ActionRequest.ConfirmIdentityActionRequest::class.java) + verify { + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.AttendantId, any()) + } + } + + @Test + fun `when invoking with EnrolLastBiometricActionRequest, attempt tokenizing fields`() = runTest { + coEvery { configManager.getProject() } returns project + + val action = mockk { + every { userId } returns "user".asTokenizableRaw() + every { moduleId } returns "module".asTokenizableRaw() + } + every { action.copy(userId = any(), moduleId = any()) } returns action + val result = useCase(action) + + assertThat(result).isInstanceOf(ActionRequest.EnrolLastBiometricActionRequest::class.java) + verify { + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.ModuleId, any()) + tokenizationProcessor.tokenizeIfNecessary(any(), TokenKeyType.AttendantId, any()) + } + } +}