diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt index 2739768a8e..93906d9de2 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt @@ -26,9 +26,10 @@ import com.simprints.feature.clientapi.models.CommCareConstants import com.simprints.feature.clientapi.models.LibSimprintsConstants import com.simprints.feature.clientapi.models.OdkConstants import com.simprints.feature.clientapi.usecases.GetCurrentSessionIdUseCase -import com.simprints.feature.clientapi.usecases.SessionHasIdentificationCallbackUseCase import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.tokenization.TokenizationProcessor +import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.events.EventRepository import com.simprints.infra.orchestration.data.ActionConstants import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier @@ -37,9 +38,10 @@ import javax.inject.Inject internal class IntentToActionMapper @Inject constructor( private val getCurrentSessionId: GetCurrentSessionIdUseCase, - private val sessionHasIdentificationCallback: SessionHasIdentificationCallbackUseCase, private val tokenizationProcessor: TokenizationProcessor, private val timeHelper: TimeHelper, + private val eventRepository: EventRepository, + private val configManager: ConfigManager, ) { suspend operator fun invoke( action: String, @@ -245,7 +247,7 @@ internal class IntentToActionMapper @Inject constructor( validator = EnrolLastBiometricsValidator( extractor = extractor, currentSessionId = getCurrentSessionId(), - sessionHasIdentificationCallback = sessionHasIdentificationCallback(extractor.getSessionId()), + eventRepository = eventRepository, ), ) @@ -261,7 +263,8 @@ internal class IntentToActionMapper @Inject constructor( validator = ConfirmIdentityValidator( extractor = extractor, currentSessionId = getCurrentSessionId(), - sessionHasIdentificationCallback = sessionHasIdentificationCallback(extractor.getSessionId()), + eventRepository = eventRepository, + configManager = configManager, ), ) } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/ActionRequestBuilder.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/ActionRequestBuilder.kt index ba23672f63..53d73f2f0a 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/ActionRequestBuilder.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/ActionRequestBuilder.kt @@ -29,7 +29,7 @@ internal abstract class ActionRequestBuilder( value } - fun build(): ActionRequest { + suspend fun build(): ActionRequest { validator.validate() return buildAction().run(::encryptIfNecessary) } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidator.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidator.kt index 8ef1991e1f..e2b5dd1f1c 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidator.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidator.kt @@ -3,18 +3,25 @@ package com.simprints.feature.clientapi.mappers.request.validators import com.simprints.feature.clientapi.exceptions.InvalidRequestException import com.simprints.feature.clientapi.mappers.request.extractors.ConfirmIdentityRequestExtractor import com.simprints.feature.clientapi.models.ClientApiError +import com.simprints.infra.config.store.models.experimental +import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SESSION import com.simprints.infra.logging.Simber internal class ConfirmIdentityValidator( private val extractor: ConfirmIdentityRequestExtractor, private val currentSessionId: String, - private val sessionHasIdentificationCallback: Boolean, + private val eventRepository: EventRepository, + private val configManager: ConfigManager, ) : RequestActionValidator(extractor) { - override fun validate() { + private var identificationEvent: IdentificationCallbackEvent? = null + + override suspend fun validate() { validateProjectId() validateSessionId(extractor.getSessionId()) - validateSessionEvents() + validateSessionEvents(extractor.getSessionId()) validateSelectedGuid(extractor.getSelectedGuid()) } @@ -28,8 +35,13 @@ internal class ConfirmIdentityValidator( } } - private fun validateSessionEvents() { - if (!sessionHasIdentificationCallback) { + private suspend fun validateSessionEvents(sessionId: String) { + identificationEvent = eventRepository + .getEventsFromScope(sessionId) + .filterIsInstance() + .lastOrNull() + + if (identificationEvent == null) { throw InvalidRequestException( "Calling app wants to confirm identity, but the session doesn't have an identification callback event.", ClientApiError.INVALID_SESSION_ID, @@ -37,9 +49,36 @@ internal class ConfirmIdentityValidator( } } - private fun validateSelectedGuid(selectedId: String) { + private suspend fun validateSelectedGuid(selectedId: String) { if (selectedId.isBlank()) { throw InvalidRequestException("Missing Selected GUID", ClientApiError.INVALID_SELECTED_ID) } + + // Allow 'NONE_SELECTED' as a special case to indicate no selection + if (selectedId.equals("NONE_SELECTED", ignoreCase = true)) { + return + } + + // Skip further validation if skip flag is enabled + if (configManager.getProjectConfiguration().experimental().allowConfirmingGuidsNotInCallback) { + return + } + + val validGuids = identificationEvent?.payload?.scores?.map { it.guid } ?: emptyList() + + if (validGuids.isEmpty()) { + throw InvalidRequestException( + "No identification results found in session", + ClientApiError.INVALID_SELECTED_ID, + ) + } + + if (!validGuids.contains(selectedId)) { + Simber.i("Selected GUID '$selectedId' not found in identification results: $validGuids", tag = SESSION) + throw InvalidRequestException( + "Selected GUID was not part of the identification results", + ClientApiError.INVALID_SELECTED_ID, + ) + } } } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidator.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidator.kt index fdd721516c..55e34b5935 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidator.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidator.kt @@ -3,18 +3,20 @@ package com.simprints.feature.clientapi.mappers.request.validators import com.simprints.feature.clientapi.exceptions.InvalidRequestException import com.simprints.feature.clientapi.mappers.request.extractors.EnrolLastBiometricsRequestExtractor import com.simprints.feature.clientapi.models.ClientApiError +import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SESSION import com.simprints.infra.logging.Simber internal class EnrolLastBiometricsValidator( private val extractor: EnrolLastBiometricsRequestExtractor, private val currentSessionId: String, - private val sessionHasIdentificationCallback: Boolean, + private val eventRepository: EventRepository, ) : RequestActionValidator(extractor) { - override fun validate() { + override suspend fun validate() { super.validate() validateSessionId(extractor.getSessionId()) - validateSessionEvents() + validateSessionEvents(extractor.getSessionId()) } private fun validateSessionId(sessionId: String) { @@ -27,8 +29,12 @@ internal class EnrolLastBiometricsValidator( } } - private fun validateSessionEvents() { - if (!sessionHasIdentificationCallback) { + private suspend fun validateSessionEvents(sessionId: String) { + val hasIdentificationCallback = eventRepository + .getEventsFromScope(sessionId) + .any { it is IdentificationCallbackEvent } + + if (!hasIdentificationCallback) { throw InvalidRequestException( "Calling app wants to enrol last biometrics, but the session doesn't have an identification callback event.", ClientApiError.INVALID_STATE_FOR_INTENT_ACTION, diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/RequestActionValidator.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/RequestActionValidator.kt index 0a79676f35..3deb76513f 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/RequestActionValidator.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/RequestActionValidator.kt @@ -8,7 +8,7 @@ import com.simprints.feature.clientapi.models.ClientApiError internal abstract class RequestActionValidator( private val extractor: ActionRequestExtractor, ) { - open fun validate() { + open suspend fun validate() { validateProjectId() validateUserId() validateModuleId() diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidator.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidator.kt index babddaf296..17d7c7a53c 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidator.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidator.kt @@ -8,7 +8,7 @@ import com.simprints.feature.clientapi.models.ClientApiError internal class VerifyValidator( private val extractor: VerifyRequestExtractor, ) : RequestActionValidator(extractor) { - override fun validate() { + override suspend fun validate() { super.validate() validateVerifyGuid(extractor.getVerifyGuid()) } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/SessionHasIdentificationCallbackUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/SessionHasIdentificationCallbackUseCase.kt deleted file mode 100644 index d59e856ff4..0000000000 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/SessionHasIdentificationCallbackUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.simprints.feature.clientapi.usecases - -import com.simprints.infra.events.EventRepository -import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent -import javax.inject.Inject - -internal class SessionHasIdentificationCallbackUseCase @Inject constructor( - private val eventRepository: EventRepository, -) { - suspend operator fun invoke(sessionId: String): Boolean = eventRepository - .getEventsFromScope(sessionId) - .any { it is IdentificationCallbackEvent } -} diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapperTest.kt index b7965c1bb3..e61304991a 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapperTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapperTest.kt @@ -6,8 +6,8 @@ import com.simprints.core.tools.time.Timestamp import com.simprints.feature.clientapi.exceptions.InvalidRequestException import com.simprints.feature.clientapi.models.ClientApiConstants import com.simprints.feature.clientapi.usecases.GetCurrentSessionIdUseCase -import com.simprints.feature.clientapi.usecases.SessionHasIdentificationCallbackUseCase import com.simprints.infra.config.store.tokenization.TokenizationProcessor +import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.libsimprints.Constants.SIMPRINTS_LIB_VERSION import com.simprints.libsimprints.Constants.SIMPRINTS_MODULE_ID @@ -22,6 +22,7 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -35,15 +36,18 @@ class IntentToActionMapperTest { @MockK private lateinit var getCurrentSessionIdUseCase: GetCurrentSessionIdUseCase - @MockK - private lateinit var sessionHasIdentificationCallback: SessionHasIdentificationCallbackUseCase - @MockK private lateinit var tokenizationProcessor: TokenizationProcessor @MockK private lateinit var timeHelper: TimeHelper + @MockK + private lateinit var eventRepository: com.simprints.infra.events.EventRepository + + @MockK + private lateinit var configManager: com.simprints.infra.config.sync.ConfigManager + private lateinit var mapper: IntentToActionMapper @Before @@ -51,14 +55,25 @@ class IntentToActionMapperTest { MockKAnnotations.init(this, relaxed = true) coEvery { getCurrentSessionIdUseCase.invoke() } returns SESSION_ID - coEvery { sessionHasIdentificationCallback.invoke(any()) } returns true every { timeHelper.now() } returns Timestamp(0L) + coEvery { eventRepository.getEventsFromScope(any()) } returns listOf( + mockk { + every { payload } returns mockk { + every { scores } returns listOf( + mockk { + every { guid } returns SESSION_ID + }, + ) + } + }, + ) mapper = IntentToActionMapper( getCurrentSessionIdUseCase, - sessionHasIdentificationCallback, tokenizationProcessor, timeHelper, + eventRepository, + configManager, ) } diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/ConfirmIdentifyRequestBuilderTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/ConfirmIdentifyRequestBuilderTest.kt index daac1b5e31..f4bbb90b3f 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/ConfirmIdentifyRequestBuilderTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/ConfirmIdentifyRequestBuilderTest.kt @@ -14,11 +14,12 @@ import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test internal class ConfirmIdentifyRequestBuilderTest { @Test - fun `ConfirmActionRequest should contain mandatory fields`() { + fun `ConfirmActionRequest should contain mandatory fields`() = runTest { val extractor = ConfirmIdentityActionFactory.getMockExtractor() val validator = ConfirmIdentityActionFactory.getValidator(extractor) val tokenizationProcessor = mockk() diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolLastBiometricRequestBuilderTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolLastBiometricRequestBuilderTest.kt index fbe098c655..d67450717a 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolLastBiometricRequestBuilderTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolLastBiometricRequestBuilderTest.kt @@ -14,11 +14,12 @@ import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test internal class EnrolLastBiometricRequestBuilderTest { @Test - fun `EnrolLastBiometricActionRequest should contain mandatory fields`() { + fun `EnrolLastBiometricActionRequest should contain mandatory fields`() = runTest { val extractor = EnrolLastBiometricsActionFactory.getMockExtractor() val validator = EnrolLastBiometricsActionFactory.getValidator(extractor) val tokenizationProcessor = mockk() diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilderTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilderTest.kt index e02fda8058..8446ee963d 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilderTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilderTest.kt @@ -13,11 +13,12 @@ import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test internal class EnrolRequestBuilderTest { @Test - fun `EnrolActionRequest should contain mandatory fields`() { + fun `EnrolActionRequest should contain mandatory fields`() = runTest { val extractor = EnrolActionFactory.getMockExtractor() val validator = EnrolActionFactory.getValidator(extractor) val tokenizationProcessor = mockk() diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilderTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilderTest.kt index d10caf2764..5537c22b73 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilderTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilderTest.kt @@ -13,11 +13,12 @@ import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test internal class IdentifyRequestBuilderTest { @Test - fun `IdentifyActionRequest should contain mandatory fields`() { + fun `IdentifyActionRequest should contain mandatory fields`() = runTest { val extractor = IdentifyRequestActionFactory.getMockExtractor() val validator = IdentifyRequestActionFactory.getValidator(extractor) val tokenizationProcessor = mockk() diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt index 1a81b23cf7..a3323c5b84 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt @@ -14,11 +14,12 @@ import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test internal class VerifyRequestBuilderTest { @Test - fun `VerifyActionRequest should contain mandatory fields`() { + fun `VerifyActionRequest should contain mandatory fields`() = runTest { val extractor = VerifyActionFactory.getMockExtractor() val validator = VerifyActionFactory.getValidator(extractor) val tokenizationProcessor = mockk() diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/ConfirmIdentityActionFactory.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/ConfirmIdentityActionFactory.kt index efd5b8960c..3de35f9338 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/ConfirmIdentityActionFactory.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/ConfirmIdentityActionFactory.kt @@ -5,9 +5,13 @@ import com.simprints.feature.clientapi.mappers.request.builders.ConfirmIdentifyR import com.simprints.feature.clientapi.mappers.request.extractors.ActionRequestExtractor import com.simprints.feature.clientapi.mappers.request.extractors.ConfirmIdentityRequestExtractor import com.simprints.feature.clientapi.mappers.request.validators.ConfirmIdentityValidator +import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent import com.simprints.infra.orchestration.data.ActionConstants import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -30,11 +34,30 @@ internal object ConfirmIdentityActionFactory : RequestActionFactory() { unknownExtras = emptyMap(), ) - override fun getValidator(extractor: ActionRequestExtractor): ConfirmIdentityValidator = ConfirmIdentityValidator( - extractor as ConfirmIdentityRequestExtractor, - MOCK_SESSION_ID, - true, - ) + override fun getValidator(extractor: ActionRequestExtractor): ConfirmIdentityValidator { + val mockEventRepository = mockk() + // Return a valid identification callback event with the selected GUID + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf( + mockk { + every { payload } returns mockk { + every { scores } returns listOf( + mockk { + every { guid } returns MOCK_SELECTED_GUID + }, + ) + } + }, + ) + + val mockConfigManager = mockk(relaxed = true) + + return ConfirmIdentityValidator( + extractor as ConfirmIdentityRequestExtractor, + MOCK_SESSION_ID, + mockEventRepository, + configManager = mockConfigManager, + ) + } override fun getBuilder(extractor: ActionRequestExtractor): ConfirmIdentifyRequestBuilder = ConfirmIdentifyRequestBuilder( actionIdentifier = getIdentifier(), diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolLastBiometricsActionFactory.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolLastBiometricsActionFactory.kt index 8a32d5caa2..e55ff4c3be 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolLastBiometricsActionFactory.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolLastBiometricsActionFactory.kt @@ -5,9 +5,12 @@ import com.simprints.feature.clientapi.mappers.request.builders.EnrolLastBiometr import com.simprints.feature.clientapi.mappers.request.extractors.ActionRequestExtractor import com.simprints.feature.clientapi.mappers.request.extractors.EnrolLastBiometricsRequestExtractor import com.simprints.feature.clientapi.mappers.request.validators.EnrolLastBiometricsValidator +import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent import com.simprints.infra.orchestration.data.ActionConstants import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -30,11 +33,18 @@ internal object EnrolLastBiometricsActionFactory : RequestActionFactory() { unknownExtras = emptyMap(), ) - override fun getValidator(extractor: ActionRequestExtractor): EnrolLastBiometricsValidator = EnrolLastBiometricsValidator( - extractor as EnrolLastBiometricsRequestExtractor, - MOCK_SESSION_ID, - true, - ) + override fun getValidator(extractor: ActionRequestExtractor): EnrolLastBiometricsValidator { + val mockEventRepository = mockk() + // Return a valid identification callback event + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf( + mockk(), + ) + return EnrolLastBiometricsValidator( + extractor as EnrolLastBiometricsRequestExtractor, + MOCK_SESSION_ID, + mockEventRepository, + ) + } override fun getBuilder(extractor: ActionRequestExtractor): EnrolLastBiometricsRequestBuilder = EnrolLastBiometricsRequestBuilder( actionIdentifier = getIdentifier(), diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ActionRequestValidatorTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ActionRequestValidatorTest.kt index a7c83a3b1d..1ff11ba05a 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ActionRequestValidatorTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ActionRequestValidatorTest.kt @@ -4,18 +4,19 @@ import com.simprints.feature.clientapi.exceptions.InvalidRequestException import com.simprints.feature.clientapi.mappers.request.requestFactories.RequestActionFactory import com.simprints.testtools.common.syntax.assertThrows import io.mockk.every +import kotlinx.coroutines.test.runTest import org.junit.Test internal abstract class ActionRequestValidatorTest( private val mockFactory: RequestActionFactory, ) { @Test - open fun `valid ActionRequest should not fail`() { + open fun `valid ActionRequest should not fail`() = runTest { mockFactory.getValidator(mockFactory.getMockExtractor()).validate() } @Test - open fun `should fail if no projectId`() { + open fun `should fail if no projectId`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getProjectId() } returns "" @@ -23,7 +24,7 @@ internal abstract class ActionRequestValidatorTest( } @Test - open fun `should fail with projectId of invalid length`() { + open fun `should fail with projectId of invalid length`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getProjectId() } returns "a".repeat(19) @@ -31,7 +32,7 @@ internal abstract class ActionRequestValidatorTest( } @Test - open fun `should fail if no userId`() { + open fun `should fail if no userId`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getUserId() } returns "" @@ -39,7 +40,7 @@ internal abstract class ActionRequestValidatorTest( } @Test - open fun `should fail if no moduleId`() { + open fun `should fail if no moduleId`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getModuleId() } returns "" @@ -47,7 +48,7 @@ internal abstract class ActionRequestValidatorTest( } @Test - open fun `should fail with illegal moduleId`() { + open fun `should fail with illegal moduleId`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getModuleId() } returns "moduleId|moduleId" @@ -55,7 +56,7 @@ internal abstract class ActionRequestValidatorTest( } @Test - open fun `should not fail if no metadata`() { + open fun `should not fail if no metadata`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getMetadata() } returns "" @@ -63,7 +64,7 @@ internal abstract class ActionRequestValidatorTest( } @Test - open fun `should not fail if valid metadata`() { + open fun `should not fail if valid metadata`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getMetadata() } returns "{}" @@ -71,7 +72,7 @@ internal abstract class ActionRequestValidatorTest( } @Test - open fun `should fail with illegal metadata`() { + open fun `should fail with illegal metadata`() = runTest { val extractor = mockFactory.getMockExtractor() every { extractor.getMetadata() } returns "{illegalJson" diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt index 92950ef3bb..2fc8b8f359 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt @@ -3,11 +3,47 @@ package com.simprints.feature.clientapi.mappers.request.validators import com.simprints.feature.clientapi.exceptions.InvalidRequestException import com.simprints.feature.clientapi.mappers.request.requestFactories.ConfirmIdentityActionFactory import com.simprints.feature.clientapi.mappers.request.requestFactories.RequestActionFactory +import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.domain.models.callback.CallbackComparisonScore +import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent import com.simprints.testtools.common.syntax.assertThrows +import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test internal class ConfirmIdentityValidatorTest : ActionRequestValidatorTest(ConfirmIdentityActionFactory) { + @MockK + private lateinit var mockEventRepository: EventRepository + + @MockK + private lateinit var mockCallback: IdentificationCallbackEvent + + @MockK + private lateinit var mockCallbackPayload: IdentificationCallbackEvent.IdentificationCallbackPayload + + @MockK + private lateinit var mockScore1: CallbackComparisonScore + + @MockK + private lateinit var mockScore2: CallbackComparisonScore + + @MockK + private lateinit var mockProjectConfig: ProjectConfiguration + + @MockK(relaxed = true) + private lateinit var mockConfigManager: ConfigManager + + @Before + fun setUp() { + MockKAnnotations.init(this) + } + override fun `should fail if no moduleId`() {} override fun `should fail with illegal moduleId`() {} @@ -17,7 +53,7 @@ internal class ConfirmIdentityValidatorTest : ActionRequestValidatorTest(Confirm override fun `should fail with illegal metadata`() {} @Test - fun `should fail if no sessionId`() { + fun `should fail if no sessionId`() = runTest { val extractor = ConfirmIdentityActionFactory.getMockExtractor() every { extractor.getSessionId() } returns "" @@ -27,7 +63,7 @@ internal class ConfirmIdentityValidatorTest : ActionRequestValidatorTest(Confirm } @Test - fun `should fail if no selectedGuid`() { + fun `should fail if no selectedGuid`() = runTest { val extractor = ConfirmIdentityActionFactory.getMockExtractor() every { extractor.getSelectedGuid() } returns "" @@ -37,18 +73,168 @@ internal class ConfirmIdentityValidatorTest : ActionRequestValidatorTest(Confirm } @Test - fun `should fail if no identification callback in session`() { + fun `should fail if no identification callback in session`() = runTest { + val extractor = ConfirmIdentityActionFactory.getMockExtractor() + coEvery { mockEventRepository.getEventsFromScope(any()) } returns emptyList() + val validator = ConfirmIdentityValidator( + extractor, + RequestActionFactory.MOCK_SESSION_ID, + mockEventRepository, + configManager = mockConfigManager, + ) + assertThrows { + validator.validate() + } + } + + @Test + fun `should fail if invalid sessionId`() = runTest { + val extractor = ConfirmIdentityActionFactory.getMockExtractor() + coEvery { mockEventRepository.getEventsFromScope(any()) } returns emptyList() + val validator = ConfirmIdentityValidator( + extractor, + "anotherSessionID", + mockEventRepository, + configManager = mockConfigManager, + ) + assertThrows { + validator.validate() + } + } + + @Test + fun `should succeed when selected GUID is in identification results`() = runTest { + val extractor = ConfirmIdentityActionFactory.getMockExtractor() + every { extractor.getSelectedGuid() } returns "valid-guid-123" + + every { mockScore1.guid } returns "other-guid-456" + every { mockScore2.guid } returns "valid-guid-123" + every { mockCallbackPayload.scores } returns listOf(mockScore1, mockScore2) + every { mockCallback.payload } returns mockCallbackPayload + + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) + + val validator = ConfirmIdentityValidator( + extractor, + RequestActionFactory.MOCK_SESSION_ID, + mockEventRepository, + configManager = mockConfigManager, + ) + + // Should not throw + validator.validate() + } + + @Test + fun `should fail when selected GUID is not in identification results`() = runTest { + val extractor = ConfirmIdentityActionFactory.getMockExtractor() + every { extractor.getSelectedGuid() } returns "invalid-guid-999" + + every { mockScore1.guid } returns "valid-guid-123" + every { mockScore2.guid } returns "valid-guid-456" + every { mockCallbackPayload.scores } returns listOf(mockScore1, mockScore2) + every { mockCallback.payload } returns mockCallbackPayload + + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) + + val validator = ConfirmIdentityValidator( + extractor, + RequestActionFactory.MOCK_SESSION_ID, + mockEventRepository, + configManager = mockConfigManager, + ) + + assertThrows { + validator.validate() + } + } + + @Test + fun `should fail when identification results have no scores`() = runTest { val extractor = ConfirmIdentityActionFactory.getMockExtractor() - val validator = ConfirmIdentityValidator(extractor, RequestActionFactory.MOCK_SESSION_ID, false) + every { extractor.getSelectedGuid() } returns "any-guid" + + every { mockCallbackPayload.scores } returns emptyList() + every { mockCallback.payload } returns mockCallbackPayload + + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) + + val validator = ConfirmIdentityValidator( + extractor, + RequestActionFactory.MOCK_SESSION_ID, + mockEventRepository, + configManager = mockConfigManager, + ) + assertThrows { validator.validate() } } @Test - fun `should fail if invalid sessionId`() { + fun `should succeed if selectedGuid is NONE_SELECTED`() = runTest { val extractor = ConfirmIdentityActionFactory.getMockExtractor() - val validator = ConfirmIdentityValidator(extractor, "anotherSessionID", false) + every { extractor.getSelectedGuid() } returns "NONE_SELECTED" + every { mockCallbackPayload.scores } returns emptyList() + every { mockCallback.payload } returns mockCallbackPayload + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) + val validator = ConfirmIdentityValidator( + extractor, + RequestActionFactory.MOCK_SESSION_ID, + mockEventRepository, + configManager = mockConfigManager, + ) + // Should not throw + validator.validate() + } + + @Test + fun `should succeed when GUID not in results but skip feature flag is enabled`() = runTest { + val extractor = ConfirmIdentityActionFactory.getMockExtractor() + every { extractor.getSelectedGuid() } returns "guid-not-in-results" + + every { mockScore1.guid } returns "different-guid" + every { mockCallbackPayload.scores } returns listOf(mockScore1) + every { mockCallback.payload } returns mockCallbackPayload + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) + + // Mock ConfigManager with feature flag enabled + every { mockProjectConfig.custom } returns mapOf("allowConfirmingGuidsNotInCallback" to true) + coEvery { mockConfigManager.getProjectConfiguration() } returns mockProjectConfig + + val validator = ConfirmIdentityValidator( + extractor, + RequestActionFactory.MOCK_SESSION_ID, + mockEventRepository, + mockConfigManager, + ) + + // Should not throw despite GUID not being in results + validator.validate() + } + + @Test + fun `should fail when GUID not in results and feature flag is disabled`() = runTest { + val extractor = ConfirmIdentityActionFactory.getMockExtractor() + every { extractor.getSelectedGuid() } returns "guid-not-in-results" + + every { mockScore1.guid } returns "different-guid" + every { mockCallbackPayload.scores } returns listOf(mockScore1) + every { mockCallback.payload } returns mockCallbackPayload + coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) + + // Mock ConfigManager with feature flag disabled + every { mockProjectConfig.custom } returns mapOf("allowConfirmingGuidsNotInCallback" to false) + coEvery { mockConfigManager.getProjectConfiguration() } returns mockProjectConfig + + val validator = ConfirmIdentityValidator( + extractor, + RequestActionFactory.MOCK_SESSION_ID, + mockEventRepository, + mockConfigManager, + ) + + // Should throw because GUID is not in results and flag is disabled assertThrows { validator.validate() } diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidatorTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidatorTest.kt index 3a4063f256..20adcb2b88 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidatorTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/EnrolLastBiometricsValidatorTest.kt @@ -3,23 +3,29 @@ package com.simprints.feature.clientapi.mappers.request.validators import com.simprints.feature.clientapi.exceptions.InvalidRequestException import com.simprints.feature.clientapi.mappers.request.requestFactories.EnrolLastBiometricsActionFactory import com.simprints.feature.clientapi.mappers.request.requestFactories.RequestActionFactory +import com.simprints.infra.events.EventRepository import com.simprints.testtools.common.syntax.assertThrows +import io.mockk.coEvery import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test internal class EnrolLastBiometricsValidatorTest : ActionRequestValidatorTest(EnrolLastBiometricsActionFactory) { private val mockExtractor = EnrolLastBiometricsActionFactory.getMockExtractor() @Test - fun `should fail if no identification callback in session`() { - val validator = EnrolLastBiometricsValidator(mockExtractor, RequestActionFactory.MOCK_SESSION_ID, false) + fun `should fail if no identification callback in session`() = runTest { + val mockEventRepository = mockk() + coEvery { mockEventRepository.getEventsFromScope(any()) } returns emptyList() + val validator = EnrolLastBiometricsValidator(mockExtractor, RequestActionFactory.MOCK_SESSION_ID, mockEventRepository) assertThrows { validator.validate() } } @Test - fun `should fail if no sessionId`() { + fun `should fail if no sessionId`() = runTest { every { mockExtractor.getSessionId() } returns "" assertThrows { EnrolLastBiometricsActionFactory.getValidator(mockExtractor).validate() @@ -27,7 +33,7 @@ internal class EnrolLastBiometricsValidatorTest : ActionRequestValidatorTest(Enr } @Test - fun `should fail if sessionId does not match`() { + fun `should fail if sessionId does not match`() = runTest { every { mockExtractor.getSessionId() } returns "otherSessionId" assertThrows { EnrolLastBiometricsActionFactory.getValidator(mockExtractor).validate() diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidatorTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidatorTest.kt index 8e082adc34..d8fa2a9f2e 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidatorTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/VerifyValidatorTest.kt @@ -5,6 +5,7 @@ import com.simprints.feature.clientapi.exceptions.InvalidRequestException import com.simprints.feature.clientapi.mappers.request.requestFactories.VerifyActionFactory import com.simprints.testtools.common.syntax.assertThrows import io.mockk.every +import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Test import java.util.UUID @@ -13,7 +14,7 @@ internal class VerifyValidatorTest : ActionRequestValidatorTest(VerifyActionFact private val mockExtractor = VerifyActionFactory.getMockExtractor() @Test - fun `should fail if no guid to verify`() { + fun `should fail if no guid to verify`() = runTest { every { mockExtractor.getVerifyGuid() } returns "" assertThrows { @@ -22,7 +23,7 @@ internal class VerifyValidatorTest : ActionRequestValidatorTest(VerifyActionFact } @Test - fun `should fail if not guid `() { + fun `should fail if not guid `() = runTest { every { mockExtractor.getVerifyGuid() } returns "Trust me, this is a valid GUID!" assertThrows { @@ -31,7 +32,7 @@ internal class VerifyValidatorTest : ActionRequestValidatorTest(VerifyActionFact } @Test - fun `should fail if not valid guid`() { + fun `should fail if not valid guid`() = runTest { // The following UUID is one character short of fitting into the valid pattern every { mockExtractor.getVerifyGuid() } returns "123e4567-e89b-12d3-a456-55664244000" @@ -41,7 +42,7 @@ internal class VerifyValidatorTest : ActionRequestValidatorTest(VerifyActionFact } @Test - fun `should fail if nil guid`() { + fun `should fail if nil guid`() = runTest { val nilUuid = UUID(0, 0).toString() every { mockExtractor.getVerifyGuid() } returns nilUuid @@ -51,12 +52,12 @@ internal class VerifyValidatorTest : ActionRequestValidatorTest(VerifyActionFact } @Test - fun `should not fail if valid guid`() { + fun `should not fail if valid guid`() = runTest { every { mockExtractor.getVerifyGuid() } returns randomUUID() try { VerifyActionFactory.getValidator(mockExtractor).validate() - } catch (ex: Exception) { + } catch (_: Exception) { Assert.fail() } } diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/SessionHasIdentificationCallbackUseCaseTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/SessionHasIdentificationCallbackUseCaseTest.kt deleted file mode 100644 index fc3d763582..0000000000 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/SessionHasIdentificationCallbackUseCaseTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.simprints.feature.clientapi.usecases - -import com.google.common.truth.Truth.assertThat -import com.simprints.infra.events.EventRepository -import com.simprints.infra.events.event.domain.models.callback.IdentificationCallbackEvent -import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class SessionHasIdentificationCallbackUseCaseTest { - @get:Rule - val testCoroutineRule = TestCoroutineRule() - - @MockK - private lateinit var eventRepository: EventRepository - - private lateinit var useCase: SessionHasIdentificationCallbackUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - useCase = SessionHasIdentificationCallbackUseCase(eventRepository) - } - - @Test - fun `sessionHasIdentificationCallback return true if session has IdentificationCallbackEvent`() = runTest { - // Given - coEvery { eventRepository.getEventsFromScope(any()) } returns listOf( - mockk(), - mockk(), - mockk(), - ) - // Then - assertThat(useCase("sessionId")).isTrue() - } - - @Test - fun `sessionHasIdentificationCallback return false if session doesn't have IdentificationCallbackEvent`() = runTest { - // Given - coEvery { eventRepository.getEventsFromScope(any()) } returns listOf( - mockk(), - mockk(), - mockk(), - ) - // Then - assertThat(useCase("sessionId")).isFalse() - } - - @Test - fun `sessionHasIdentificationCallback return false if session events is empty`() = runTest { - // Given - coEvery { eventRepository.getEventsFromScope(any()) } returns emptyList() - // Then - assertThat(useCase("sessionId")).isFalse() - } -} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt index e6ced18048..bafa7b0a04 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt @@ -63,6 +63,12 @@ data class ExperimentalProjectConfiguration( ?.toLong() ?: FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT + val allowConfirmingGuidsNotInCallback: Boolean + get() = customConfig + ?.get(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK) + ?.let { it as? Boolean } + .let { it == true } + companion object { internal const val ENABLE_ID_POOL_VALIDATION = "validateIdentificationPool" internal const val SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED = "singleQualityFallbackRequired" @@ -82,5 +88,7 @@ data class ExperimentalProjectConfiguration( internal const val FALLBACK_TO_COMMCARE_THRESHOLD_DAYS = "fallbackToCommCareThresholdDays" internal const val FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT = 5L + + internal const val ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK = "allowConfirmingGuidsNotInCallback" } }