diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt index f36192bded..bc0a798ead 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt @@ -27,6 +27,7 @@ import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.allowedAgeRanges import com.simprints.infra.config.store.models.fromDomainToModuleApi import com.simprints.infra.config.store.models.isAgeRestricted +import com.simprints.infra.config.store.models.sortedUniqueAgeGroups import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.orchestration.data.ActionRequest @@ -422,7 +423,7 @@ internal class BuildStepsUseCase @Inject constructor( private fun ageGroupFromSubjectAge(action: ActionRequest, projectConfiguration: ProjectConfiguration): AgeGroup? { return action.getSubjectAgeIfAvailable()?.let { subjectAge -> - projectConfiguration.allowedAgeRanges().firstOrNull{ it.includes(subjectAge) } + projectConfiguration.sortedUniqueAgeGroups().firstOrNull{ it.includes(subjectAge) } } } } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt index 991895e796..21892cf49e 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt @@ -60,7 +60,7 @@ class BuildStepsUseCaseTest { Finger.LEFT_THUMB, Finger.RIGHT_THUMB, ) - every { secugenSimMatcher.allowedAgeRange } returns null + every { secugenSimMatcher.allowedAgeRange } returns AgeGroup(0, null) every { projectConfiguration.fingerprint?.secugenSimMatcher } returns secugenSimMatcher every { projectConfiguration.fingerprint?.getSdkConfiguration(SECUGEN_SIM_MATCHER) } returns secugenSimMatcher @@ -68,7 +68,7 @@ class BuildStepsUseCaseTest { Finger.LEFT_INDEX_FINGER, Finger.RIGHT_INDEX_FINGER, ) - every { nec.allowedAgeRange } returns null + every { nec.allowedAgeRange } returns AgeGroup(0, null) every { projectConfiguration.fingerprint?.nec } returns nec every { projectConfiguration.fingerprint?.getSdkConfiguration(NEC) } returns nec diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt index b5d1b90d82..3dc7218102 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt @@ -3,7 +3,7 @@ package com.simprints.feature.selectagegroup.screen import android.content.Context import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.AgeGroup -import com.simprints.infra.config.store.models.allowedAgeRanges +import com.simprints.infra.config.store.models.sortedUniqueAgeGroups import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import com.simprints.infra.resources.R as IDR @@ -19,28 +19,12 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( * it also adds a 0- and -above if they are not present */ suspend operator fun invoke(): List { - val allowedAgeRanges = configurationRepo.getProjectConfiguration().allowedAgeRanges() - val processedAgeRanges = generateSortedUniqueAgeGroups(allowedAgeRanges) + val processedAgeRanges = configurationRepo.getProjectConfiguration().sortedUniqueAgeGroups() return processedAgeRanges.map { ageGroup -> AgeGroupDisplayModel(ageGroup.getDisplayName(), ageGroup) } } - private fun generateSortedUniqueAgeGroups(ageGroups: List): List { - // Handle empty list case by returning a single age group starting at 0 and ending with null - if (ageGroups.isEmpty()) return listOf(AgeGroup(0, null)) - - // Flatten all start and end ages into a single list, removing nulls, duplicates, and sorting. - var sortedUniqueAges = - ageGroups.flatMap { listOf(it.startInclusive, it.endExclusive) }.filterNotNull() - // Ensure the first age group starts at 0 - sortedUniqueAges = (listOf(0) + sortedUniqueAges).sorted().distinct() - // Create age groups based on sorted unique ages - return sortedUniqueAges.zipWithNext { start, end -> AgeGroup(start, end) } + AgeGroup( - sortedUniqueAges.last(), null - ) - } - // Helper function to convert months to readable format private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { val years = ageInMonths / 12 diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt index f4656ddff4..85d0cbc05f 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.local.models import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration internal fun FaceConfiguration.toProto(): ProtoFaceConfiguration = @@ -22,8 +23,8 @@ internal fun FaceConfiguration.FaceSdkConfiguration.toProto() = .setImageSavingStrategy(imageSavingStrategy.toProto()) .setDecisionPolicy(decisionPolicy.toProto()) .setVersion(version) + .setAllowedAgeRange(allowedAgeRange.toProto()) .also { - if (allowedAgeRange != null) it.allowedAgeRange = allowedAgeRange.toProto() if (verificationMatchThreshold != null) it.verificationMatchThreshold = verificationMatchThreshold }.build() @@ -53,8 +54,8 @@ internal fun ProtoFaceConfiguration.ProtoFaceSdkConfiguration.toDomain() = imageSavingStrategy = imageSavingStrategy.toDomain(), decisionPolicy = decisionPolicy.toDomain(), version = version, - if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else null, - if (hasVerificationMatchThreshold()) verificationMatchThreshold else null + allowedAgeRange = if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else AgeGroup(0, null), + verificationMatchThreshold = if (hasVerificationMatchThreshold()) verificationMatchThreshold else null ) internal fun ProtoFaceConfiguration.ImageSavingStrategy.toDomain(): FaceConfiguration.ImageSavingStrategy = diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt index 5123cc098d..3d613ac4ec 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt @@ -20,10 +20,10 @@ internal fun FingerprintConfiguration.FingerprintSdkConfiguration.toProto() = .addAllFingersToCapture(fingersToCapture.map { it.toProto() }) .setDecisionPolicy(decisionPolicy.toProto()) .setComparisonStrategyForVerification(comparisonStrategyForVerification.toProto()) + .setAllowedAgeRange(allowedAgeRange.toProto()) .also { if (vero1 != null) it.vero1 = vero1.toProto() if (vero2 != null) it.vero2 = vero2.toProto() - if (allowedAgeRange != null) it.allowedAgeRange = allowedAgeRange.toProto() if (verificationMatchThreshold != null) it.verificationMatchThreshold = verificationMatchThreshold }.build() @@ -85,13 +85,13 @@ internal fun ProtoFingerprintConfiguration.ProtoBioSdk.toDomain() = when (this) internal fun ProtoFingerprintConfiguration.ProtoFingerprintSdkConfiguration.toDomain() = FingerprintConfiguration.FingerprintSdkConfiguration( - fingersToCaptureList.map { it.toDomain() }, - decisionPolicy.toDomain(), - comparisonStrategyForVerification.toDomain(), - if (hasVero1()) vero1.toDomain() else null, - if (hasVero2()) vero2.toDomain() else null, - if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else null, - if (hasVerificationMatchThreshold()) verificationMatchThreshold else null, + fingersToCapture = fingersToCaptureList.map { it.toDomain() }, + decisionPolicy = decisionPolicy.toDomain(), + comparisonStrategyForVerification = comparisonStrategyForVerification.toDomain(), + vero1 = if (hasVero1()) vero1.toDomain() else null, + vero2 = if (hasVero2()) vero2.toDomain() else null, + allowedAgeRange = if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else AgeGroup(0, null), + verificationMatchThreshold = if (hasVerificationMatchThreshold()) verificationMatchThreshold else null, ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt index 423e85b275..aa26030465 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt @@ -26,7 +26,7 @@ data class FaceConfiguration( val imageSavingStrategy: ImageSavingStrategy, val decisionPolicy: DecisionPolicy, val version: String, - val allowedAgeRange: AgeGroup? = null, + val allowedAgeRange: AgeGroup = AgeGroup(0, null), val verificationMatchThreshold: Float? = null, ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt index 14c8beeabb..db9a36e91d 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt @@ -14,7 +14,7 @@ data class FingerprintConfiguration( val comparisonStrategyForVerification: FingerComparisonStrategy, val vero1: Vero1Configuration? = null, val vero2: Vero2Configuration? = null, - val allowedAgeRange: AgeGroup? = null, + val allowedAgeRange: AgeGroup = AgeGroup(0, null), val verificationMatchThreshold: Float? = null, ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt index 6d3aa3c287..e6b7946450 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt @@ -42,11 +42,33 @@ fun ProjectConfiguration.imagesUploadRequiresUnmeteredConnection(): Boolean = synchronization.up.simprints.imagesRequireUnmeteredConnection fun ProjectConfiguration.allowedAgeRanges(): List { - return listOf( + return listOfNotNull( face?.rankOne?.allowedAgeRange, fingerprint?.secugenSimMatcher?.allowedAgeRange, fingerprint?.nec?.allowedAgeRange - ).filterNotNull().filterNot { it.isEmpty() } + ) } -fun ProjectConfiguration.isAgeRestricted() = allowedAgeRanges().isNotEmpty() +/** + * Returns a list of all age groups a subject can fall into based on the allowed age ranges + * of the project's face and fingerprint SDK configurations. + * Note that some of these age groups might be unsupported! + */ +fun ProjectConfiguration.sortedUniqueAgeGroups(): List { + val ageGroups = allowedAgeRanges() + + // Handle empty list case by returning a single age group starting at 0 and ending with null + if (ageGroups.isEmpty()) return listOf(AgeGroup(0, null)) + + // Flatten all start and end ages into a single list, removing nulls, duplicates, and sorting. + var sortedUniqueAges = + ageGroups.flatMap { listOf(it.startInclusive, it.endExclusive) }.filterNotNull() + // Ensure the first age group starts at 0 + sortedUniqueAges = (listOf(0) + sortedUniqueAges).sorted().distinct() + // Create age groups based on sorted unique ages + return sortedUniqueAges.zipWithNext { start, end -> AgeGroup(start, end) } + AgeGroup( + sortedUniqueAges.last(), null + ) +} + +fun ProjectConfiguration.isAgeRestricted() = allowedAgeRanges().any { !it.isEmpty()} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt index ec0c6bee81..185cd009ed 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt @@ -9,9 +9,5 @@ data class ApiAllowedAgeRange( val endExclusive: Int?, ) { - fun toDomain() = - // When allowedAgeRange is disabled the API returns an empty object - // which is then parsed as {null, null} - if (startInclusive == null && endExclusive == null) null - else AgeGroup(startInclusive ?: 0, endExclusive) + fun toDomain() = AgeGroup(startInclusive ?: 0, endExclusive) } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt index 38ad15e340..432ed8cb86 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration @Keep @@ -30,7 +31,7 @@ internal data class ApiFaceConfiguration( qualityThreshold = qualityThreshold, decisionPolicy = decisionPolicy.toDomain(), imageSavingStrategy = imageSavingStrategy.toDomain(), - allowedAgeRange = allowedAgeRange?.toDomain(), + allowedAgeRange = allowedAgeRange?.toDomain() ?: AgeGroup(0, null), verificationMatchThreshold = verificationMatchThreshold, version = version ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt index 14f8ce516c..4c355a8ea0 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.Finger as DomainFingerprint @@ -37,7 +38,7 @@ internal data class ApiFingerprintConfiguration( comparisonStrategyForVerification = comparisonStrategyForVerification.toDomain(), vero1 = vero1?.toDomain(), vero2 = vero2?.toDomain(), - allowedAgeRange = allowedAgeRange?.toDomain(), + allowedAgeRange = allowedAgeRange?.toDomain() ?: AgeGroup(0, null), verificationMatchThreshold = verificationMatchThreshold, ) } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt index e365f97a51..1377d1c966 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt @@ -9,6 +9,7 @@ import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.local.migrations.ProjectConfigSharedPrefsMigration.Companion.ALL_KEYS import com.simprints.infra.config.store.local.migrations.ProjectConfigSharedPrefsMigration.Companion.PROJECT_SETTINGS_JSON_STRING_KEY import com.simprints.infra.config.store.local.migrations.models.OldProjectConfig +import com.simprints.infra.config.store.local.models.ProtoAllowedAgeRange import com.simprints.infra.config.store.local.models.ProtoConsentConfiguration import com.simprints.infra.config.store.local.models.ProtoDecisionPolicy import com.simprints.infra.config.store.local.models.ProtoDownSynchronizationConfiguration @@ -654,6 +655,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setDecisionPolicy( ProtoDecisionPolicy.newBuilder().setLow(1).setMedium(20).setHigh(100).build() ) + .setAllowedAgeRange(ProtoAllowedAgeRange.newBuilder().build()) .setVersion("1.23") .build() ) @@ -669,6 +671,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setDecisionPolicy( ProtoDecisionPolicy.newBuilder().setLow(0).setMedium(0).setHigh(0).build() ) + .setAllowedAgeRange(ProtoAllowedAgeRange.newBuilder().setStartInclusive(0).build()) .setVersion("1.23") .build() ) @@ -725,6 +728,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setComparisonStrategyForVerification(ProtoFingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER) .setVero1(ProtoVero1Configuration.newBuilder().setQualityThreshold(60).build()) .setVero2(PROTO_VERO_2_CONFIGURATION) + .setAllowedAgeRange(ProtoAllowedAgeRange.newBuilder().build()) .build() ).build() @@ -749,6 +753,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setVero1( ProtoVero1Configuration.newBuilder().setQualityThreshold(60).build() ) + .setAllowedAgeRange(ProtoAllowedAgeRange.newBuilder().build()) .build() ).build() } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt index 11204b14d9..436502ecdc 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt @@ -14,6 +14,15 @@ class FaceConfigurationTest { assertThat(faceConfiguration.toProto()).isEqualTo(protoFaceConfiguration) } + @Test + fun `should map correctly the model with allowedAgeRange missing`() { + val protoFaceConfigurationWithoutAgeRange = protoFaceConfiguration.toBuilder() + .setRankOne(protoFaceConfiguration.rankOne.toBuilder().clearAllowedAgeRange()) + .build() + + assertThat(protoFaceConfigurationWithoutAgeRange.toDomain()).isEqualTo(faceConfiguration) + } + @Test fun `should map correctly the ImageSavingStrategy enums`() { val mapping = mapOf( diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt index 803cfd68ce..63282cadd3 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.local.models import com.google.common.truth.Truth.assertThat +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.Finger import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.testtools.fingerprintConfiguration @@ -15,6 +16,20 @@ class FingerprintConfigurationTest { assertThat(fingerprintConfiguration.toProto()).isEqualTo(protoFingerprintConfiguration) } + @Test + fun `should map correctly the model with allowedAgeRange missing`() { + val protoFingerprintConfigurationWithoutAgeRange = protoFingerprintConfiguration.toBuilder() + .setSecugenSimMatcher(protoFingerprintConfiguration.secugenSimMatcher.toBuilder().clearAllowedAgeRange()) + .build() + val fingerprintConfigurationWithoutAgeRange = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy( + allowedAgeRange = AgeGroup(0, null) + ) + ) + + assertThat(protoFingerprintConfigurationWithoutAgeRange.toDomain()).isEqualTo(fingerprintConfigurationWithoutAgeRange) + } + @Test fun `should map correctly the old model to new model`() { val proto = protoFingerprintConfiguration.toBuilder() diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt index c963aa355d..aed1450675 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt @@ -269,38 +269,32 @@ class ProjectConfigurationTest { } @Test - fun `allowedAgeRanges does not return empty age ranges`() { - val faceAgeRange = AgeGroup(10, 20) - val emptyAgeRange = AgeGroup(0, 0) - + fun `isAgeRestricted should return false when all are empty`() { + // Arrange val projectConfiguration = projectConfiguration.copy( - face = faceConfiguration.copy( - rankOne = faceConfiguration.rankOne?.copy( - allowedAgeRange = faceAgeRange - ) - ), + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = AgeGroup(0, null))), fingerprint = fingerprintConfiguration.copy( - secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy( - allowedAgeRange = emptyAgeRange - ), + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = AgeGroup(0, null)), nec = null ) ) // Act - val result = projectConfiguration.allowedAgeRanges() + val result = projectConfiguration.isAgeRestricted() // Assert - assertThat(result).containsExactly(faceAgeRange) + assertThat(result).isFalse() } @Test - fun `isAgeRestricted should return false when all are null`() { + fun `isAgeRestricted should return false when all age ranges are empty`() { // Arrange + val emptyAgeRange = AgeGroup(0, 0) + val projectConfiguration = projectConfiguration.copy( - face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = null)), + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = emptyAgeRange)), fingerprint = fingerprintConfiguration.copy( - secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = null), + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = emptyAgeRange), nec = null ) ) @@ -313,14 +307,15 @@ class ProjectConfigurationTest { } @Test - fun `isAgeRestricted should return false when all age ranges are empty`() { + fun `isAgeRestricted should return true when all age ranges are non-empty`() { // Arrange - val emptyAgeRange = AgeGroup(0, 0) + val faceAgeRange = AgeGroup(10, 20) + val secugenSimMatcherAgeRange = AgeGroup(20, 30) val projectConfiguration = projectConfiguration.copy( - face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = emptyAgeRange)), + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)), fingerprint = fingerprintConfiguration.copy( - secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = emptyAgeRange), + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange), nec = null ) ) @@ -329,12 +324,27 @@ class ProjectConfigurationTest { val result = projectConfiguration.isAgeRestricted() // Assert - assertThat(result).isFalse() + assertThat(result).isTrue() } @Test - fun `isAgeRestricted should return true when all age ranges are non-empty`() { - // Arrange + fun `sortedUniqueAgeGroups should return (0, null) when all age groups are empty`() { + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = null), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = null, + nec = null + ) + ) + + val result = projectConfiguration.sortedUniqueAgeGroups() + val expected = listOf(AgeGroup(0, null)) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `sortedUniqueAgeGroups should return a sorted list of unique age groups when there are no overlapping age groups`() { val faceAgeRange = AgeGroup(10, 20) val secugenSimMatcherAgeRange = AgeGroup(20, 30) @@ -346,10 +356,109 @@ class ProjectConfigurationTest { ) ) - // Act - val result = projectConfiguration.isAgeRestricted() + val result = projectConfiguration.sortedUniqueAgeGroups() + val expected = listOf( + AgeGroup(0, faceAgeRange.startInclusive), + faceAgeRange, + secugenSimMatcherAgeRange, + AgeGroup(secugenSimMatcherAgeRange.endExclusive!!, null) + ) - // Assert - assertThat(result).isTrue() + assertThat(result).isEqualTo(expected) + } + + @Test + fun `sortedUniqueAgeGroups should handle overlapping age groups correctly`() { + val faceAgeRange = AgeGroup(10, 20) + val secugenSimMatcherAgeRange = AgeGroup(15, 30) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange), + nec = null + ) + ) + + val result = projectConfiguration.sortedUniqueAgeGroups() + val expected = listOf( + AgeGroup(0, faceAgeRange.startInclusive), + AgeGroup(faceAgeRange.startInclusive, secugenSimMatcherAgeRange.startInclusive), + AgeGroup(secugenSimMatcherAgeRange.startInclusive, faceAgeRange.endExclusive), + AgeGroup(faceAgeRange.endExclusive!!, secugenSimMatcherAgeRange.endExclusive!!), + AgeGroup(secugenSimMatcherAgeRange.endExclusive!!, null) + ) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `sortedUniqueAgeGroups should remove duplicates and sort the age groups correctly`() { + val faceAgeRange = AgeGroup(10, 20) + val duplicateAgeRange = AgeGroup(10, 20) + val secugenSimMatcherAgeRange = AgeGroup(20, 30) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange), + nec = fingerprintConfiguration.nec?.copy(allowedAgeRange = duplicateAgeRange) + ) + ) + + val result = projectConfiguration.sortedUniqueAgeGroups() + val expected = listOf( + AgeGroup(0, faceAgeRange.startInclusive), + faceAgeRange, + secugenSimMatcherAgeRange, + AgeGroup(secugenSimMatcherAgeRange.endExclusive!!, null) + ) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `sortedUniqueAgeGroups should handle correctly a mix of restricted and unrestricted SDKs`() { + val faceAgeRange = AgeGroup(0, null) + val secugenSimMatcherAgeRange = AgeGroup(20, 30) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange), + nec = null + ) + ) + + val result = projectConfiguration.sortedUniqueAgeGroups() + val expected = listOf( + AgeGroup(0, secugenSimMatcherAgeRange.startInclusive), + secugenSimMatcherAgeRange, + AgeGroup(secugenSimMatcherAgeRange.endExclusive!!, null) + ) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `sortedUniqueAgeGroups should handle correctly overlapping null end ranges`() { + val faceAgeRange = AgeGroup(0, null) + val secugenSimMatcherAgeRange = AgeGroup(20, null) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange), + nec = null + ) + ) + + val result = projectConfiguration.sortedUniqueAgeGroups() + val expected = listOf( + AgeGroup(0, secugenSimMatcherAgeRange.startInclusive), + secugenSimMatcherAgeRange, + ) + + assertThat(result).isEqualTo(expected) } } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt index 6b5e9080c8..ce1a456d36 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt @@ -14,10 +14,11 @@ class ApiAllowedAgeRangeTest { } @Test - fun `should return null when startInclusive and endExclusive are null`() { + fun `should return empty range when startInclusive and endExclusive are null`() { val apiAllowedAgeRange = ApiAllowedAgeRange(null, null) + val expectedAgeGroup = AgeGroup(0, null) - assertThat(apiAllowedAgeRange.toDomain()).isNull() + assertThat(apiAllowedAgeRange.toDomain()).isEqualTo(expectedAgeGroup) } @Test diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt index e134eacfc0..53936baa2b 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.remote.models import com.google.common.truth.Truth.assertThat +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.testtools.apiFaceConfiguration import com.simprints.infra.config.store.testtools.faceConfiguration @@ -13,6 +14,21 @@ class ApiFaceConfigurationTest { assertThat(apiFaceConfiguration.toDomain()).isEqualTo(faceConfiguration) } + @Test + fun `should map correctly the model with allowedAgeRange present`() { + val apiFaceConfigurationWithAgeRange = apiFaceConfiguration.copy( + rankOne = apiFaceConfiguration.rankOne.copy( + allowedAgeRange = ApiAllowedAgeRange(10, 20) + ) + ) + val faceConfigurationWithAgeRange = faceConfiguration.copy( + rankOne = faceConfiguration.rankOne!!.copy( + allowedAgeRange = AgeGroup(10, 20) + ) + ) + assertThat(apiFaceConfigurationWithAgeRange.toDomain()).isEqualTo(faceConfigurationWithAgeRange) + } + @Test fun `should map correctly the ImageSavingStrategy enums`() { val mapping = mapOf( diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt index f8e2759874..480f749c81 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.remote.models import com.google.common.truth.Truth.assertThat +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.Finger import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.Vero1Configuration @@ -17,6 +18,21 @@ class ApiFingerprintConfigurationTest { assertThat(apiFingerprintConfiguration.toDomain()).isEqualTo(fingerprintConfiguration) } + @Test + fun `should map correctly the model with allowedAgeRange null`() { + val apiFingerprintConfigurationWithAgeRange = apiFingerprintConfiguration.copy( + secugenSimMatcher = apiFingerprintConfiguration.secugenSimMatcher?.copy( + allowedAgeRange = null + ) + ) + val fingerprintConfigurationWithAgeRange = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy( + allowedAgeRange = AgeGroup(0, null) + ) + ) + assertThat(apiFingerprintConfigurationWithAgeRange.toDomain()).isEqualTo(fingerprintConfigurationWithAgeRange) + } + @Test fun `should map correctly the model when the vero1 is missing`() { val apiFingerprintConfiguration = ApiFingerprintConfiguration( diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt index d81121b510..2358d504f2 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt @@ -117,7 +117,7 @@ internal val rankOneConfiguration = FaceSdkConfiguration( qualityThreshold = -1, imageSavingStrategy = FaceConfiguration.ImageSavingStrategy.NEVER, decisionPolicy = decisionPolicy, - allowedAgeRange = null, + allowedAgeRange = AgeGroup(0, null), verificationMatchThreshold = null, version = "1.0" ) @@ -147,6 +147,7 @@ internal val protoFaceConfiguration = ProtoFaceConfiguration.newBuilder() .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.NEVER) .setDecisionPolicy(protoDecisionPolicy) .setVersion("1.0") + .setAllowedAgeRange(ProtoAllowedAgeRange.newBuilder().build()) .build() ) .build()