diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt index 6661aa5441..cba8da95f5 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt @@ -47,11 +47,13 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor( } private fun Subject.fromSubjectToEnrolmentCreationEvent() = EnrolmentRecordCreationEvent( - subjectId, - projectId, - moduleId, - attendantId, - EnrolmentRecordCreationEvent.buildBiometricReferences(fingerprintSamples, faceSamples, encoder), + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId, + attendantId = attendantId, + biometricReferences = EnrolmentRecordCreationEvent.buildBiometricReferences(fingerprintSamples, faceSamples, encoder), + // TODO [CORE-3421] Review if EnrolmentRecordCreationEvent should contain List of external credentials, as it currently doesn't make sense + externalCredentials = externalCredentials ) companion object { diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt index ba78166670..b232c0c3e4 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt @@ -1,5 +1,6 @@ package com.simprints.feature.enrollast.screen.usecase +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier @@ -31,8 +32,14 @@ internal class BuildSubjectUseCase @Inject constructor( faceSamples = getFaceCaptureResult(params.steps) ?.let { result -> result.results.map { faceSample(result.referenceId, it) } } .orEmpty(), + externalCredentials = getExternalCredentialResult(params.steps)?.let { listOf(it) } ?: emptyList() ) + // TODO [CORE-3421] When an external credential can be extracted from the UI-level steps, extract it here + private fun getExternalCredentialResult(steps: List): ExternalCredential? { + return null + } + private fun getFingerprintCaptureResult(steps: List) = steps .filterIsInstance() .firstOrNull() diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt index 69d2b57198..53f4dc9a2f 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt @@ -1,5 +1,6 @@ package com.simprints.feature.orchestrator.usecases.response +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.response.AppErrorReason import com.simprints.face.capture.FaceCaptureResult import com.simprints.fingerprint.capture.FingerprintCaptureResult @@ -25,6 +26,8 @@ internal class CreateEnrolResponseUseCase @Inject constructor( ): AppResponse { val fingerprintCapture = results.filterIsInstance(FingerprintCaptureResult::class.java).lastOrNull() val faceCapture = results.filterIsInstance(FaceCaptureResult::class.java).lastOrNull() + // TODO [CORE-3421] When an external credential can be extracted from the UI-level steps, extract it here + val externalCredential: ExternalCredential? = null return try { val subject = subjectFactory.buildSubjectFromCaptureResults( @@ -33,6 +36,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( moduleId = request.moduleId, fingerprintResponse = fingerprintCapture, faceResponse = faceCapture, + externalCredential = externalCredential, ) enrolSubject(subject, project) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt index 476ef8afec..4112ef3859 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt @@ -49,7 +49,14 @@ internal class CreateEnrolResponseUseCaseTest { @Test fun `Converts correct results to response`() = runTest { every { - subjectFactory.buildSubjectFromCaptureResults(any(), any(), any(), any(), any()) + subjectFactory.buildSubjectFromCaptureResults( + projectId = any(), + attendantId = any(), + moduleId = any(), + fingerprintResponse = any(), + faceResponse = any(), + externalCredential = any() + ) } returns mockk { every { subjectId } returns "guid" } assertThat( @@ -68,7 +75,14 @@ internal class CreateEnrolResponseUseCaseTest { @Test fun `Returns error if no valid response`() = runTest { every { - subjectFactory.buildSubjectFromCaptureResults(any(), any(), any(), null, null) + subjectFactory.buildSubjectFromCaptureResults( + projectId = any(), + attendantId = any(), + moduleId = any(), + fingerprintResponse = null, + faceResponse = null, + externalCredential = null + ) } throws MissingCaptureException() assertThat(useCase(action, emptyList(), project)).isInstanceOf(AppErrorResponse::class.java) diff --git a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt index 968f04c4dc..e38b099a26 100644 --- a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt +++ b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt @@ -164,9 +164,9 @@ class ProjectAuthenticatorTest { } returns Token("", "", "", "") coEvery { configManager.getProjectConfiguration() } returns ProjectConfiguration( - "id", - PROJECT_ID, - "", + id = "id", + projectId = PROJECT_ID, + updatedAt = "", general = GeneralConfiguration( modalities = mockk(), matchingModalities = mockk(), @@ -176,12 +176,13 @@ class ProjectAuthenticatorTest { duplicateBiometricEnrolmentCheck = false, settingsPassword = mockk(), ), - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), + face = mockk(), + fingerprint = mockk(), + consent = mockk(), + identification = mockk(), + synchronization = mockk(), + multifactorId = mockk(), + custom = mockk(), ) coEvery { configManager.getPrivacyNotice(any(), any()) } returns emptyFlow() diff --git a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt index 5ba9343990..af8a2c6dc8 100644 --- a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt +++ b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt @@ -227,16 +227,17 @@ internal class SignerManagerTest { tokenizationKeys = emptyMap(), ), ProjectConfiguration( - "id", - DEFAULT_PROJECT_ID, - "", - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), - null, + id = "id", + projectId = DEFAULT_PROJECT_ID, + updatedAt = "", + general = mockk(), + face = mockk(), + fingerprint = mockk(), + consent = mockk(), + identification = mockk(), + synchronization = mockk(), + multifactorId = mockk(), + custom = mockk(), ), ), ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt index 633ab8e1f3..69ebd618fa 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt @@ -238,6 +238,7 @@ internal class ConfigLocalDataSourceImpl @Inject constructor( ), ), custom = null, + multifactorId = null ).toProto() val defaultDeviceConfiguration: ProtoDeviceConfiguration = DeviceConfiguration( language = "", diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt index 75352715fc..38d5957162 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt @@ -75,6 +75,7 @@ internal data class OldProjectConfig( consent = consentConfiguration(), identification = identificationConfiguration(), synchronization = synchronizationConfiguration(), + multifactorId = null, custom = null, ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ExternalCredentialType.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ExternalCredentialType.kt new file mode 100644 index 0000000000..c2f20a6014 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ExternalCredentialType.kt @@ -0,0 +1,18 @@ +package com.simprints.infra.config.store.local.models + +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException + +internal fun ExternalCredentialType.toProto(): ProtoExternalCredentialType = when(this){ + ExternalCredentialType.NHISCard -> ProtoExternalCredentialType.NHIS_CARD + ExternalCredentialType.GhanaIdCard -> ProtoExternalCredentialType.GHANA_ID_CARD + ExternalCredentialType.QRCode -> ProtoExternalCredentialType.QR_CODE +} + +internal fun ProtoExternalCredentialType.toDomain(): ExternalCredentialType = when(this){ + ProtoExternalCredentialType.UNRECOGNIZED, + ProtoExternalCredentialType.EXTERNAL_CREDENTIAL_TYPE_UNSPECIFIED -> throw InvalidProtobufEnumException("invalid External credential $name") + ProtoExternalCredentialType.NHIS_CARD -> ExternalCredentialType.NHISCard + ProtoExternalCredentialType.GHANA_ID_CARD -> ExternalCredentialType.GhanaIdCard + ProtoExternalCredentialType.QR_CODE -> ExternalCredentialType.QRCode +} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/MutliFactorIdConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/MutliFactorIdConfiguration.kt new file mode 100644 index 0000000000..100fa4afe0 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/MutliFactorIdConfiguration.kt @@ -0,0 +1,12 @@ +package com.simprints.infra.config.store.local.models + +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration + +internal fun MultiFactorIdConfiguration.toProto(): ProtoMultiFactorIdConfiguration = ProtoMultiFactorIdConfiguration + .newBuilder() + .addAllAllowedExternalCredentials(allowedExternalCredentials.map { it.toProto() }) + .build() + +internal fun ProtoMultiFactorIdConfiguration.toDomain(): MultiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = allowedExternalCredentialsList.map { it.toDomain() } +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt index 9579957ae7..93acadfa1a 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt @@ -15,6 +15,7 @@ internal fun ProjectConfiguration.toProto(): ProtoProjectConfiguration = ProtoPr .also { if (face != null) it.face = face.toProto() if (fingerprint != null) it.fingerprint = fingerprint.toProto() + if (multifactorId != null) it.multiFactorId = multifactorId.toProto() }.also { if (custom != null) { try { @@ -28,16 +29,17 @@ internal fun ProjectConfiguration.toProto(): ProtoProjectConfiguration = ProtoPr }.build() internal fun ProtoProjectConfiguration.toDomain(): ProjectConfiguration = ProjectConfiguration( - id, - projectId, - updatedAt, - general.toDomain(), - hasFace().let { if (it) face.toDomain() else null }, - hasFingerprint().let { if (it) fingerprint.toDomain() else null }, - consent.toDomain(), - identification.toDomain(), - synchronization.toDomain(), - customJson?.takeIf { it.isNotBlank() }?.let { + id = id, + projectId = projectId, + updatedAt = updatedAt, + general = general.toDomain(), + face = hasFace().let { if (it) face.toDomain() else null }, + fingerprint = hasFingerprint().let { if (it) fingerprint.toDomain() else null }, + consent = consent.toDomain(), + identification = identification.toDomain(), + synchronization = synchronization.toDomain(), + multifactorId = multiFactorId?.toDomain(), + custom = customJson?.takeIf { it.isNotBlank() }?.let { try { JsonHelper.fromJson(it) } catch (_: Exception) { diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt new file mode 100644 index 0000000000..f315afa5f9 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt @@ -0,0 +1,7 @@ +package com.simprints.infra.config.store.models + +import com.simprints.core.domain.externalcredential.ExternalCredentialType + +data class MultiFactorIdConfiguration( + val allowedExternalCredentials: List +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt index f9a97c7a52..acb5c4c235 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt @@ -14,5 +14,6 @@ data class Project( enum class TokenKeyType { AttendantId, ModuleId, + ExternalCredential, Unknown, } 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 de6e8d9aaf..88af76ad8b 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 @@ -10,6 +10,7 @@ data class ProjectConfiguration( val consent: ConsentConfiguration, val identification: IdentificationConfiguration, val synchronization: SynchronizationConfiguration, + val multifactorId: MultiFactorIdConfiguration?, val custom: Map?, ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt new file mode 100644 index 0000000000..47a1501851 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt @@ -0,0 +1,25 @@ +package com.simprints.infra.config.store.remote.models + +import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration + +@Keep +internal data class ApiMultiFactorIdConfiguration( + val allowedExternalCredentials: List +) { + fun toDomain(): MultiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = allowedExternalCredentials.map { it.toDomain() } + ) +} + +@Keep +enum class ApiExternalCredentialType { + NHISCard, GhanaIdCard, QRCode; + + fun toDomain(): ExternalCredentialType = when (this) { + NHISCard -> ExternalCredentialType.NHISCard + GhanaIdCard -> ExternalCredentialType.GhanaIdCard + QRCode -> ExternalCredentialType.QRCode + } +} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt index e4b589bcd7..696d4444fb 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt @@ -14,18 +14,20 @@ internal data class ApiProjectConfiguration( val consent: ApiConsentConfiguration, val identification: ApiIdentificationConfiguration, val synchronization: ApiSynchronizationConfiguration, + val multiFactorId: ApiMultiFactorIdConfiguration?, val custom: Map?, ) { fun toDomain(): ProjectConfiguration = ProjectConfiguration( - id, - projectId, - updatedAt, - general.toDomain(), - face?.toDomain(), - fingerprint?.toDomain(), - consent.toDomain(), - identification.toDomain(), - synchronization.toDomain(), - custom, + id = id, + projectId = projectId, + updatedAt = updatedAt, + general = general.toDomain(), + face = face?.toDomain(), + fingerprint = fingerprint?.toDomain(), + consent = consent.toDomain(), + identification = identification.toDomain(), + synchronization = synchronization.toDomain(), + multifactorId = multiFactorId?.toDomain(), + custom = custom, ) } diff --git a/infra/config-store/src/main/proto/project_config.proto b/infra/config-store/src/main/proto/project_config.proto index b03ea409ce..69615d2745 100644 --- a/infra/config-store/src/main/proto/project_config.proto +++ b/infra/config-store/src/main/proto/project_config.proto @@ -14,6 +14,18 @@ message ProtoProjectConfiguration { string updated_at = 8; optional string customJson = 9; string id = 10; + optional ProtoMultiFactorIdConfiguration multiFactorId = 11; +} + +enum ProtoExternalCredentialType { + EXTERNAL_CREDENTIAL_TYPE_UNSPECIFIED = 0; + NHIS_CARD = 1; + GHANA_ID_CARD = 2; + QR_CODE = 3; +} + +message ProtoMultiFactorIdConfiguration { + repeated ProtoExternalCredentialType allowed_external_credentials = 1; } message ProtoGeneralConfiguration { diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt index 68378448af..6b70f71501 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt @@ -17,6 +17,7 @@ import com.simprints.infra.config.store.testtools.consentConfiguration import com.simprints.infra.config.store.testtools.faceConfiguration import com.simprints.infra.config.store.testtools.generalConfiguration import com.simprints.infra.config.store.testtools.identificationConfiguration +import com.simprints.infra.config.store.testtools.multiFactorIdConfiguration import com.simprints.infra.config.store.testtools.project import com.simprints.infra.config.store.testtools.projectConfiguration import com.simprints.infra.config.store.testtools.synchronizationConfiguration @@ -173,16 +174,17 @@ class ConfigLocalDataSourceImplTest { fun `should save the project configuration and update the device configuration correctly with an empty list of fingersToCollect if fingerprint config is missing`() = runTest { val projectConfigurationToSave = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - faceConfiguration, - null, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - null, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = faceConfiguration, + fingerprint = null, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = null, ) configLocalDataSourceImpl.saveProjectConfiguration(projectConfigurationToSave) diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExternalCredentialTypeTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExternalCredentialTypeTest.kt new file mode 100644 index 0000000000..45d8d0e2d4 --- /dev/null +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExternalCredentialTypeTest.kt @@ -0,0 +1,43 @@ +package com.simprints.infra.config.store.models + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException +import com.simprints.infra.config.store.local.models.ProtoExternalCredentialType +import com.simprints.infra.config.store.local.models.toDomain +import com.simprints.infra.config.store.local.models.toProto +import com.simprints.testtools.common.syntax.assertThrows + +class ExternalCredentialTypeMapperTest { + + @Test + fun `should map correctly the model to proto`() { + val pairs = listOf( + ExternalCredentialType.NHISCard to ProtoExternalCredentialType.NHIS_CARD, + ExternalCredentialType.GhanaIdCard to ProtoExternalCredentialType.GHANA_ID_CARD, + ExternalCredentialType.QRCode to ProtoExternalCredentialType.QR_CODE + ) + + pairs.forEach { (domain, proto) -> + assertThat(proto).isEqualTo(domain.toProto()) + } + } + + @Test + fun `should map correctly the model from proto`() { + val pairs = listOf( + ProtoExternalCredentialType.NHIS_CARD to ExternalCredentialType.NHISCard, + ProtoExternalCredentialType.GHANA_ID_CARD to ExternalCredentialType.GhanaIdCard, + ProtoExternalCredentialType.QR_CODE to ExternalCredentialType.QRCode + ) + + pairs.forEach { (proto, domain) -> + assertThat(domain).isEqualTo(proto.toDomain()) + } + + assertThrows { + ProtoExternalCredentialType.UNRECOGNIZED.toDomain() + } + } +} diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt index f5b2a3e4e2..1e48265958 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt @@ -5,12 +5,14 @@ import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.testtools.apiConsentConfiguration import com.simprints.infra.config.store.testtools.apiGeneralConfiguration import com.simprints.infra.config.store.testtools.apiIdentificationConfiguration +import com.simprints.infra.config.store.testtools.apiMultiFactorIdConfiguration import com.simprints.infra.config.store.testtools.apiProjectConfiguration import com.simprints.infra.config.store.testtools.apiSynchronizationConfiguration import com.simprints.infra.config.store.testtools.consentConfiguration import com.simprints.infra.config.store.testtools.customKeyMap import com.simprints.infra.config.store.testtools.generalConfiguration import com.simprints.infra.config.store.testtools.identificationConfiguration +import com.simprints.infra.config.store.testtools.multiFactorIdConfiguration import com.simprints.infra.config.store.testtools.projectConfiguration import com.simprints.infra.config.store.testtools.synchronizationConfiguration import org.junit.Test @@ -24,28 +26,30 @@ class ApiProjectConfigurationTest { @Test fun `should map correctly the model when both fingerprint and face are missing`() { val apiProjectConfiguration = ApiProjectConfiguration( - "id", - "projectId", - "updatedAt", - apiGeneralConfiguration, - null, - null, - apiConsentConfiguration, - apiIdentificationConfiguration, - apiSynchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = apiGeneralConfiguration, + face = null, + fingerprint = null, + consent = apiConsentConfiguration, + identification = apiIdentificationConfiguration, + synchronization = apiSynchronizationConfiguration, + multiFactorId = apiMultiFactorIdConfiguration, + custom = customKeyMap, ) val projectConfiguration = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - null, - null, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = null, + fingerprint = null, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = customKeyMap, ) assertThat(apiProjectConfiguration.toDomain()).isEqualTo(projectConfiguration) 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 229040bc55..b141abc1b5 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 @@ -1,17 +1,20 @@ package com.simprints.infra.config.store.testtools +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.tokenization.asTokenizableEncrypted 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.ProtoDeviceConfiguration import com.simprints.infra.config.store.local.models.ProtoDownSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoExternalCredentialType import com.simprints.infra.config.store.local.models.ProtoFaceConfiguration import com.simprints.infra.config.store.local.models.ProtoFinger import com.simprints.infra.config.store.local.models.ProtoFingerprintConfiguration import com.simprints.infra.config.store.local.models.ProtoFingerprintConfiguration.ProtoMaxCaptureAttempts import com.simprints.infra.config.store.local.models.ProtoGeneralConfiguration import com.simprints.infra.config.store.local.models.ProtoIdentificationConfiguration +import com.simprints.infra.config.store.local.models.ProtoMultiFactorIdConfiguration import com.simprints.infra.config.store.local.models.ProtoProject import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration import com.simprints.infra.config.store.local.models.ProtoSampleSynchronizationConfiguration @@ -35,6 +38,7 @@ import com.simprints.infra.config.store.models.Frequency import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.IdentificationConfiguration import com.simprints.infra.config.store.models.MaxCaptureAttempts +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectState @@ -49,12 +53,14 @@ import com.simprints.infra.config.store.remote.models.ApiAllowedAgeRange import com.simprints.infra.config.store.remote.models.ApiConsentConfiguration import com.simprints.infra.config.store.remote.models.ApiDecisionPolicy import com.simprints.infra.config.store.remote.models.ApiDeviceState +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType import com.simprints.infra.config.store.remote.models.ApiFaceConfiguration import com.simprints.infra.config.store.remote.models.ApiFaceConfiguration.ApiFaceSdkConfiguration import com.simprints.infra.config.store.remote.models.ApiFingerprintConfiguration import com.simprints.infra.config.store.remote.models.ApiGeneralConfiguration import com.simprints.infra.config.store.remote.models.ApiIdentificationConfiguration import com.simprints.infra.config.store.remote.models.ApiMaxCaptureAttempts +import com.simprints.infra.config.store.remote.models.ApiMultiFactorIdConfiguration import com.simprints.infra.config.store.remote.models.ApiProject import com.simprints.infra.config.store.remote.models.ApiProjectConfiguration import com.simprints.infra.config.store.remote.models.ApiProjectState @@ -405,6 +411,16 @@ internal val synchronizationConfiguration = SynchronizationConfiguration( ), ) +internal val allowedExternalCredential = ExternalCredentialType.NHISCard + +internal val multiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = listOf(allowedExternalCredential) +) + +internal val protoMultiFactorIdConfiguration = ProtoMultiFactorIdConfiguration + .newBuilder() + .addAllowedExternalCredentials(ProtoExternalCredentialType.NHIS_CARD) + internal val protoSynchronizationConfiguration = ProtoSynchronizationConfiguration .newBuilder() .setUp( @@ -451,6 +467,12 @@ internal val protoSynchronizationConfiguration = ProtoSynchronizationConfigurati .build(), ).build() +internal val apiAllowedExternalCredential = ApiExternalCredentialType.NHISCard + +internal val apiMultiFactorIdConfiguration = ApiMultiFactorIdConfiguration( + allowedExternalCredentials = listOf(apiAllowedExternalCredential) +) + internal val customKeyMap: Map? = mapOf( "key1" to 7, "key2" to 4.2, @@ -460,29 +482,31 @@ internal val customKeyMap: Map? = mapOf( internal const val PROTO_CUSTOM_KEY_MAP_JSON = "{\"key1\":7,\"key2\":4.2,\"key3\":false,\"key4\":\"test\"}" internal val apiProjectConfiguration = ApiProjectConfiguration( - "id", - "projectId", - "updatedAt", - apiGeneralConfiguration, - apiFaceConfiguration, - apiFingerprintConfiguration, - apiConsentConfiguration, - apiIdentificationConfiguration, - apiSynchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = apiGeneralConfiguration, + face = apiFaceConfiguration, + fingerprint = apiFingerprintConfiguration, + consent = apiConsentConfiguration, + identification = apiIdentificationConfiguration, + synchronization = apiSynchronizationConfiguration, + multiFactorId = apiMultiFactorIdConfiguration, + custom = customKeyMap, ) internal val projectConfiguration = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - faceConfiguration, - fingerprintConfiguration, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = faceConfiguration, + fingerprint = fingerprintConfiguration, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = customKeyMap ) internal val protoProjectConfiguration = ProtoProjectConfiguration @@ -496,6 +520,7 @@ internal val protoProjectConfiguration = ProtoProjectConfiguration .setConsent(protoConsentConfiguration) .setIdentification(protoIdentificationConfiguration) .setSynchronization(protoSynchronizationConfiguration) + .setMultiFactorId(protoMultiFactorIdConfiguration) .setCustomJson(PROTO_CUSTOM_KEY_MAP_JSON) .build() diff --git a/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt new file mode 100644 index 0000000000..478508ce21 --- /dev/null +++ b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt @@ -0,0 +1,14 @@ +package com.simprints.core.domain.externalcredential + +import android.os.Parcelable +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.tokenization.TokenizableString +import kotlinx.parcelize.Parcelize + +@Parcelize +@ExcludedFromGeneratedTestCoverageReports("Data class with generated code") +data class ExternalCredential( + val value: TokenizableString.Tokenized, + val subjectId: String, + val type: ExternalCredentialType, +) : Parcelable diff --git a/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredentialType.kt b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredentialType.kt new file mode 100644 index 0000000000..1481ce4af3 --- /dev/null +++ b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredentialType.kt @@ -0,0 +1,5 @@ +package com.simprints.core.domain.externalcredential + +enum class ExternalCredentialType { + NHISCard, GhanaIdCard, QRCode +} diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt index 5a967181e1..ff6e0b7881 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt @@ -4,6 +4,7 @@ import androidx.annotation.Keep import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.enrolment.records.realm.store.BuildConfig import com.simprints.infra.enrolment.records.realm.store.migration.RealmMigrations +import com.simprints.infra.enrolment.records.realm.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.realm.store.models.DbFaceSample import com.simprints.infra.enrolment.records.realm.store.models.DbFingerprintSample import com.simprints.infra.enrolment.records.realm.store.models.DbProject @@ -26,6 +27,7 @@ class RealmConfig @Inject constructor() { DbFaceSample::class, DbSubject::class, DbProject::class, + DbExternalCredential::class, ), ).name("$databaseName.realm") .schemaVersion(REALM_SCHEMA_VERSION) @@ -36,6 +38,6 @@ class RealmConfig @Inject constructor() { .build() companion object { - private const val REALM_SCHEMA_VERSION: Long = 17 + private const val REALM_SCHEMA_VERSION: Long = 18 } } diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt new file mode 100644 index 0000000000..d076cca478 --- /dev/null +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt @@ -0,0 +1,21 @@ +package com.simprints.infra.enrolment.records.realm.store.models + +import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +@Keep +@ExcludedFromGeneratedTestCoverageReports("Data model definition for Realm table") +class DbExternalCredential : RealmObject { + @PrimaryKey + var id: String = "" + get() = "$value$SEPARATOR$subjectId" + var value: String = "" + var subjectId: String = "" + var type: String = "" + + companion object { + const val SEPARATOR = "|" + } +} diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt index 612e8bb8a7..4824b59a16 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt @@ -22,6 +22,7 @@ class DbSubject : RealmObject { var fingerprintSamples: RealmList = realmListOf() var faceSamples: RealmList = realmListOf() + var externalCredentials: RealmList = realmListOf() var isAttendantIdTokenized: Boolean = false var isModuleIdTokenized: Boolean = false diff --git a/infra/enrolment-records/repository/build.gradle.kts b/infra/enrolment-records/repository/build.gradle.kts index e87c549e8a..085e5cbe1f 100644 --- a/infra/enrolment-records/repository/build.gradle.kts +++ b/infra/enrolment-records/repository/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(libs.libsimprints) implementation(libs.retrofit.core) implementation(libs.jackson.core) + implementation(libs.testing.androidX.room) } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt index 23d307709b..3cc6060579 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt @@ -1,6 +1,7 @@ package com.simprints.infra.enrolment.records.repository.domain.models import android.os.Parcelable +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.tokenization.TokenizableString @@ -17,4 +18,5 @@ data class Subject( val updatedAt: Date? = null, var fingerprintSamples: List = emptyList(), var faceSamples: List = emptyList(), + var externalCredentials: List = emptyList(), ) : Parcelable diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt index 421a6f11c6..7953eac800 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt @@ -1,6 +1,7 @@ package com.simprints.infra.enrolment.records.repository.domain.models import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample @@ -14,6 +15,7 @@ sealed class SubjectAction { val subjectId: String, val faceSamplesToAdd: List, val fingerprintSamplesToAdd: List, + val externalCredentialsToAdd: List, val referenceIdsToRemove: List, ) : SubjectAction() diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt index 73f3af0d43..2151ecb31e 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt @@ -17,4 +17,5 @@ data class SubjectQuery( val sort: Boolean = false, val afterSubjectId: String? = null, val metadata: String? = null, + val externalCredential: String? = null, ) : StepParams diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt index b3b2e3037d..1b751691a4 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt @@ -211,6 +211,12 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( .filterNot { it.id in faceSampleIds } .takeIf { it.isNotEmpty() } ?.forEach { realm.delete(it) } + + val externalCredentialIds = newSubject.externalCredentials.map { it.id }.toSet() + dbSubject.externalCredentials + .filterNot { it.id in externalCredentialIds } + .takeIf { it.isNotEmpty() } + ?.forEach { realm.delete(it) } } realm.copyToRealm(newSubject, updatePolicy = UpdatePolicy.ALL) @@ -222,14 +228,16 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( val referencesToDelete = action.referenceIdsToRemove.toSet() // to make lookup O(1) val faceSamplesMap = dbSubject.faceSamples.groupBy { it.referenceId in referencesToDelete } val fingerprintSamplesMap = dbSubject.fingerprintSamples.groupBy { it.referenceId in referencesToDelete } + val allExternalCredentials = (dbSubject.externalCredentials + action.externalCredentialsToAdd.map { it.toRealmDb() }).distinctBy { it.id }.toSet() // Append new samples to the list of samples that remain after removing dbSubject.faceSamples = ( faceSamplesMap[false].orEmpty() + action.faceSamplesToAdd.map { it.toRealmDb() } - ).toRealmList() + ).toRealmList() dbSubject.fingerprintSamples = ( fingerprintSamplesMap[false].orEmpty() + action.fingerprintSamplesToAdd.map { it.toRealmDb() } - ).toRealmList() + ).toRealmList() + dbSubject.externalCredentials = allExternalCredentials.toRealmList() faceSamplesMap[true]?.forEach { realm.delete(it) } fingerprintSamplesMap[true]?.forEach { realm.delete(it) } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt index 61aed68169..bf76f891a5 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt @@ -289,6 +289,10 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( val dbFaces = samples.map { it.toRoomDb(subject.subjectId) } subjectDao.insertBiometricSamples(dbFaces) } + subject.externalCredentials.takeIf { it.isNotEmpty() }?.let { credentials -> + val dbExternalCredentials = credentials.map { it.toRoomDb() } + subjectDao.insertExternalCredentials(dbExternalCredentials) + } } private suspend fun updateSubject(action: SubjectAction.Update) { @@ -314,6 +318,9 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( if (templatesToAdd.isNotEmpty()) { subjectDao.insertBiometricSamples(templatesToAdd) } + if (action.externalCredentialsToAdd.isNotEmpty()) { + subjectDao.insertExternalCredentials(action.externalCredentialsToAdd.map { it.toRoomDb() }) + } } else { Simber.e( "[updateSubject] Subject ${action.subjectId} not found for update", diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt new file mode 100644 index 0000000000..b002d06ac6 --- /dev/null +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt @@ -0,0 +1,10 @@ +package com.simprints.infra.enrolment.records.repository.local.models + +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.infra.enrolment.records.realm.store.models.DbExternalCredential as RealmExternalCredential + +internal fun ExternalCredential.toRealmDb(): RealmExternalCredential = RealmExternalCredential().also { sample -> + sample.value = value.value + sample.subjectId = subjectId + sample.type = type.toString() +} diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt new file mode 100644 index 0000000000..9a1822ffaa --- /dev/null +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt @@ -0,0 +1,19 @@ +package com.simprints.infra.enrolment.records.repository.local.models + +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential + +internal fun DbExternalCredential.toDomain(): ExternalCredential = ExternalCredential( + value = value.asTokenizableEncrypted(), + subjectId = subjectId, + type = ExternalCredentialType.valueOf(type) +) + +internal fun ExternalCredential.toRoomDb(): DbExternalCredential = DbExternalCredential( + value = value.value, + subjectId = subjectId, + type = type.name +) + diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt index 385e341830..456b54b7e6 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt @@ -15,6 +15,7 @@ internal fun SubjectBiometrics.toDomain() = Subject( updatedAt = subject.updatedAt?.toDate(), fingerprintSamples = biometricTemplates.filter { it.modality == Modality.FINGERPRINT.id }.map { it.toFingerprintSample() }, faceSamples = biometricTemplates.filter { it.modality == Modality.FACE.id }.map { it.toFaceSample() }, + externalCredentials = externalCredentials.map { it.toDomain() }, ) fun Long.toDate() = Date(this) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt index 8508074a68..dd6bdc4ff4 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt @@ -1,6 +1,8 @@ package com.simprints.infra.enrolment.records.repository.local import com.google.common.truth.Truth.* +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier @@ -316,6 +318,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(getRandomFaceSample()), fingerprintSamplesToAdd = listOf(getRandomFingerprintSample()), referenceIdsToRemove = listOf(faceReferenceId, fingerReferenceId), + externalCredentialsToAdd = listOf(), ), ), project, @@ -430,6 +433,9 @@ class RealmEnrolmentRecordLocalDataSourceTest { getRandomFaceSample(), ), fingerprintSamples: List = listOf(), + externalCredentials: List = listOf( + getRandomExternalCredential() + ), ): Subject = Subject( subjectId = patientId, projectId = projectId, @@ -437,6 +443,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { moduleId = moduleId.asTokenizableRaw(), faceSamples = faceSamples, fingerprintSamples = fingerprintSamples, + externalCredentials = externalCredentials ) private fun getRandomFaceSample( @@ -448,4 +455,10 @@ class RealmEnrolmentRecordLocalDataSourceTest { id: String = UUID.randomUUID().toString(), referenceId: String = "referenceId", ) = FingerprintSample(IFingerIdentifier.LEFT_3RD_FINGER, Random.nextBytes(64), "fingerprintTemplateFormat", referenceId, id) + + private fun getRandomExternalCredential() = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt index c916779534..5a37523cff 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt @@ -3,6 +3,8 @@ package com.simprints.infra.enrolment.records.repository.local import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier @@ -13,6 +15,7 @@ import com.simprints.infra.enrolment.records.repository.domain.models.BiometricD import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery +import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase.Companion.SUBJECT_DB_VERSION import com.simprints.infra.enrolment.records.room.store.SubjectsDatabaseFactory import com.simprints.infra.security.keyprovider.LocalDbKey import com.simprints.testtools.common.coroutines.TestCoroutineRule @@ -65,6 +68,14 @@ class RoomEnrolmentRecordLocalDataSourceTest { // --- Test Data --- private val date = Date() // Use a fixed date for consistent timestamps in tests + // External credentials + private val externalCredential = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) + + // Samples defined first private val faceSample1 = FaceSample( template = byteArrayOf(1, 2, 3), @@ -116,6 +127,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = emptyList(), createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-001")) ) private val subject2P1WithFinger = Subject( subjectId = "subj-002", @@ -126,6 +138,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = listOf(fingerprintSample1), createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-002")) ) private val subject3P1WithBoth = Subject( subjectId = "subj-003", @@ -134,6 +147,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { moduleId = MODULE_2_ID, faceSamples = listOf(faceSample2), fingerprintSamples = listOf(fingerprintSample2), + externalCredentials = listOf(getExternalCredential("subj-003")) ) private val subject4P2WithBoth = Subject( subjectId = "subj-004", @@ -144,6 +158,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = listOf(fingerprintSample3), createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-004")) ) private val subject5P2WithFace = Subject( // Added subject @@ -155,6 +170,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = emptyList(), createdAt = Date(date.time + 1000), // Slightly different time updatedAt = Date(date.time + 1000), + externalCredentials = listOf(getExternalCredential("subj-005")) ) private val subject6P2WithFinger = Subject( // Added subject @@ -166,6 +182,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = listOf(fingerprintSample3.copy(id = UUID.randomUUID().toString())), createdAt = Date(date.time + 2000), // Different time updatedAt = Date(date.time + 2000), + externalCredentials = listOf(getExternalCredential("subj-006")) ) private val subjectInvalidNoSamples = Subject( subjectId = "subj-invalid", @@ -174,6 +191,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { moduleId = MODULE_1_ID, createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-invalid")) ) private val project: Project = mockk() @@ -213,6 +231,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { SubjectAction.Creation(subject6P2WithFinger), ) + private fun getExternalCredential(subjectId: String) = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = subjectId, + type = ExternalCredentialType.NHISCard + ) + private suspend fun setupInitialData() { dataSource.performActions( initialData, @@ -334,6 +358,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(faceSample2), fingerprintSamplesToAdd = listOf(fingerprintSample1), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) // When @@ -365,6 +390,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), // Explicitly empty as in original fingerprintSamplesToAdd = listOf(), // Explicitly empty as in original referenceIdsToRemove = listOf(faceSample2.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -394,6 +420,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(fingerprintSample2.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -422,6 +449,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(faceSample1.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -445,6 +473,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(fingerprintSample1.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -464,6 +493,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(faceSample1, faceSample2), fingerprintSamplesToAdd = listOf(fingerprintSample1), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) // When @@ -493,6 +523,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(faceSample1), // Try to add samples fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) // When @@ -601,6 +632,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(fingerprintSample1), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) dataSource.performActions(listOf(updateAction), project) loadedSubject = @@ -1453,7 +1485,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { val result = dataSource.getLocalDBInfo() // Then assertThat(result).contains("Database Name: db-subjects") - assertThat(result).contains("Database Version: 1") + assertThat(result).contains("Database Version: $SUBJECT_DB_VERSION") assertThat(result).contains("Is Encrypted: false") // db not encrypted in tests assertThat(result).contains("Number of Subjects: 6") } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt new file mode 100644 index 0000000000..5cda6f65fe --- /dev/null +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt @@ -0,0 +1,42 @@ +package com.simprints.infra.enrolment.records.room.store.migration + +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.* +import androidx.test.platform.app.* +import com.google.common.truth.Truth.* +import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase +import org.junit.Rule +import org.junit.runner.RunWith +import kotlin.test.Test + +@RunWith(AndroidJUnit4::class) +class Migration1to2Test { + + @get:Rule + val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + SubjectsDatabase::class.java, + ) + + @Test + fun `when migrating from 1 to 2 then external credential table should be added`() { + val db1 = helper.createDatabase(name = TEST_DB, version = 1) + val db2 = helper.runMigrationsAndValidate( + name = TEST_DB, + version = 2, + validateDroppedTables = true, + MIGRATION_1_2 + ) + + // Verify external credentials table exists + val cursor = db2.query("SELECT name FROM sqlite_master WHERE name='DbExternalCredential'") + assertThat(cursor.count).isEqualTo(1) + cursor.close() + db1.close() + db2.close() + } + + companion object { + private const val TEST_DB = "migration-test" + } +} diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json index bbe29315e1..4df59295d1 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "94bee827928a2618c6873579bc6bc63a", + "identityHash": "527fcc2c704906558681cb31beddb0c3", "entities": [ { "tableName": "DbSubject", @@ -170,11 +170,55 @@ ] } ] + }, + { + "tableName": "DbExternalCredential", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "value", + "subjectId" + ] + }, + "foreignKeys": [ + { + "table": "DbSubject", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "subjectId" + ], + "referencedColumns": [ + "subjectId" + ] + } + ] } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '94bee827928a2618c6873579bc6bc63a')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '527fcc2c704906558681cb31beddb0c3')" ] } -} \ No newline at end of file +} diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt index d7836eb8cf..ffdba547e4 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt @@ -8,6 +8,7 @@ import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SupportSQLiteQuery import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject import com.simprints.infra.enrolment.records.room.store.models.SubjectBiometrics @@ -19,12 +20,18 @@ interface SubjectDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertBiometricSamples(samples: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertExternalCredentials(value: List) + @Query("DELETE FROM DbSubject WHERE subjectId = :subjectId") suspend fun deleteSubject(subjectId: String) @Query("DELETE FROM DbBiometricTemplate WHERE uuid = :uuid") suspend fun deleteBiometricSample(uuid: String) + @Query("DELETE FROM DbExternalCredential WHERE value = :value") + suspend fun deleteExternalCredential(value: String) + @RawQuery suspend fun deleteSubjects(query: SupportSQLiteQuery): Int @@ -34,6 +41,9 @@ interface SubjectDao { @RawQuery suspend fun loadSubjects(query: SupportSQLiteQuery): List + @Query("SELECT * FROM DbExternalCredential") + suspend fun getAllExternalCredentials(): List + @RawQuery suspend fun countSubjects(query: SupportSQLiteQuery): Int diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt index 16f004f62d..422c0dc33c 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt @@ -5,7 +5,10 @@ import androidx.annotation.Keep import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase.Companion.SUBJECT_DB_VERSION +import com.simprints.infra.enrolment.records.room.store.migration.MIGRATION_1_2 import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject import net.zetetic.database.sqlcipher.SupportOpenHelperFactory import javax.inject.Singleton @@ -15,8 +18,9 @@ import javax.inject.Singleton entities = [ DbSubject::class, DbBiometricTemplate::class, + DbExternalCredential::class, ], - version = 1, + version = SUBJECT_DB_VERSION, exportSchema = true, ) @Keep @@ -29,11 +33,14 @@ abstract class SubjectsDatabase : RoomDatabase() { factory: SupportOpenHelperFactory, dbName: String, ): SubjectsDatabase { - val builder = Room.databaseBuilder(context, SubjectsDatabase::class.java, dbName) + val builder = Room + .databaseBuilder(context, SubjectsDatabase::class.java, dbName) + .addMigrations(MIGRATION_1_2) if (BuildConfig.DB_ENCRYPTION) { builder.openHelperFactory(factory) } return builder.build() } + const val SUBJECT_DB_VERSION = 2 } } diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt new file mode 100644 index 0000000000..106bbdc2a3 --- /dev/null +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt @@ -0,0 +1,27 @@ +package com.simprints.infra.enrolment.records.room.store.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +/** + * Schema version 1 -> 2 + * + * Changes: + * - Adding [DbExternalCredential] entity + * */ +val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `DbExternalCredential` ( + `value` TEXT NOT NULL, + `subjectId` TEXT NOT NULL, + `type` TEXT NOT NULL, + PRIMARY KEY(`value`, `subjectId`), + FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON DELETE CASCADE + ) + """.trimIndent() + ) + } +} + diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt index 7590c579bd..91c0075666 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt @@ -39,3 +39,4 @@ data class DbBiometricTemplate( const val FORMAT_COLUMN = "format" } } + diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt new file mode 100644 index 0000000000..f476ef190c --- /dev/null +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt @@ -0,0 +1,33 @@ +package com.simprints.infra.enrolment.records.room.store.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_TABLE_NAME +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_VALUE_COLUMN +import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Companion.SUBJECT_ID_COLUMN + +@Entity( + tableName = EXTERNAL_CREDENTIAL_TABLE_NAME, + primaryKeys = [EXTERNAL_CREDENTIAL_VALUE_COLUMN, SUBJECT_ID_COLUMN], + foreignKeys = [ + ForeignKey( + entity = DbSubject::class, + parentColumns = [SUBJECT_ID_COLUMN], + childColumns = [SUBJECT_ID_COLUMN], + onDelete = ForeignKey.CASCADE, + ) + ] +) +data class DbExternalCredential( + @ColumnInfo(name = EXTERNAL_CREDENTIAL_VALUE_COLUMN) + val value: String, + @ColumnInfo(name = SUBJECT_ID_COLUMN) + val subjectId: String, + val type: String, +) { + companion object { + const val EXTERNAL_CREDENTIAL_VALUE_COLUMN = "value" + const val EXTERNAL_CREDENTIAL_TABLE_NAME = "DbExternalCredential" + } +} diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt index 1ea2f722d2..d35bff3420 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt @@ -11,4 +11,9 @@ data class SubjectBiometrics( entityColumn = SUBJECT_ID_COLUMN, ) val biometricTemplates: List, + @Relation( + parentColumn = SUBJECT_ID_COLUMN, + entityColumn = SUBJECT_ID_COLUMN, + ) + val externalCredentials: List, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt index 92fd4a0598..f18557c181 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt @@ -25,6 +25,7 @@ internal data class ApiEnrolmentPayloadV2( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "attendantId" TokenKeyType.ModuleId -> "moduleId" + TokenKeyType.ExternalCredential -> "externalCredential" TokenKeyType.Unknown -> null } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt index 519e6ed6df..28368915c6 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt @@ -1,10 +1,12 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 @Keep +@ExcludedFromGeneratedTestCoverageReports("Data class") internal data class ApiEnrolmentPayloadV4( override val startTime: ApiTimestamp, val subjectId: String, @@ -25,6 +27,7 @@ internal data class ApiEnrolmentPayloadV4( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "attendantId" TokenKeyType.ModuleId -> "moduleId" + TokenKeyType.ExternalCredential -> "externalCredential" TokenKeyType.Unknown -> null } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt index 0475985a95..45441ef474 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt @@ -3,6 +3,7 @@ package com.simprints.infra.eventsync.event.remote.models.callout import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.callout.ConfirmationCalloutEventV2.ConfirmationCalloutPayload import com.simprints.infra.events.event.domain.models.callout.EnrolmentCalloutEventV2.EnrolmentCalloutPayload @@ -15,6 +16,7 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @Keep @JsonInclude(Include.NON_NULL) +@ExcludedFromGeneratedTestCoverageReports("Data class") internal data class ApiCalloutPayloadV2( override val startTime: ApiTimestamp, val callout: ApiCallout, @@ -73,6 +75,7 @@ internal data class ApiCalloutPayloadV2( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "callout.userId" TokenKeyType.ModuleId -> "callout.moduleId" + TokenKeyType.ExternalCredential -> "callout.externalCredential" TokenKeyType.Unknown -> null } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt index ab9f1a7cae..373e28cd0f 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt @@ -3,6 +3,7 @@ package com.simprints.infra.eventsync.event.remote.models.callout import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.callout.ConfirmationCalloutEventV3.ConfirmationCalloutPayload import com.simprints.infra.events.event.domain.models.callout.EnrolmentCalloutEventV3.EnrolmentCalloutPayload @@ -15,6 +16,7 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @Keep @JsonInclude(Include.NON_NULL) +@ExcludedFromGeneratedTestCoverageReports("Data class") internal data class ApiCalloutPayloadV3( override val startTime: ApiTimestamp, val callout: ApiCallout, @@ -76,6 +78,7 @@ internal data class ApiCalloutPayloadV3( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "callout.userId" TokenKeyType.ModuleId -> "callout.moduleId" + TokenKeyType.ExternalCredential -> "callout.externalCredential" TokenKeyType.Unknown -> null } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt index 94f749f810..dc5b0e7178 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt @@ -4,6 +4,7 @@ import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.ApiBiometricReference import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fromApiToDomain @@ -16,12 +17,14 @@ internal data class ApiEnrolmentRecordCreationPayload( val moduleId: String, val attendantId: String, val biometricReferences: List?, + val externalCredential: ApiExternalCredential?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordCreation) internal fun ApiEnrolmentRecordCreationPayload.fromApiToDomain() = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( - subjectId, - projectId, - moduleId.asTokenizableEncrypted(), - attendantId.asTokenizableEncrypted(), - biometricReferences?.map { it.fromApiToDomain() } ?: emptyList(), + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId.asTokenizableEncrypted(), + attendantId = attendantId.asTokenizableEncrypted(), + biometricReferences = biometricReferences?.map { it.fromApiToDomain() } ?: emptyList(), + externalCredentials = externalCredential?.let { listOf(it.fromApiToDomain(subjectId)) } ?: emptyList() ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt index 87a8b7d67a..53e5492ab7 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt @@ -31,25 +31,27 @@ internal data class ApiEnrolmentRecordMovePayload( val moduleId: String, val attendantId: String, val biometricReferences: List?, + val externalCredential: ApiExternalCredential?, ) } internal fun ApiEnrolmentRecordMovePayload.fromApiToDomain() = EnrolmentRecordMoveEvent.EnrolmentRecordMovePayload( with(enrolmentRecordCreation) { EnrolmentRecordCreationInMove( - subjectId, - projectId, - moduleId.asTokenizableEncrypted(), - attendantId.asTokenizableEncrypted(), - biometricReferences?.map { it.fromApiToDomain() }, + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId.asTokenizableEncrypted(), + attendantId = attendantId.asTokenizableEncrypted(), + biometricReferences = biometricReferences?.map { it.fromApiToDomain() }, + externalCredential = externalCredential?.fromApiToDomain(subjectId) ) }, enrolmentRecordDeletion.let { EnrolmentRecordDeletionInMove( - it.subjectId, - it.projectId, - it.moduleId.asTokenizableEncrypted(), - it.attendantId.asTokenizableEncrypted(), + subjectId = it.subjectId, + projectId = it.projectId, + moduleId = it.moduleId.asTokenizableEncrypted(), + attendantId = it.attendantId.asTokenizableEncrypted(), ) }, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt index 57014fc4ef..414c9ab056 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt @@ -1,6 +1,7 @@ package com.simprints.infra.eventsync.event.remote.models.subject import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.ApiBiometricReference import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fromApiToDomain @@ -10,10 +11,13 @@ internal data class ApiEnrolmentRecordUpdatePayload( val subjectId: String, val biometricReferencesAdded: List?, val biometricReferencesRemoved: List?, + val externalCredentialAdded: ApiExternalCredential?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate) internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain() = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( subjectId, biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), biometricReferencesRemoved.orEmpty(), + externalCredentialAdded?.fromApiToDomain(subjectId), ) + diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt new file mode 100644 index 0000000000..e1890996e3 --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt @@ -0,0 +1,18 @@ +package com.simprints.infra.eventsync.event.remote.models.subject + +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.core.domain.tokenization.asTokenizableEncrypted + +data class ApiExternalCredential( + val id: String, + val type: String, + val value: String, +) + + +internal fun ApiExternalCredential.fromApiToDomain(subjectId: String) = ExternalCredential( + value = value.asTokenizableEncrypted(), + subjectId = subjectId, + type = ExternalCredentialType.valueOf(type) +) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt index 1539651746..f3e09e0c18 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt @@ -266,6 +266,7 @@ internal class EventDownSyncTask @Inject constructor( faceSamplesToAdd = subjectFactory.extractFaceSamplesFromBiometricReferences(biometricReferencesAdded), fingerprintSamplesToAdd = subjectFactory.extractFingerprintSamplesFromBiometricReferences(biometricReferencesAdded), referenceIdsToRemove = biometricReferencesRemoved, + externalCredentialsToAdd = externalCredentialAdded?.let { listOf(it) } ?: emptyList() ), ) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt index 8c5ce2496e..920e0cf22e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt @@ -1,5 +1,6 @@ package com.simprints.infra.eventsync.sync.down.tasks +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.tokenization.TokenizableString @@ -32,6 +33,7 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), + externalCredentials = payload.externalCredentials, ) } @@ -43,6 +45,7 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), + externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), ) } @@ -70,6 +73,7 @@ class SubjectFactory @Inject constructor( moduleId: TokenizableString, fingerprintResponse: FingerprintCaptureResult?, faceResponse: FaceCaptureResult?, + externalCredential: ExternalCredential?, ): Subject { val subjectId = UUID.randomUUID().toString() return buildSubject( @@ -80,6 +84,7 @@ class SubjectFactory @Inject constructor( createdAt = Date(timeHelper.now().ms), fingerprintSamples = fingerprintResponse?.let { extractFingerprintSamples(it) }.orEmpty(), faceSamples = faceResponse?.let { extractFaceSamples(it) }.orEmpty(), + externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), ) } @@ -92,6 +97,7 @@ class SubjectFactory @Inject constructor( updatedAt: Date? = null, fingerprintSamples: List = emptyList(), faceSamples: List = emptyList(), + externalCredentials: List = emptyList(), ) = Subject( subjectId = subjectId, projectId = projectId, @@ -101,6 +107,7 @@ class SubjectFactory @Inject constructor( updatedAt = updatedAt, fingerprintSamples = fingerprintSamples, faceSamples = faceSamples, + externalCredentials = externalCredentials ) private fun extractFingerprintSamples(fingerprintResponse: FingerprintCaptureResult) = diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt index 523977bddb..de5b9695a7 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt @@ -14,7 +14,8 @@ class ApiEnrolmentPayloadV2Test { when (it) { TokenKeyType.AttendantId -> assertThat(result).isEqualTo("attendantId") TokenKeyType.ModuleId -> assertThat(result).isEqualTo("moduleId") - else -> assertThat(result).isNull() + TokenKeyType.ExternalCredential -> assertThat(result).isEqualTo("externalCredential") + TokenKeyType.Unknown -> assertThat(result).isNull() } } } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt index 72bda216d3..769eee6696 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt @@ -1,6 +1,8 @@ package com.simprints.infra.eventsync.event.remote.models.subject import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent @@ -14,11 +16,11 @@ class ApiEnrolmentRecordCreationEventTest { @Test fun convert_EnrolmentRecordCreationEvent() { val apiPayload = ApiEnrolmentRecordCreationPayload( - "subjectId", - "projectId", - "moduleId", - "attendantId", - listOf( + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId", + attendantId = "attendantId", + biometricReferences = listOf( ApiFingerprintReference( "fpRefId", listOf( @@ -27,13 +29,18 @@ class ApiEnrolmentRecordCreationEventTest { "NEC_1", ), ), + externalCredential = ApiExternalCredential( + id = "id", + type = ExternalCredentialType.NHISCard.toString(), + value = "value" + ), ) val expectedPayload = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( - "subjectId", - "projectId", - "moduleId".asTokenizableEncrypted(), - "attendantId".asTokenizableEncrypted(), - listOf( + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId".asTokenizableEncrypted(), + attendantId = "attendantId".asTokenizableEncrypted(), + biometricReferences = listOf( FingerprintReference( "fpRefId", listOf( @@ -42,6 +49,13 @@ class ApiEnrolmentRecordCreationEventTest { "NEC_1", ), ), + externalCredentials = listOf( + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) + ), ) assertThat(apiPayload.fromApiToDomain()).isEqualTo(expectedPayload) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt index c1ae48e35e..8c13aa8acb 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt @@ -1,6 +1,8 @@ package com.simprints.infra.eventsync.event.remote.models.subject import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent @@ -28,6 +30,11 @@ class ApiEnrolmentRecordMoveEventTest { "NEC_1", ), ), + ApiExternalCredential( + value = "value", + id = "subjectId", + type = ExternalCredentialType.NHISCard.toString() + ) ), ApiEnrolmentRecordMovePayload.ApiEnrolmentRecordDeletionInMove( "subjectId", @@ -51,6 +58,11 @@ class ApiEnrolmentRecordMoveEventTest { "NEC_1", ), ), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt index 030c7bf493..e3ed24eef0 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt @@ -1,7 +1,10 @@ package com.simprints.infra.eventsync.event.remote.models.subject import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.events.event.domain.models.subject.FaceReference import com.simprints.infra.events.event.domain.models.subject.FaceTemplate @@ -17,8 +20,8 @@ class ApiEnrolmentRecordUpdateEventTest { @Test fun convert_EnrolmentRecordUpdateEvent() { val apiPayload = ApiEnrolmentRecordUpdatePayload( - "subjectId", - listOf( + subjectId = "subjectId", + biometricReferencesAdded = listOf( ApiFingerprintReference( "fpRefId", listOf( @@ -32,11 +35,16 @@ class ApiEnrolmentRecordUpdateEventTest { "ROC_3", ), ), - listOf("fpRefId2"), + biometricReferencesRemoved = listOf("fpRefId2"), + externalCredentialAdded = ApiExternalCredential( + id = "id", + type = ExternalCredentialType.NHISCard.toString(), + value = "value" + ) ) val expectedPayload = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( - "subjectId", - listOf( + subjectId = "subjectId", + biometricReferencesAdded = listOf( FingerprintReference( "fpRefId", listOf(FingerprintTemplate("template", IFingerIdentifier.LEFT_THUMB)), @@ -48,7 +56,12 @@ class ApiEnrolmentRecordUpdateEventTest { "ROC_3", ), ), - listOf("fpRefId2"), + biometricReferencesRemoved = listOf("fpRefId2"), + externalCredentialAdded = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ) assertThat(apiPayload.fromApiToDomain()).isEqualTo(expectedPayload) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index a7d6b01de8..5de7881756 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt @@ -1,7 +1,10 @@ package com.simprints.infra.eventsync.sync.down.tasks import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException @@ -58,11 +61,18 @@ class EventDownSyncTaskTest { "attendantId", ) val ENROLMENT_RECORD_CREATION = EnrolmentRecordCreationEvent( - "subjectId", - "projectId", - "moduleId".asTokenizableRaw(), - "attendantId".asTokenizableRaw(), - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId".asTokenizableRaw(), + attendantId = "attendantId".asTokenizableRaw(), + biometricReferences = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + externalCredentials = listOf( + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) + ), ) val ENROLMENT_RECORD_MOVE_MODULE = EnrolmentRecordMoveEvent( EnrolmentRecordMoveEvent.EnrolmentRecordCreationInMove( @@ -71,6 +81,11 @@ class EventDownSyncTaskTest { DEFAULT_MODULE_ID_2, "attendantId".asTokenizableRaw(), listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -86,6 +101,11 @@ class EventDownSyncTaskTest { "moduleId".asTokenizableRaw(), DEFAULT_USER_ID, listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -101,6 +121,11 @@ class EventDownSyncTaskTest { "moduleId".asTokenizableRaw(), DEFAULT_USER_ID_2, listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -110,9 +135,14 @@ class EventDownSyncTaskTest { ), ) val ENROLMENT_RECORD_UPDATE = EnrolmentRecordUpdateEvent( - "subjectId", - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), - listOf("referenceIdToDelete"), + subjectId = "subjectId", + biometricReferencesAdded = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + biometricReferencesRemoved = listOf("referenceIdToDelete"), + externalCredentialAdded = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ) } @@ -519,6 +549,13 @@ class EventDownSyncTaskTest { faceSamples = listOf( FaceSample(byteArrayOf(), "format", "referenceId"), ), + externalCredentials = listOf( + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) + ) ), ) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt index 684a61f533..12f17d952f 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt @@ -1,9 +1,12 @@ package com.simprints.infra.eventsync.sync.down.tasks import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.utils.EncodingUtils @@ -61,6 +64,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, biometricReferences = listOf(FINGERPRINT_REFERENCE, faceReference), + externalCredentials = emptyList() ) val result = factory.buildSubjectFromCreationPayload(payload) val expected = Subject( @@ -95,6 +99,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, biometricReferences = listOf(FINGERPRINT_REFERENCE, faceReference), + externalCredential = null ) val result = factory.buildSubjectFromMovePayload(payload) @@ -177,6 +182,7 @@ class SubjectFactoryTest { templates = listOf(FaceTemplate(template = BASE_64_BYTES.toString())), ), ), + externalCredentialAdded = EXTERNAL_CREDENTIAL ) val result = factory.buildSubjectFromUpdatePayload(subject, payload) @@ -240,6 +246,7 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, ), ), + externalCredentials = listOf(EXTERNAL_CREDENTIAL) ) val result = factory.buildSubjectFromCaptureResults( @@ -277,6 +284,7 @@ class SubjectFactoryTest { ), ), ), + externalCredential = EXTERNAL_CREDENTIAL ) assertThat(result).isEqualTo(expected) } @@ -303,6 +311,7 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, ), ), + externalCredentials = listOf(EXTERNAL_CREDENTIAL) ) val result = factory.buildSubject( @@ -312,6 +321,7 @@ class SubjectFactoryTest { moduleId = expected.moduleId, fingerprintSamples = expected.fingerprintSamples, faceSamples = expected.faceSamples, + externalCredentials = expected.externalCredentials ) assertThat(result).isEqualTo(expected) } @@ -326,6 +336,8 @@ class SubjectFactoryTest { private const val REFERENCE_ID = "fpRefId" private const val REFERENCE_FORMAT = "NEC_1" private const val TEMPLATE_NAME = "template" + private val EXTERNAL_CREDENTIAL_VALUE = "value".asTokenizableEncrypted() + private val EXTERNAL_CREDENTIAL_TYPE = ExternalCredentialType.NHISCard private val IDENTIFIER = IFingerIdentifier.LEFT_THUMB private const val QUALITY = 10 private val FINGERPRINT_REFERENCE = FingerprintReference( @@ -343,5 +355,10 @@ class SubjectFactoryTest { templates = listOf(FaceTemplate(TEMPLATE_NAME)), format = REFERENCE_FORMAT, ) + private val EXTERNAL_CREDENTIAL = ExternalCredential( + value = EXTERNAL_CREDENTIAL_VALUE, + subjectId = SUBJECT_ID, + type = EXTERNAL_CREDENTIAL_TYPE + ) } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt index e3fc850615..df5939165b 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt @@ -50,11 +50,13 @@ class CoSyncEnrolmentRecordCreationEventDeserializer : return EnrolmentRecordCreationEvent( id, EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( - subjectId, - projectId, - moduleId, - attendantId, - biometricReferences, + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId, + attendantId = attendantId, + biometricReferences = biometricReferences, + // TODO [CORE-3421] Update when CoSync supports external credentials (MfID) + externalCredentials = emptyList() ), ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt index e583230b9b..3e17b20424 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt @@ -1,6 +1,8 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.tokenization.TokenizableString @@ -8,6 +10,7 @@ import com.simprints.core.tools.utils.EncodingUtils import java.util.UUID @Keep +@ExcludedFromGeneratedTestCoverageReports("Data class") data class EnrolmentRecordCreationEvent( override val id: String, val payload: EnrolmentRecordCreationPayload, @@ -18,14 +21,16 @@ data class EnrolmentRecordCreationEvent( moduleId: TokenizableString, attendantId: TokenizableString, biometricReferences: List, + externalCredentials: List, ) : this( UUID.randomUUID().toString(), EnrolmentRecordCreationPayload( - subjectId, - projectId, - moduleId, - attendantId, - biometricReferences, + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId, + attendantId = attendantId, + biometricReferences = biometricReferences, + externalCredentials = externalCredentials, ), ) @@ -36,6 +41,7 @@ data class EnrolmentRecordCreationEvent( val moduleId: TokenizableString, val attendantId: TokenizableString, val biometricReferences: List, + val externalCredentials: List ) companion object { diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt index 32dcd17da6..21392b5452 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt @@ -1,6 +1,7 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.tokenization.TokenizableString import java.util.UUID @@ -35,5 +36,6 @@ data class EnrolmentRecordMoveEvent( val moduleId: TokenizableString, val attendantId: TokenizableString, val biometricReferences: List?, + val externalCredential: ExternalCredential?, ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt index 4ca63c97f8..34c9196462 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt @@ -1,9 +1,12 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.externalcredential.ExternalCredential import java.util.UUID @Keep +@ExcludedFromGeneratedTestCoverageReports("Data class") data class EnrolmentRecordUpdateEvent( override val id: String, val payload: EnrolmentRecordUpdatePayload, @@ -12,12 +15,14 @@ data class EnrolmentRecordUpdateEvent( subjectId: String, biometricReferencesAdded: List, biometricReferencesRemoved: List, + externalCredentialAdded: ExternalCredential?, ) : this( UUID.randomUUID().toString(), EnrolmentRecordUpdatePayload( - subjectId, - biometricReferencesAdded, - biometricReferencesRemoved, + subjectId = subjectId, + biometricReferencesAdded = biometricReferencesAdded, + biometricReferencesRemoved = biometricReferencesRemoved, + externalCredentialAdded = externalCredentialAdded, ), ) @@ -26,5 +31,6 @@ data class EnrolmentRecordUpdateEvent( val subjectId: String, val biometricReferencesAdded: List, val biometricReferencesRemoved: List, + val externalCredentialAdded: ExternalCredential?, ) } diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt index aa055eb0b9..2810ef06ed 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt @@ -1,5 +1,6 @@ package com.simprints.infra.sync.config.testtools +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.ConsentConfiguration @@ -12,6 +13,7 @@ import com.simprints.infra.config.store.models.Frequency import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.IdentificationConfiguration import com.simprints.infra.config.store.models.MaxCaptureAttempts +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectState @@ -114,6 +116,12 @@ internal val simprintsDownSyncConfigurationConfiguration = DownSynchronizationCo frequency = Frequency.PERIODICALLY, ) +internal val allowedExternalCredential = ExternalCredentialType.NHISCard + +internal val multiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = listOf(allowedExternalCredential) +) + internal val synchronizationConfiguration = SynchronizationConfiguration( up = UpSynchronizationConfiguration( simprints = simprintsUpSyncConfigurationConfiguration, @@ -131,16 +139,17 @@ internal val identificationConfiguration = IdentificationConfiguration(4, IdentificationConfiguration.PoolType.PROJECT) internal val projectConfiguration = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - faceConfiguration, - fingerprintConfiguration, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - null, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = faceConfiguration, + fingerprint = fingerprintConfiguration, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = null ) internal const val TOKENIZATION_JSON =