diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCase.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCase.kt index a130c46137..38fda4066e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCase.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCase.kt @@ -19,15 +19,20 @@ internal class TokenizeEventPayloadFieldsUseCase @Inject constructor( project = project, ) } - val tokenizedListFields = event.getTokenizableListFields().mapValues { (tokenKeyType, listFieldValue) -> - listFieldValue.map { fieldValue -> - tokenizationProcessor.tokenizeIfNecessary( - tokenizableString = fieldValue, - tokenKeyType = tokenKeyType, - project = project, - ) + val tokenizedListFields = event + .getTokenizableListFields() + // Empty lists have nothing to tokenize, and propagating them would replace an absent + // field with an empty one that the backend then rejects as untokenized. + .filterValues { it.isNotEmpty() } + .mapValues { (tokenKeyType, listFieldValue) -> + listFieldValue.map { fieldValue -> + tokenizationProcessor.tokenizeIfNecessary( + tokenizableString = fieldValue, + tokenKeyType = tokenKeyType, + project = project, + ) + } } - } return event.setTokenizedFields(tokenizedFields).setTokenizedListFields(tokenizedListFields) } } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt index ac9bfd322a..2238e80ee3 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt @@ -639,6 +639,18 @@ internal class MapDomainEventToApiUseCaseTest { validateDeviceConfigurationUpdatedApiModel(json) } + @Test + fun `when no modules are selected, then downSyncModules should be omitted from the ApiEvent`() { + val event = createDeviceConfigurationUpdatedEvent().let { + it.copy(payload = it.payload.copy(configuration = it.payload.configuration.copy(downSyncModules = null))) + } + val apiEvent = useCase(event, project) + val json = JSONObject(SimJson.encodeToString(apiEvent)) + + assertThat(json.getJSONObject("payload").getJSONObject("configuration").has("downSyncModules")).isFalse() + assertThat(json.getJSONArray("tokenizedFields").length()).isEqualTo(0) + } + @Test fun `when event contains tokenized attendant id, then ApiEvent should contain tokenizedField`() { validateUserIdTokenization(attendantId = "attendantId".asTokenizableEncrypted()) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCaseTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCaseTest.kt index 8681e88490..430a904677 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCaseTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/TokenizeEventPayloadFieldsUseCaseTest.kt @@ -61,6 +61,17 @@ internal class TokenizeEventPayloadFieldsUseCaseTest { verify { event.setTokenizedListFields(mapOf(tokenKeyTypeList to listOf(tokenizedString, tokenizedString))) } } + @Test + fun `should not invoke tokenizeIfNecessary for empty list of TokenizableString`() { + every { event.getTokenizableFields() } returns emptyMap() + every { event.getTokenizableListFields() } returns mapOf(tokenKeyTypeList to emptyList()) + + useCase(event = event, project = project) + + verify(exactly = 0) { tokenizationProcessor.tokenizeIfNecessary(any(), any(), any()) } + verify { event.setTokenizedListFields(emptyMap()) } + } + @Test fun `should handle both field and list of TokenizableString in same event`() { every { event.getTokenizableFields() } returns mapOf(tokenKeyType to rawString) diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/DeviceConfigurationUpdatedEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/DeviceConfigurationUpdatedEventTest.kt index 3e5d557ed9..9b430733ff 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/DeviceConfigurationUpdatedEventTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/DeviceConfigurationUpdatedEventTest.kt @@ -1,6 +1,7 @@ package com.simprints.infra.events.event.domain.models import com.google.common.truth.Truth.* +import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.infra.config.store.models.TokenKeyType @@ -56,13 +57,36 @@ class DeviceConfigurationUpdatedEventTest { ) } - private fun createDefaultEvent(): DeviceConfigurationUpdatedEvent = DeviceConfigurationUpdatedEvent( - createdAt = CREATED_AT, - language = "en", - downSyncModules = listOf( + @Test + fun setTokenizableFields_keepsExistingModuleIdsWhenMapHasNoModuleIds() { + val event = createDefaultEvent() + + val updatedEvent = event.setTokenizedListFields(emptyMap()) as DeviceConfigurationUpdatedEvent + + assertThat(updatedEvent.payload.configuration.downSyncModules).containsExactly( + "module1".asTokenizableRaw(), + "module2".asTokenizableEncrypted(), + ) + } + + @Test + fun setTokenizableFields_keepsModuleIdsNullWhenMapHasNoModuleIds() { + val event = createDefaultEvent(downSyncModules = null) + + val updatedEvent = event.setTokenizedListFields(emptyMap()) as DeviceConfigurationUpdatedEvent + + assertThat(updatedEvent.payload.configuration.downSyncModules).isNull() + } + + private fun createDefaultEvent( + downSyncModules: List? = listOf( "module1".asTokenizableRaw(), "module2".asTokenizableEncrypted(), ), + ): DeviceConfigurationUpdatedEvent = DeviceConfigurationUpdatedEvent( + createdAt = CREATED_AT, + language = "en", + downSyncModules = downSyncModules, sourceUpdate = DeviceConfigurationUpdatedEvent.DeviceConfigurationUpdateSource.REMOTE, ) }