From bcbcf3bdc4d24e7512adaa345b954f9b59b2fb8e Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 15 Oct 2025 15:18:29 +0300 Subject: [PATCH] Expand step param and result integration tests to ensure all classes serialise correctly --- .../face/capture/FaceCaptureResult.kt | 27 +- .../feature/orchestrator/steps/Step.kt | 8 + .../cache/OrchestratorCacheIntegrationTest.kt | 281 +++++++++++++++++- .../capture/FingerprintCaptureResult.kt | 29 +- .../domain/models/BiometricDataSource.kt | 2 +- 5 files changed, 332 insertions(+), 15 deletions(-) diff --git a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt index 23cd5993dc..d12839ee2c 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt @@ -1,6 +1,7 @@ package com.simprints.face.capture import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.step.StepResult import com.simprints.infra.images.model.SecuredImageRef @@ -22,5 +23,29 @@ data class FaceCaptureResult( val template: ByteArray, val imageRef: SecuredImageRef?, val format: String, - ) : StepResult + ) : StepResult { + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Sample + + if (faceId != other.faceId) return false + if (!template.contentEquals(other.template)) return false + if (imageRef != other.imageRef) return false + if (format != other.format) return false + + return true + } + + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun hashCode(): Int { + var result = faceId.hashCode() + result = 31 * result + template.contentHashCode() + result = 31 * result + (imageRef?.hashCode() ?: 0) + result = 31 * result + format.hashCode() + return result + } + } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt index f24eccd8ba..049edd5b83 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt @@ -121,6 +121,14 @@ abstract class StepResultMixin : StepResult ) abstract class StepParamsMixin : StepParams +/** + * Step contains all of the information required to execute an orchestration step and the result of the execution. + * + * All classes used in the params structure must implement [StepParams] interface and added to [StepParamsMixin]. + * All classes used in the result structure must implement [StepResult] interface and added to [StepResultMixin]. + * + * Additionally, [StepParams] and [StepResult] subclasses can only have fields of primitives, enums and serializeables classes. + */ @Keep internal data class Step( val id: Int, diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt index ab32d72657..ca28e48fa2 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt @@ -1,23 +1,57 @@ package com.simprints.feature.orchestrator.cache import android.content.SharedPreferences -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat +import androidx.test.ext.junit.runners.* +import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.core.domain.response.AppErrorReason +import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.tools.json.JsonHelper import com.simprints.face.capture.FaceCaptureParams +import com.simprints.face.capture.FaceCaptureResult +import com.simprints.feature.alert.AlertResult +import com.simprints.feature.consent.ConsentParams +import com.simprints.feature.consent.ConsentResult +import com.simprints.feature.consent.ConsentType +import com.simprints.feature.enrollast.EnrolLastBiometricParams +import com.simprints.feature.enrollast.EnrolLastBiometricStepResult +import com.simprints.feature.enrollast.FaceTemplateCaptureResult +import com.simprints.feature.enrollast.FingerTemplateCaptureResult +import com.simprints.feature.enrollast.MatchResult +import com.simprints.feature.exitform.ExitFormOption +import com.simprints.feature.exitform.ExitFormResult +import com.simprints.feature.fetchsubject.FetchSubjectParams +import com.simprints.feature.fetchsubject.FetchSubjectResult +import com.simprints.feature.login.LoginError +import com.simprints.feature.login.LoginParams +import com.simprints.feature.login.LoginResult import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.feature.orchestrator.steps.StepStatus +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult +import com.simprints.feature.selectsubject.SelectSubjectParams +import com.simprints.feature.selectsubject.SelectSubjectResult +import com.simprints.feature.setup.SetupResult +import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams +import com.simprints.feature.validatepool.ValidateSubjectPoolResult +import com.simprints.fingerprint.capture.FingerprintCaptureParams import com.simprints.fingerprint.capture.FingerprintCaptureResult +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration +import com.simprints.infra.config.store.models.Finger +import com.simprints.infra.config.store.models.FingerprintConfiguration +import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource +import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 +import com.simprints.infra.images.model.Path +import com.simprints.infra.images.model.SecuredImageRef import com.simprints.infra.security.SecurityManager -import io.mockk.MockKAnnotations -import io.mockk.every +import com.simprints.matcher.FaceMatchResult +import com.simprints.matcher.FingerprintMatchResult +import com.simprints.matcher.MatchParams +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.justRun -import io.mockk.slot import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -52,12 +86,119 @@ class OrchestratorCacheIntegrationTest { } @Test - fun `Stores and restores steps`() { + fun `Stores and restores common steps`() { + val expected = listOf( + Step( + id = StepId.SETUP, + navigationActionId = 5, + destinationId = 6, + params = null, + status = StepStatus.IN_PROGRESS, + result = SetupResult(true), + ), + Step( + id = StepId.FETCH_GUID, + navigationActionId = 5, + destinationId = 6, + params = FetchSubjectParams("projectId", "subjectId", ""), + status = StepStatus.COMPLETED, + result = FetchSubjectResult(false), + ), + Step( + id = StepId.CONSENT, + navigationActionId = 5, + destinationId = 6, + params = ConsentParams(type = ConsentType.ENROL), + status = StepStatus.COMPLETED, + result = ConsentResult(true), + ), + Step( + id = StepId.ENROL_LAST_BIOMETRIC, + navigationActionId = 5, + destinationId = 6, + params = EnrolLastBiometricParams( + projectId = "projectId", + userId = TokenizableString.Raw("value"), + moduleId = TokenizableString.Raw("value"), + steps = listOf( + EnrolLastBiometricStepResult.FingerprintCaptureResult( + "referenceId", + listOf( + FingerTemplateCaptureResult( + Finger.LEFT_4TH_FINGER, + byteArrayOf(1, 2, 3), + 10, + "NEC", + ), + ), + ), + EnrolLastBiometricStepResult.FingerprintMatchResult( + listOf(MatchResult("subjectId", 0.5f)), + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + ), + EnrolLastBiometricStepResult.FaceCaptureResult( + "referenceId", + listOf( + FaceTemplateCaptureResult(byteArrayOf(1, 2, 3), "RankOne"), + ), + ), + EnrolLastBiometricStepResult.FaceMatchResult( + listOf(MatchResult("subjectId", 0.5f)), + FaceConfiguration.BioSdk.RANK_ONE, + ), + EnrolLastBiometricStepResult.EnrolLastBiometricsResult("subjectId"), + ), + ), + status = StepStatus.COMPLETED, + result = ValidateSubjectPoolResult(true), + ), + Step( + id = StepId.CONFIRM_IDENTITY, + navigationActionId = 5, + destinationId = 6, + params = SelectSubjectParams("projectId", "subjectId"), + status = StepStatus.COMPLETED, + result = SelectSubjectResult(true), + ), + Step( + id = StepId.VALIDATE_ID_POOL, + navigationActionId = 5, + destinationId = 6, + params = ValidateSubjectPoolFragmentParams(SubjectQuery()), + status = StepStatus.COMPLETED, + result = ValidateSubjectPoolResult(true), + ), + Step( + id = StepId.SELECT_SUBJECT_AGE, + navigationActionId = 5, + destinationId = 6, + params = null, + status = StepStatus.COMPLETED, + result = SelectSubjectAgeGroupResult(AgeGroup(10, null)), + ), + ) + + cache.steps = expected + val actual = cache.steps + + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } + } + + @Test + fun `Stores and restores fingerprint modality steps`() { val expected = listOf( Step( id = StepId.FINGERPRINT_CAPTURE, navigationActionId = 3, destinationId = 4, + params = FingerprintCaptureParams( + flowType = FlowType.ENROL, + fingerprintsToCapture = listOf(IFingerIdentifier.LEFT_4TH_FINGER), + fingerprintSDK = FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + ), status = StepStatus.COMPLETED, result = FingerprintCaptureResult( "", @@ -65,27 +206,143 @@ class OrchestratorCacheIntegrationTest { FingerprintCaptureResult.Item( captureEventId = GUID1, identifier = IFingerIdentifier.LEFT_THUMB, - sample = null, + sample = FingerprintCaptureResult.Sample( + fingerIdentifier = IFingerIdentifier.LEFT_4TH_FINGER, + template = byteArrayOf(1, 2, 3), + templateQualityScore = 10, + imageRef = SecuredImageRef(Path("file/path")), + format = "NEC", + ), ), ), ), ), + Step( + id = StepId.FINGERPRINT_MATCHER, + navigationActionId = 3, + destinationId = 4, + params = MatchParams( + probeReferenceId = GUID1, + flowType = FlowType.IDENTIFY, + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.CommCare("name"), + probeFingerprintSamples = listOf( + MatchParams.FingerprintSample( + fingerId = IFingerIdentifier.LEFT_4TH_FINGER, + format = "NEC", + template = byteArrayOf(1, 2, 3), + ), + ), + ), + status = StepStatus.COMPLETED, + result = FingerprintMatchResult( + listOf(FingerprintMatchResult.Item("subjectId", 0.5f)), + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + ), + ), + ) + + cache.steps = expected + val actual = cache.steps + + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } + } + + @Test + fun `Stores and restores face modality steps`() { + val expected = listOf( Step( id = StepId.FACE_CAPTURE, navigationActionId = 5, destinationId = 6, params = FaceCaptureParams(3, FaceConfiguration.BioSdk.RANK_ONE), status = StepStatus.COMPLETED, - result = null, + result = FaceCaptureResult( + "", + results = listOf( + FaceCaptureResult.Item( + captureEventId = "event", + index = 0, + sample = FaceCaptureResult.Sample( + faceId = "faceId", + template = byteArrayOf(1, 2, 3), + imageRef = SecuredImageRef(Path("file/path")), + format = "ROC", + ), + ), + ), + ), + ), + Step( + id = StepId.FACE_MATCHER, + navigationActionId = 3, + destinationId = 4, + params = MatchParams( + probeReferenceId = GUID1, + flowType = FlowType.IDENTIFY, + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.Simprints, + probeFaceSamples = listOf( + MatchParams.FaceSample( + faceId = "faceId", + template = byteArrayOf(1, 2, 3), + ), + ), + ), + status = StepStatus.COMPLETED, + result = FaceMatchResult( + listOf(FaceMatchResult.Item("subjectId", 0.5f)), + FaceConfiguration.BioSdk.RANK_ONE, + ), + ), + ) + + cache.steps = expected + val actual = cache.steps + + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } + } + + @Test + fun `Stores and restores exception steps`() { + val expected = listOf( + Step( + id = 1, + navigationActionId = 5, + destinationId = 6, + params = LoginParams("projectId", TokenizableString.Tokenized("value")), + status = StepStatus.NOT_STARTED, + result = LoginResult(false, LoginError.LoginNotCompleted), + ), + Step( + id = 2, + navigationActionId = 5, + destinationId = 6, + status = StepStatus.NOT_STARTED, + result = AlertResult("key", AppErrorReason.UNEXPECTED_ERROR), + ), + Step( + id = 3, + navigationActionId = 5, + destinationId = 6, + status = StepStatus.NOT_STARTED, + result = ExitFormResult(true, ExitFormOption.DataConcerns), ), ) cache.steps = expected val actual = cache.steps - assertThat(actual).hasSize(2) - compareStubs(expected[0], actual[0]) - compareStubs(expected[1], actual[1]) + assertThat(actual).hasSize(expected.size) + for (i in expected.indices) { + compareStubs(expected[i], actual[i]) + } } private fun compareStubs( diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt index ad73c56792..a8c8b916ef 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt @@ -1,6 +1,7 @@ package com.simprints.fingerprint.capture import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.step.StepResult import com.simprints.infra.images.model.SecuredImageRef @@ -24,5 +25,31 @@ data class FingerprintCaptureResult( val templateQualityScore: Int, val imageRef: SecuredImageRef?, val format: String, - ) : StepResult + ) : StepResult { + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Sample + + if (templateQualityScore != other.templateQualityScore) return false + if (fingerIdentifier != other.fingerIdentifier) return false + if (!template.contentEquals(other.template)) return false + if (imageRef != other.imageRef) return false + if (format != other.format) return false + + return true + } + + @ExcludedFromGeneratedTestCoverageReports("Generated code") + override fun hashCode(): Int { + var result = templateQualityScore + result = 31 * result + fingerIdentifier.hashCode() + result = 31 * result + template.contentHashCode() + result = 31 * result + (imageRef?.hashCode() ?: 0) + result = 31 * result + format.hashCode() + return result + } + } } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt index d468c0e756..6b69d6abb3 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/BiometricDataSource.kt @@ -13,7 +13,7 @@ sealed class BiometricDataSource : StepParams { data object Simprints : BiometricDataSource() data class CommCare( - private val callerPackageName: String, + val callerPackageName: String, ) : BiometricDataSource() { override fun callerPackageName() = callerPackageName