diff --git a/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/UserInstitutionCdcService.java b/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/UserInstitutionCdcService.java index 59e7a565..c18cc34e 100644 --- a/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/UserInstitutionCdcService.java +++ b/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/UserInstitutionCdcService.java @@ -15,11 +15,15 @@ import io.quarkus.runtime.Startup; import io.quarkus.runtime.configuration.ConfigUtils; import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; import it.pagopa.selfcare.user.UserUtils; +import it.pagopa.selfcare.user.client.EventHubFdRestClient; import it.pagopa.selfcare.user.client.EventHubRestClient; import it.pagopa.selfcare.user.event.entity.UserInstitution; import it.pagopa.selfcare.user.event.mapper.NotificationMapper; import it.pagopa.selfcare.user.event.repository.UserInstitutionRepository; +import it.pagopa.selfcare.user.model.NotificationUserType; +import it.pagopa.selfcare.user.model.OnboardedProduct; import it.pagopa.selfcare.user.model.TrackEventInput; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -42,6 +46,7 @@ import static it.pagopa.selfcare.user.model.TrackEventInput.toTrackEventInput; import static it.pagopa.selfcare.user.model.constants.EventsMetric.*; import static it.pagopa.selfcare.user.model.constants.EventsName.EVENT_USER_CDC_NAME; +import static it.pagopa.selfcare.user.model.constants.EventsName.FD_EVENT_USER_CDC_NAME; import static java.util.Arrays.asList; @Startup @@ -52,6 +57,10 @@ public class UserInstitutionCdcService { private static final String COLLECTION_NAME = "userInstitutions"; private static final String OPERATION_NAME = "USER-CDC-UserInfoUpdate"; public static final String USERS_FIELD_LIST_WITHOUT_FISCAL_CODE = "name,familyName,email,workContacts"; + private static final String PROD_FD = "prod-fd"; + private static final String PROD_FD_GARANTITO = "prod-fd-garantito"; + public static final String ERROR_DURING_SUBSCRIBE_COLLECTION_EXCEPTION_MESSAGE = "Error during subscribe collection, exception: {} , message: {}"; + private final TelemetryClient telemetryClient; @@ -72,6 +81,10 @@ public class UserInstitutionCdcService { @Inject EventHubRestClient eventHubRestClient; + @RestClient + @Inject + EventHubFdRestClient eventHubFdRestClient; + private final NotificationMapper notificationMapper; @@ -81,6 +94,7 @@ public UserInstitutionCdcService(ReactiveMongoClient mongoClient, @ConfigProperty(name = "user-cdc.retry.max-backoff") Integer retryMaxBackOff, @ConfigProperty(name = "user-cdc.retry") Integer maxRetry, @ConfigProperty(name = "user-cdc.send-events.watch.enabled") Boolean sendEventsEnabled, + @ConfigProperty(name = "user-cdc.send-events-fd.watch.enabled") Boolean sendFdEventsEnabled, UserInstitutionRepository userInstitutionRepository, TelemetryClient telemetryClient, TableClient tableClient, NotificationMapper notificationMapper) { @@ -94,16 +108,16 @@ public UserInstitutionCdcService(ReactiveMongoClient mongoClient, this.tableClient = tableClient; this.notificationMapper = notificationMapper; telemetryClient.getContext().getOperation().setName(OPERATION_NAME); - initOrderStream(sendEventsEnabled); + initOrderStream(sendEventsEnabled, sendFdEventsEnabled); } - private void initOrderStream(Boolean sendEventsEnabled) { + private void initOrderStream(Boolean sendEventsEnabled, Boolean sendFdEventsEnabled) { log.info("Starting initOrderStream ... "); //Retrieve last resumeToken for watching collection at specific operation String resumeToken = null; - if(!ConfigUtils.getProfiles().contains("test")) { + if (!ConfigUtils.getProfiles().contains("test")) { try { TableEntity cdcStartAtEntity = tableClient.getEntity(CDC_START_AT_PARTITION_KEY, CDC_START_AT_ROW_KEY); if (Objects.nonNull(cdcStartAtEntity)) @@ -117,7 +131,7 @@ private void initOrderStream(Boolean sendEventsEnabled) { ReactiveMongoCollection dataCollection = getCollection(); ChangeStreamOptions options = new ChangeStreamOptions() .fullDocument(FullDocument.UPDATE_LOOKUP); - if(Objects.nonNull(resumeToken)) + if (Objects.nonNull(resumeToken)) options = options.resumeAfter(BsonDocument.parse(resumeToken)); Bson match = Aggregates.match(Filters.in("operationType", asList("update", "replace", "insert"))); @@ -128,21 +142,32 @@ private void initOrderStream(Boolean sendEventsEnabled) { publisher.subscribe().with( this::consumerUserInstitutionRepositoryEvent, failure -> { - log.error("Error during subscribe collection, exception: {} , message: {}", failure.toString(), failure.getMessage()); - telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(TrackEventInput.builder().exception(failure.getClass().toString()).build()), Map.of(USER_INFO_UPDATE_FAILURE, 1D)); + log.error(ERROR_DURING_SUBSCRIBE_COLLECTION_EXCEPTION_MESSAGE, failure.toString(), failure.getMessage()); + telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(TrackEventInput.builder().exception(failure.getClass().toString()).build()), Map.of(USER_INFO_UPDATE_FAILURE, 1D)); Quarkus.asyncExit(); }); - if(sendEventsEnabled) { + if (Boolean.TRUE.equals(sendEventsEnabled)) { publisher.subscribe().with( this::consumerToSendScUserEvent, failure -> { - log.error("Error during subscribe collection, exception: {} , message: {}", failure.toString(), failure.getMessage()); + log.error(ERROR_DURING_SUBSCRIBE_COLLECTION_EXCEPTION_MESSAGE, failure.toString(), failure.getMessage()); + telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(TrackEventInput.builder().exception(failure.getClass().toString()).build()), Map.of(EVENTS_USER_INSTITUTION_FAILURE, 1D)); + Quarkus.asyncExit(); + }); + } + + if (Boolean.TRUE.equals(sendFdEventsEnabled)) { + publisher.subscribe().with( + this::consumerToSendUserEventForFD, + failure -> { + log.error(ERROR_DURING_SUBSCRIBE_COLLECTION_EXCEPTION_MESSAGE, failure.toString(), failure.getMessage()); telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(TrackEventInput.builder().exception(failure.getClass().toString()).build()), Map.of(EVENTS_USER_INSTITUTION_FAILURE, 1D)); Quarkus.asyncExit(); }); } + log.info("Completed initOrderStream ... "); } @@ -196,13 +221,13 @@ public void consumerToSendScUserEvent(ChangeStreamDocument docu userRegistryApi.findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitutionChanged.getUserId()) .onFailure(this::checkIfIsRetryableException) - .retry().withBackOff(Duration.ofSeconds(retryMinBackOff), Duration.ofSeconds(retryMaxBackOff)).atMost(maxRetry) + .retry().withBackOff(Duration.ofSeconds(retryMinBackOff), Duration.ofSeconds(retryMaxBackOff)).atMost(maxRetry) .onItem().transformToUni(userResource -> Multi.createFrom().iterable(UserUtils.groupingProductAndReturnMinStateProduct(userInstitutionChanged.getProducts())) .map(onboardedProduct -> notificationMapper.toUserNotificationToSend(userInstitutionChanged, onboardedProduct, userResource)) .onItem().transformToUniAndMerge(userNotificationToSend -> eventHubRestClient.sendMessage(userNotificationToSend) - .onFailure().retry().withBackOff(Duration.ofSeconds(retryMinBackOff), Duration.ofSeconds(retryMaxBackOff)).atMost(maxRetry) - .onItem().invoke(() -> telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInput(userNotificationToSend)), Map.of(EVENTS_USER_INSTITUTION_PRODUCT_SUCCESS, 1D))) - .onFailure().invoke(() -> telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInput(userNotificationToSend)), Map.of(EVENTS_USER_INSTITUTION_PRODUCT_FAILURE, 1D)))) + .onFailure().retry().withBackOff(Duration.ofSeconds(retryMinBackOff), Duration.ofSeconds(retryMaxBackOff)).atMost(maxRetry) + .onItem().invoke(() -> telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInput(userNotificationToSend)), Map.of(EVENTS_USER_INSTITUTION_PRODUCT_SUCCESS, 1D))) + .onFailure().invoke(() -> telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInput(userNotificationToSend)), Map.of(EVENTS_USER_INSTITUTION_PRODUCT_FAILURE, 1D)))) .toUni() ) .subscribe().with( @@ -216,6 +241,47 @@ public void consumerToSendScUserEvent(ChangeStreamDocument docu }); } + public void consumerToSendUserEventForFD(ChangeStreamDocument document) { + + if (Objects.nonNull(document.getFullDocument()) && Objects.nonNull(document.getDocumentKey())) { + UserInstitution userInstitutionChanged = document.getFullDocument(); + + log.info("Starting consumerToSendUserEventForFd ... "); + + userRegistryApi.findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitutionChanged.getUserId()) + .onFailure(this::checkIfIsRetryableException) + .retry().withBackOff(Duration.ofSeconds(retryMinBackOff), Duration.ofSeconds(retryMaxBackOff)).atMost(maxRetry) + .onItem().transformToUni(userResource -> Uni.createFrom().item(UserUtils.retrieveFdProductIfItChanged(userInstitutionChanged.getProducts(), List.of(PROD_FD, PROD_FD_GARANTITO))) + .onItem().ifNotNull().transform(onboardedProduct -> notificationMapper.toFdUserNotificationToSend(userInstitutionChanged, onboardedProduct, userResource, evaluateType(onboardedProduct))) + .onItem().ifNotNull().transformToUni(fdUserNotificationToSend -> { + log.info("Sending message to EventHubFdRestClient ... "); + return eventHubFdRestClient.sendMessage(fdUserNotificationToSend) + .onFailure().retry().withBackOff(Duration.ofSeconds(retryMinBackOff), Duration.ofSeconds(retryMaxBackOff)).atMost(maxRetry) + .onItem().invoke(() -> telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInput(fdUserNotificationToSend)), Map.of(FD_EVENTS_USER_INSTITUTION_PRODUCT_SUCCESS, 1D))) + .onFailure().invoke(() -> telemetryClient.trackEvent(EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInput(fdUserNotificationToSend)), Map.of(FD_EVENTS_USER_INSTITUTION_PRODUCT_FAILURE, 1D))); + } + )) + .subscribe().with( + result -> { + log.info("SendFdEvents successfully performed from UserInstitution document having id: {}", document.getDocumentKey().toJson()); + telemetryClient.trackEvent(FD_EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInputByUserInstitution(userInstitutionChanged)), Map.of(FD_EVENTS_USER_INSTITUTION_SUCCESS, 1D)); + }, + failure -> { + log.error("Error during SendFdEvents from UserInstitution document having id: {} , message: {}", document.getDocumentKey().toJson(), failure.getMessage()); + telemetryClient.trackEvent(FD_EVENT_USER_CDC_NAME, mapPropsForTrackEvent(toTrackEventInputByUserInstitution(userInstitutionChanged)), Map.of(FD_EVENTS_USER_INSTITUTION_FAILURE, 1D)); + }); + } + } + + private NotificationUserType evaluateType(OnboardedProduct onboardedProduct) { + return switch (onboardedProduct.getStatus()) { + case ACTIVE -> NotificationUserType.ACTIVE_USER; + case SUSPENDED -> NotificationUserType.SUSPEND_USER; + case DELETED -> NotificationUserType.DELETE_USER; + default -> null; + }; + } + private boolean checkIfIsRetryableException(Throwable throwable) { return throwable instanceof TimeoutException || (throwable instanceof ClientWebApplicationException webApplicationException && webApplicationException.getResponse().getStatus() == 429); diff --git a/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/mapper/NotificationMapper.java b/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/mapper/NotificationMapper.java index 3f5d90ac..88632682 100644 --- a/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/mapper/NotificationMapper.java +++ b/apps/user-cdc/src/main/java/it/pagopa/selfcare/user/event/mapper/NotificationMapper.java @@ -1,15 +1,17 @@ package it.pagopa.selfcare.user.event.mapper; +import com.microsoft.applicationinsights.web.dependencies.apachecommons.lang3.StringUtils; import it.pagopa.selfcare.user.UserUtils; import it.pagopa.selfcare.user.event.entity.UserInstitution; -import it.pagopa.selfcare.user.model.OnboardedProduct; -import it.pagopa.selfcare.user.model.UserNotificationToSend; +import it.pagopa.selfcare.user.model.*; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Named; +import org.openapi.quarkus.user_registry_json.model.CertifiableFieldResourceOfstring; import org.openapi.quarkus.user_registry_json.model.UserResource; -import java.util.UUID; +import javax.swing.text.html.Option; +import java.util.*; @Mapper(componentModel = "cdi", imports = UUID.class) public interface NotificationMapper { @@ -18,13 +20,7 @@ public interface NotificationMapper { @Mapping(target = "productId", source = "product.productId") @Mapping(target = "createdAt", source = "product.createdAt") @Mapping(target = "updatedAt", expression = "java((null == product.getUpdatedAt()) ? product.getCreatedAt() : product.getUpdatedAt())") - @Mapping(target = "user.role", source = "product.role") - @Mapping(target = "user.productRole", source = "product.productRole") - @Mapping(target = "user.relationshipStatus", source = "product.status") - @Mapping(target = "user.userId", source = "userResource.id", ignore = true) - @Mapping(target = "user.name", source = "userResource.name.value") - @Mapping(target = "user.familyName", source = "userResource.familyName.value") - @Mapping(target = "user.email", source = "userResource.email.value") + @Mapping(target = "user", expression = "java(mapUser(userResource, userInstitution.getUserMailUuid(), product))") @Mapping(target = "id", expression = "java(toUniqueIdNotification(userInstitution, product))") @Mapping(target = "eventType", expression = "java(it.pagopa.selfcare.user.model.constants.QueueEvent.UPDATE)") UserNotificationToSend toUserNotificationToSend(UserInstitution userInstitution, OnboardedProduct product, UserResource userResource); @@ -33,4 +29,43 @@ public interface NotificationMapper { default String toUniqueIdNotification(UserInstitution userInstitution, OnboardedProduct product) { return UserUtils.uniqueIdNotification(userInstitution.getId().toHexString(), product.getProductId(), product.getProductRole()); } + + @Mapping(target = "id", expression = "java(toUniqueIdNotification(userInstitutionChanged, product))") + @Mapping(target = "onboardingTokenId", source = "product.tokenId") + @Mapping(target = "product", source = "product.productId") + @Mapping(target = "createdAt", source = "product.createdAt") + @Mapping(target = "updatedAt", expression = "java((null == product.getUpdatedAt()) ? product.getCreatedAt() : product.getUpdatedAt())") + @Mapping(target = "user", expression = "java(mapUserForFD(userResource, product))") + @Mapping(target = "type", source = "type") + FdUserNotificationToSend toFdUserNotificationToSend(UserInstitution userInstitutionChanged, OnboardedProduct product, UserResource userResource, NotificationUserType type); + + @Named("mapUserForFD") + default UserToNotify mapUserForFD(UserResource userResource,OnboardedProduct onboardedProduct) { + UserToNotify userToNotify = new UserToNotify(); + userToNotify.setUserId(Optional.ofNullable(userResource.getId()).map(UUID::toString).orElse(null)); + userToNotify.setRoles(StringUtils.isNotBlank(onboardedProduct.getProductRole()) ? List.of(onboardedProduct.getProductRole()) : Collections.emptyList()); + userToNotify.setRole(Optional.ofNullable(onboardedProduct.getRole()).map(Enum::name).orElse(null)); + return userToNotify; + } + + @Named("mapUser") + default UserToNotify mapUser(UserResource userResource, String userMailUuid, OnboardedProduct onboardedProduct) { + UserToNotify userToNotify = new UserToNotify(); + userToNotify.setUserId(Optional.ofNullable(userResource.getId()).map(UUID::toString).orElse(null)); + userToNotify.setName(Optional.ofNullable(userResource.getName()).map(CertifiableFieldResourceOfstring::getValue).orElse(null)); + userToNotify.setFamilyName(Optional.ofNullable(userResource.getFamilyName()).map(CertifiableFieldResourceOfstring::getValue).orElse(null)); + userToNotify.setEmail(Optional.ofNullable(userMailUuid).map(mailUuid -> retrieveMailFromWorkContacts(userResource, mailUuid)).orElse(null)); + userToNotify.setProductRole(onboardedProduct.getProductRole()); + userToNotify.setRole(Optional.ofNullable(onboardedProduct.getRole()).map(Enum::name).orElse(null)); + userToNotify.setRelationshipStatus(onboardedProduct.getStatus()); + return userToNotify; + } + + default String retrieveMailFromWorkContacts(UserResource userResource, String userMailUuid) { + return Optional.ofNullable(userResource.getWorkContacts()) + .flatMap(stringWorkContactResourceMap -> Optional.ofNullable(stringWorkContactResourceMap.get(userMailUuid)) + .flatMap(workContactResource -> Optional.ofNullable(workContactResource.getEmail()) + .map(CertifiableFieldResourceOfstring::getValue))) + .orElse(null); + } } diff --git a/apps/user-cdc/src/main/resources/application.properties b/apps/user-cdc/src/main/resources/application.properties index 7fa5255f..28cc9ceb 100644 --- a/apps/user-cdc/src/main/resources/application.properties +++ b/apps/user-cdc/src/main/resources/application.properties @@ -7,7 +7,8 @@ quarkus.mongodb.connection-string = ${MONGODB-CONNECTION-STRING} quarkus.mongodb.database = selcUser #False for pnpg use case because we must not send events -user-cdc.send-events.watch.enabled=${USER_CDC_SEND_EVENTS_WATCH_ENABLED:false} +user-cdc.send-events.watch.enabled=${USER_CDC_SEND_EVENTS_WATCH_ENABLED:true} +user-cdc.send-events-fd.watch.enabled=${USER_CDC_SEND_EVENTS_FD_WATCH_ENABLED:true} user-cdc.appinsights.connection-string=${APPLICATIONINSIGHTS_CONNECTION_STRING:InstrumentationKey=00000000-0000-0000-0000-000000000000} user-cdc.table.name=${START_AT_TABLE_NAME:CdCStartAt} user-cdc.storage.connection-string=${STORAGE_CONNECTION_STRING:UseDevelopmentStorage=true;} @@ -23,6 +24,10 @@ quarkus.openapi-generator.codegen.spec.user_registry_json.additional-model-type- quarkus.openapi-generator.user_registry_json.auth.api_key.api-key = ${USER-REGISTRY-API-KEY:example-api-key} quarkus.rest-client."org.openapi.quarkus.user_registry_json.api.UserApi".url=${USER_REGISTRY_URL:http://localhost:8080} -quarkus.rest-client.event-hub.url=${EVENT_HUB_BASE_PATH:test} +quarkus.rest-client.event-hub.url=${EVENT_HUB_BASE_PATH:test}${EVENT_HUB_SC_USERS_TOPIC:sc-users} eventhub.rest-client.keyName=${SHARED_ACCESS_KEY_NAME:test} -eventhub.rest-client.key=${EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC:test} \ No newline at end of file +eventhub.rest-client.key=${EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC:test} + +quarkus.rest-client.event-hub-fd.url=${EVENT_HUB_BASE_PATH:test}${EVENT_HUB_SELFCARE_FD_TOPIC:selfcare-fd} +eventhubfd.rest-client.keyName=${FD_SHARED_ACCESS_KEY_NAME:test} +eventhubfd.rest-client.key=${EVENTHUB_SELFCARE_FD_EXTERNAL_KEY_LC:test} \ No newline at end of file diff --git a/apps/user-cdc/src/test/java/it/pagopa/selfcare/user/event/mapper/NotificationMapperTest.java b/apps/user-cdc/src/test/java/it/pagopa/selfcare/user/event/mapper/NotificationMapperTest.java new file mode 100644 index 00000000..f6568b88 --- /dev/null +++ b/apps/user-cdc/src/test/java/it/pagopa/selfcare/user/event/mapper/NotificationMapperTest.java @@ -0,0 +1,102 @@ +package it.pagopa.selfcare.user.event.mapper; + +import it.pagopa.selfcare.onboarding.common.PartyRole; +import it.pagopa.selfcare.user.model.OnboardedProduct; +import it.pagopa.selfcare.user.model.UserToNotify; +import it.pagopa.selfcare.user.model.constants.OnboardedProductState; +import org.junit.jupiter.api.Test; +import org.openapi.quarkus.user_registry_json.model.CertifiableFieldResourceOfstring; +import org.openapi.quarkus.user_registry_json.model.UserResource; +import org.openapi.quarkus.user_registry_json.model.WorkContactResource; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +class NotificationMapperTest { + + @Test + void mapUser_withValidData_shouldMapFieldsCorrectly() { + UserResource userResource = new UserResource(); + userResource.setId(UUID.randomUUID()); + userResource.setName(new CertifiableFieldResourceOfstring().value("John")); + userResource.setFamilyName(new CertifiableFieldResourceOfstring().value("Doe")); + userResource.setWorkContacts(Map.of("mailUuid", new WorkContactResource().email(new CertifiableFieldResourceOfstring().value("john.doe@example.com")))); + OnboardedProduct onboardedProduct = new OnboardedProduct(); + onboardedProduct.setProductRole("Admin"); + onboardedProduct.setRole(PartyRole.MANAGER); + onboardedProduct.setStatus(OnboardedProductState.ACTIVE); + + UserToNotify result = new NotificationMapperImpl().mapUser(userResource, "mailUuid", onboardedProduct); + + assertEquals(userResource.getId().toString(), result.getUserId()); + assertEquals("John", result.getName()); + assertEquals("Doe", result.getFamilyName()); + assertEquals("john.doe@example.com", result.getEmail()); + assertEquals("Admin", result.getProductRole()); + assertEquals("MANAGER", result.getRole()); + assertEquals(OnboardedProductState.ACTIVE, result.getRelationshipStatus()); + } + + @Test + void mapUserForFdTest() { + UserResource userResource = new UserResource(); + userResource.setId(UUID.randomUUID()); + OnboardedProduct onboardedProduct = new OnboardedProduct(); + onboardedProduct.setProductRole("Admin"); + onboardedProduct.setRole(PartyRole.MANAGER); + + UserToNotify result = new NotificationMapperImpl().mapUserForFD(userResource, onboardedProduct); + + assertEquals(userResource.getId().toString(), result.getUserId()); + assertEquals(List.of("Admin"), result.getRoles()); + assertEquals("MANAGER", result.getRole()); + } + + @Test + void mapUser_withNullFields_shouldHandleNullValues() { + UserResource userResource = new UserResource(); + OnboardedProduct onboardedProduct = new OnboardedProduct(); + + UserToNotify result = new NotificationMapperImpl().mapUser(userResource, null, onboardedProduct); + + assertNull(result.getUserId()); + assertNull(result.getName()); + assertNull(result.getFamilyName()); + assertNull(result.getEmail()); + assertNull(result.getProductRole()); + assertNull(result.getRole()); + assertNull(result.getRelationshipStatus()); + } + + @Test + void retrieveMailFromWorkContacts_withValidMailUuid_shouldReturnEmail() { + UserResource userResource = new UserResource(); + userResource.setWorkContacts(Map.of("mailUuid", new WorkContactResource().email(new CertifiableFieldResourceOfstring().value("john.doe@example.com")))); + + String result = new NotificationMapperImpl().retrieveMailFromWorkContacts(userResource, "mailUuid"); + + assertEquals("john.doe@example.com", result); + } + + @Test + void retrieveMailFromWorkContacts_withInvalidMailUuid_shouldReturnNull() { + UserResource userResource = new UserResource(); + userResource.setWorkContacts(Map.of("mailUuid", new WorkContactResource().email(new CertifiableFieldResourceOfstring().value("john.doe@example.com")))); + + String result = new NotificationMapperImpl().retrieveMailFromWorkContacts(userResource, "invalidUuid"); + + assertNull(result); + } + + @Test + void retrieveMailFromWorkContacts_withNullWorkContacts_shouldReturnNull() { + UserResource userResource = new UserResource(); + + String result = new NotificationMapperImpl().retrieveMailFromWorkContacts(userResource, "mailUuid"); + + assertNull(result); + } +} diff --git a/apps/user-cdc/src/test/java/it/pagopa/selfcare/user/event/service/UserInstitutionCdcServiceTest.java b/apps/user-cdc/src/test/java/it/pagopa/selfcare/user/event/service/UserInstitutionCdcServiceTest.java index f5d29ed6..1a97a627 100644 --- a/apps/user-cdc/src/test/java/it/pagopa/selfcare/user/event/service/UserInstitutionCdcServiceTest.java +++ b/apps/user-cdc/src/test/java/it/pagopa/selfcare/user/event/service/UserInstitutionCdcServiceTest.java @@ -7,16 +7,22 @@ import io.quarkus.test.mongodb.MongoTestResource; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; +import it.pagopa.selfcare.user.client.EventHubFdRestClient; import it.pagopa.selfcare.user.client.EventHubRestClient; import it.pagopa.selfcare.user.event.UserInstitutionCdcService; +import it.pagopa.selfcare.user.event.entity.UserInfo; import it.pagopa.selfcare.user.event.entity.UserInstitution; +import it.pagopa.selfcare.user.model.FdUserNotificationToSend; import it.pagopa.selfcare.user.model.OnboardedProduct; +import it.pagopa.selfcare.user.model.UserNotificationToSend; import it.pagopa.selfcare.user.model.constants.OnboardedProductState; import jakarta.inject.Inject; import org.bson.BsonDocument; import org.bson.types.ObjectId; import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.openapi.quarkus.user_registry_json.api.UserApi; import org.openapi.quarkus.user_registry_json.model.UserResource; @@ -28,6 +34,7 @@ import java.util.List; import static it.pagopa.selfcare.user.event.UserInstitutionCdcService.USERS_FIELD_LIST_WITHOUT_FISCAL_CODE; +import static it.pagopa.selfcare.user.model.NotificationUserType.*; import static org.mockito.Mockito.*; @QuarkusTest @@ -45,9 +52,14 @@ public class UserInstitutionCdcServiceTest { @InjectMock EventHubRestClient eventHubRestClient; + @RestClient + @InjectMock + EventHubFdRestClient eventHubFdRestClient; + + @Test void consumerToSendScUserEvent() { - UserInstitution userInstitution = dummyUserInstitution(); + UserInstitution userInstitution = dummyUserInstitution(false, null); ChangeStreamDocument document = Mockito.mock(ChangeStreamDocument.class); when(document.getFullDocument()).thenReturn(userInstitution); when(document.getDocumentKey()).thenReturn(new BsonDocument()); @@ -60,27 +72,118 @@ void consumerToSendScUserEvent() { verify(userRegistryApi, times(1)). findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId()); verify(eventHubRestClient, times(2)). - sendMessage(any()); + sendMessage(any(UserNotificationToSend.class)); + } + + @Test + void consumerToSendUserEventForFDSendACTIVE_USER() { + UserInstitution userInstitution = dummyUserInstitution(true, OnboardedProductState.ACTIVE); + ChangeStreamDocument document = Mockito.mock(ChangeStreamDocument.class); + when(document.getFullDocument()).thenReturn(userInstitution); + when(document.getDocumentKey()).thenReturn(new BsonDocument()); + + UserResource userResource = dummyUserResource(); + when(userRegistryApi.findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId())) + .thenReturn(Uni.createFrom().item(userResource)); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(FdUserNotificationToSend.class); + when(eventHubFdRestClient.sendMessage(argumentCaptor.capture())) + .thenReturn(Uni.createFrom().nullItem()); + + userInstitutionCdcService.consumerToSendUserEventForFD(document); + verify(userRegistryApi, times(1)). + findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId()); + verify(eventHubFdRestClient, times(1)). + sendMessage(any(FdUserNotificationToSend.class)); + Assertions.assertEquals(ACTIVE_USER, argumentCaptor.getValue().getType()); + } + + @Test + void consumerToSendUserEventForFDSendSUSPEND_USER() { + UserInstitution userInstitution = dummyUserInstitution(true, OnboardedProductState.SUSPENDED); + ChangeStreamDocument document = Mockito.mock(ChangeStreamDocument.class); + when(document.getFullDocument()).thenReturn(userInstitution); + when(document.getDocumentKey()).thenReturn(new BsonDocument()); + + UserResource userResource = dummyUserResource(); + when(userRegistryApi.findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId())) + .thenReturn(Uni.createFrom().item(userResource)); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(FdUserNotificationToSend.class); + + when(eventHubFdRestClient.sendMessage(argumentCaptor.capture())) + .thenReturn(Uni.createFrom().nullItem()); + + userInstitutionCdcService.consumerToSendUserEventForFD(document); + verify(userRegistryApi, times(1)). + findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId()); + verify(eventHubFdRestClient, times(1)). + sendMessage(any(FdUserNotificationToSend.class)); + Assertions.assertEquals(SUSPEND_USER, argumentCaptor.getValue().getType()); + } + + @Test + void consumerToSendUserEventForFDSendDELETE_USER() { + UserInstitution userInstitution = dummyUserInstitution(true, OnboardedProductState.DELETED); + ChangeStreamDocument document = Mockito.mock(ChangeStreamDocument.class); + when(document.getFullDocument()).thenReturn(userInstitution); + when(document.getDocumentKey()).thenReturn(new BsonDocument()); + + UserResource userResource = dummyUserResource(); + when(userRegistryApi.findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId())) + .thenReturn(Uni.createFrom().item(userResource)); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(FdUserNotificationToSend.class); + when(eventHubFdRestClient.sendMessage(argumentCaptor.capture())) + .thenReturn(Uni.createFrom().nullItem()); + + userInstitutionCdcService.consumerToSendUserEventForFD(document); + verify(userRegistryApi, times(1)). + findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId()); + verify(eventHubFdRestClient, times(1)). + sendMessage(any(FdUserNotificationToSend.class)); + Assertions.assertEquals(DELETE_USER, argumentCaptor.getValue().getType()); } + @Test + void consumerToSendUserEventForFDNotSend() { + UserInstitution userInstitution = dummyUserInstitution(false, null); + ChangeStreamDocument document = Mockito.mock(ChangeStreamDocument.class); + when(document.getFullDocument()).thenReturn(userInstitution); + when(document.getDocumentKey()).thenReturn(new BsonDocument()); + + UserResource userResource = dummyUserResource(); + when(userRegistryApi.findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId())) + .thenReturn(Uni.createFrom().item(userResource)); + + userInstitutionCdcService.consumerToSendUserEventForFD(document); + verify(userRegistryApi, times(1)). + findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId()); + verify(eventHubFdRestClient, times(0)). + sendMessage(any(FdUserNotificationToSend.class)); + } + + UserResource dummyUserResource() { UserResource userResource = new UserResource(); return userResource; } - UserInstitution dummyUserInstitution() { + UserInstitution dummyUserInstitution(boolean sendForFd, OnboardedProductState state){ UserInstitution userInstitution = new UserInstitution(); userInstitution.setId(ObjectId.get()); - userInstitution.setProducts(List.of(dummyOnboardedProduct("example-1", OnboardedProductState.ACTIVE, 1), - dummyOnboardedProduct("example-2", OnboardedProductState.DELETED, 1))); + if(sendForFd) { + userInstitution.setProducts(List.of(dummyOnboardedProduct("example-1", state, 2, "prod-fd"), + dummyOnboardedProduct("example-2", OnboardedProductState.ACTIVE, 1, "prod-io"))); + }else { + userInstitution.setProducts(List.of(dummyOnboardedProduct("example-1", OnboardedProductState.ACTIVE, 2, "prod-io"), + dummyOnboardedProduct("example-2", OnboardedProductState.ACTIVE, 1, "prod-fd"))); + } return userInstitution; } - OnboardedProduct dummyOnboardedProduct(String productRole, OnboardedProductState state, int day) { + OnboardedProduct dummyOnboardedProduct(String productRole, OnboardedProductState state, int day, String productId) { OnboardedProduct onboardedProduct = new OnboardedProduct(); - onboardedProduct.setProductId("productId"); + onboardedProduct.setProductId(productId); onboardedProduct.setProductRole(productRole); onboardedProduct.setCreatedAt(OffsetDateTime.of(2024, 1, day, 0, 0, 0, 0, ZoneOffset.UTC)); onboardedProduct.setUpdatedAt(OffsetDateTime.of(2024, 1, day, 0, 0, 0, 0, ZoneOffset.UTC)); diff --git a/infra/container_apps/user-cdc/env/dev/terraform.tfvars b/infra/container_apps/user-cdc/env/dev/terraform.tfvars index 84fe364c..909d329e 100644 --- a/infra/container_apps/user-cdc/env/dev/terraform.tfvars +++ b/infra/container_apps/user-cdc/env/dev/terraform.tfvars @@ -44,14 +44,30 @@ app_settings = [ name = "USER_CDC_SEND_EVENTS_WATCH_ENABLED" value = "true" }, + { + name = "USER_CDC_SEND_EVENTS_FD_WATCH_ENABLED" + value = "true" + }, { name = "EVENT_HUB_BASE_PATH" - value = "https://selc-d-eventhub-ns.servicebus.windows.net/sc-users" + value = "https://selc-d-eventhub-ns.servicebus.windows.net/" + }, + { + name = "EVENT_HUB_SC_USERS_TOPIC" + value = "sc-users" + }, + { + name = "EVENT_HUB_SELFCARE_FD_TOPIC" + value = "selfcare-fd" }, { name = "SHARED_ACCESS_KEY_NAME" value = "selfcare-wo" }, + { + name = "FD_SHARED_ACCESS_KEY_NAME" + value = "external-interceptor-wo" + }, { name = "USER_REGISTRY_URL" value = "https://api.uat.pdv.pagopa.it/user-registry/v1" @@ -60,10 +76,11 @@ app_settings = [ secrets_names = { - "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" - "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" - "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" - "EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC" = "eventhub-sc-users-selfcare-wo-key-lc" - "USER-REGISTRY-API-KEY" = "user-registry-api-key" + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" + "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" + "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" + "EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC" = "eventhub-sc-users-selfcare-wo-key-lc" + "USER-REGISTRY-API-KEY" = "user-registry-api-key" + "EVENTHUB_SELFCARE_FD_EXTERNAL_KEY_LC" = "eventhub-selfcare-fd-external-interceptor-wo-key-lc" } diff --git a/infra/container_apps/user-cdc/env/prod/terraform.tfvars b/infra/container_apps/user-cdc/env/prod/terraform.tfvars index 71fe484e..c04cf280 100644 --- a/infra/container_apps/user-cdc/env/prod/terraform.tfvars +++ b/infra/container_apps/user-cdc/env/prod/terraform.tfvars @@ -32,14 +32,30 @@ app_settings = [ name = "USER_CDC_SEND_EVENTS_WATCH_ENABLED" value = "true" }, + { + name = "USER_CDC_SEND_EVENTS_FD_WATCH_ENABLED" + value = "true" + }, { name = "EVENT_HUB_BASE_PATH" - value = "https://selc-p-eventhub-ns.servicebus.windows.net/sc-users" + value = "https://selc-p-eventhub-ns.servicebus.windows.net/" + }, + { + name = "EVENT_HUB_SC_USERS_TOPIC" + value = "sc-users" + }, + { + name = "EVENT_HUB_SELFCARE_FD_TOPIC" + value = "selfcare-fd" }, { name = "SHARED_ACCESS_KEY_NAME" value = "selfcare-wo" }, + { + name = "FD_SHARED_ACCESS_KEY_NAME" + value = "external-interceptor-wo" + }, { name = "USER_REGISTRY_URL" value = "https://api.pdv.pagopa.it/user-registry/v1" @@ -47,10 +63,11 @@ app_settings = [ ] secrets_names = { - "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" - "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" - "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" - "EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC" = "eventhub-sc-users-selfcare-wo-key-lc" - "USER-REGISTRY-API-KEY" = "user-registry-api-key" + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" + "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" + "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" + "EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC" = "eventhub-sc-users-selfcare-wo-key-lc" + "USER-REGISTRY-API-KEY" = "user-registry-api-key" + "EVENTHUB_SELFCARE_FD_EXTERNAL_KEY_LC" = "eventhub-selfcare-fd-external-interceptor-wo-key-lc" } diff --git a/infra/container_apps/user-cdc/env/uat/terraform.tfvars b/infra/container_apps/user-cdc/env/uat/terraform.tfvars index 742c9734..d4b3c5d5 100644 --- a/infra/container_apps/user-cdc/env/uat/terraform.tfvars +++ b/infra/container_apps/user-cdc/env/uat/terraform.tfvars @@ -32,14 +32,30 @@ app_settings = [ name = "USER_CDC_SEND_EVENTS_WATCH_ENABLED" value = "false" }, + { + name = "USER_CDC_SEND_EVENTS_FD_WATCH_ENABLED" + value = "true" + }, { name = "EVENT_HUB_BASE_PATH" - value = "https://selc-d-eventhub-ns.servicebus.windows.net/sc-users" + value = "https://selc-d-eventhub-ns.servicebus.windows.net/" + }, + { + name = "EVENT_HUB_SC_USERS_TOPIC" + value = "sc-users" + }, + { + name = "EVENT_HUB_SELFCARE_FD_TOPIC" + value = "selfcare-fd" }, { name = "SHARED_ACCESS_KEY_NAME" value = "selfcare-wo" }, + { + name = "FD_SHARED_ACCESS_KEY_NAME" + value = "external-interceptor-wo" + }, { name = "USER_REGISTRY_URL" value = "https://api.uat.pdv.pagopa.it/user-registry/v1" @@ -47,9 +63,10 @@ app_settings = [ ] secrets_names = { - "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" - "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" - "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" - "EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC" = "eventhub-sc-users-selfcare-wo-key-lc" - "USER-REGISTRY-API-KEY" = "user-registry-api-key" + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" + "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" + "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" + "EVENTHUB-SC-USERS-SELFCARE-WO-KEY-LC" = "eventhub-sc-users-selfcare-wo-key-lc" + "USER-REGISTRY-API-KEY" = "user-registry-api-key" + "EVENTHUB_SELFCARE_FD_EXTERNAL_KEY_LC" = "eventhub-selfcare-fd-external-interceptor-wo-key-lc" } diff --git a/libs/user-sdk-event/pom.xml b/libs/user-sdk-event/pom.xml index 5b6f6c79..5da0fb32 100644 --- a/libs/user-sdk-event/pom.xml +++ b/libs/user-sdk-event/pom.xml @@ -64,6 +64,24 @@ 3.0.2 compile + + com.fasterxml.jackson.core + jackson-annotations + 2.17.1 + compile + + + com.fasterxml.jackson.core + jackson-databind + 2.17.1 + compile + + + io.quarkus + quarkus-bootstrap-gradle-resolver + 3.11.3 + compile + diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/UserUtils.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/UserUtils.java index df3bd980..628d78df 100644 --- a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/UserUtils.java +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/UserUtils.java @@ -3,13 +3,22 @@ import it.pagopa.selfcare.user.model.OnboardedProduct; import it.pagopa.selfcare.user.model.TrackEventInput; import it.pagopa.selfcare.user.model.constants.OnboardedProductState; +import lombok.extern.slf4j.Slf4j; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.stream.Collectors; import static java.util.Comparator.naturalOrder; import static java.util.Comparator.nullsLast; +@Slf4j public class UserUtils { /** @@ -66,4 +75,52 @@ public static Map mapPropsForTrackEvent(TrackEventInput trackEve Optional.ofNullable(trackEventInput.getException()).ifPresent(value -> propertiesMap.put("exec", value)); return propertiesMap; } + + /** + * The retrieveFdProductIfItChanged method is designed to retrieve the most recently updated OnboardedProduct + * from a list of products, provided that the product's ID is included in a specified list of product IDs to check. + */ + public static OnboardedProduct retrieveFdProductIfItChanged(List products, List productIdToCheck) { + if (Objects.nonNull(products) && !products.isEmpty()) { + return products.stream() + .max(Comparator.comparing(OnboardedProduct::getUpdatedAt, nullsLast(naturalOrder())) + .thenComparing(OnboardedProduct::getCreatedAt, nullsLast(naturalOrder()))) + .filter(onboardedProduct -> productIdToCheck.contains(onboardedProduct.getProductId())) + .orElse(null); + } + return null; + } + + public static String getSASToken(String resourceUri, String keyName, String key) { + long epoch = System.currentTimeMillis() / 1000L; + int week = 60 * 60 * 24 * 7; + String expiry = Long.toString(epoch + week); + + String sasToken; + String stringToSign = URLEncoder.encode(resourceUri, StandardCharsets.UTF_8) + "\n" + expiry; + String signature = getHMAC256(key, stringToSign); + sasToken = "SharedAccessSignature sr=" + URLEncoder.encode(resourceUri, StandardCharsets.UTF_8) + "&sig=" + + URLEncoder.encode(signature, StandardCharsets.UTF_8) + "&se=" + expiry + "&skn=" + keyName; + return sasToken; + } + + + public static String getHMAC256(String key, String input) { + Mac sha256HMAC; + String hash = null; + try { + sha256HMAC = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "HmacSHA256"); + sha256HMAC.init(secretKey); + Base64.Encoder encoder = Base64.getEncoder(); + + hash = new String(encoder.encode(sha256HMAC.doFinal(input.getBytes(StandardCharsets.UTF_8)))); + + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) { + log.error("Exception: {}", e.getMessage(), e); + } + + return hash; + } + } diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/auth/EventhubFdSasTokenAuthorization.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/auth/EventhubFdSasTokenAuthorization.java new file mode 100644 index 00000000..93dbea88 --- /dev/null +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/auth/EventhubFdSasTokenAuthorization.java @@ -0,0 +1,32 @@ +package it.pagopa.selfcare.user.auth; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.IOException; +import java.net.URI; + +import static it.pagopa.selfcare.user.UserUtils.getSASToken; + +public class EventhubFdSasTokenAuthorization implements ClientRequestFilter { + + @Inject + @ConfigProperty(name = "quarkus.rest-client.event-hub-fd.url") + URI resourceUri; + + @Inject + @ConfigProperty(name = "eventhubfd.rest-client.keyName") + String keyName; + + @Inject + @ConfigProperty(name = "eventhubfd.rest-client.key") + String key; + + @Override + public void filter(ClientRequestContext clientRequestContext) throws IOException { + clientRequestContext.getHeaders() + .add("Authorization", getSASToken(resourceUri.toString(), keyName, key)); + } +} diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/auth/EventhubSasTokenAuthorization.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/auth/EventhubSasTokenAuthorization.java index 3a91676b..e5b247cd 100644 --- a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/auth/EventhubSasTokenAuthorization.java +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/auth/EventhubSasTokenAuthorization.java @@ -5,15 +5,11 @@ import jakarta.ws.rs.client.ClientRequestFilter; import org.eclipse.microprofile.config.inject.ConfigProperty; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URLEncoder; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; + +import static it.pagopa.selfcare.user.UserUtils.getSASToken; + public class EventhubSasTokenAuthorization implements ClientRequestFilter { @Inject @@ -33,51 +29,4 @@ public void filter(ClientRequestContext clientRequestContext) throws IOException clientRequestContext.getHeaders().add("Authorization", getSASToken(resourceUri.toString(), keyName, key)); } - - private static String getSASToken(String resourceUri, String keyName, String key) { - long epoch = System.currentTimeMillis() / 1000L; - int week = 60 * 60 * 24 * 7; - String expiry = Long.toString(epoch + week); - - String sasToken = null; - try { - String stringToSign = URLEncoder.encode(resourceUri, "UTF-8") + "\n" + expiry; - String signature = getHMAC256(key, stringToSign); - sasToken = "SharedAccessSignature sr=" + URLEncoder.encode(resourceUri, "UTF-8") + "&sig=" + - URLEncoder.encode(signature, "UTF-8") + "&se=" + expiry + "&skn=" + keyName; - } catch (UnsupportedEncodingException e) { - - e.printStackTrace(); - } - - return sasToken; - } - - - public static String getHMAC256(String key, String input) { - Mac sha256_HMAC = null; - String hash = null; - try { - sha256_HMAC = Mac.getInstance("HmacSHA256"); - SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256"); - sha256_HMAC.init(secret_key); - Base64.Encoder encoder = Base64.getEncoder(); - - hash = new String(encoder.encode(sha256_HMAC.doFinal(input.getBytes("UTF-8")))); - - } catch (InvalidKeyException e) { - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - return hash; - } - - - } diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/client/EventHubFdRestClient.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/client/EventHubFdRestClient.java new file mode 100644 index 00000000..8a95d03b --- /dev/null +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/client/EventHubFdRestClient.java @@ -0,0 +1,22 @@ +package it.pagopa.selfcare.user.client; + +import io.smallrye.mutiny.Uni; +import it.pagopa.selfcare.user.auth.EventhubFdSasTokenAuthorization; +import it.pagopa.selfcare.user.model.FdUserNotificationToSend; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "event-hub-fd") +@ApplicationScoped +@Path("/") +@RegisterProvider(EventhubFdSasTokenAuthorization.class) +public interface EventHubFdRestClient { + + @POST + @Path("messages") + Uni sendMessage(FdUserNotificationToSend notification); + +} diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/FdUserNotificationToSend.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/FdUserNotificationToSend.java new file mode 100644 index 00000000..f5e6aa5a --- /dev/null +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/FdUserNotificationToSend.java @@ -0,0 +1,22 @@ +package it.pagopa.selfcare.user.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.time.OffsetDateTime; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Data +@SuppressWarnings("java:S1068") +public class FdUserNotificationToSend { + + private String id; + private String institutionId; + private String product; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; + private String onboardingTokenId; + private NotificationUserType type; + private UserToNotify user; + +} \ No newline at end of file diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/NotificationUserType.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/NotificationUserType.java new file mode 100644 index 00000000..4e55b536 --- /dev/null +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/NotificationUserType.java @@ -0,0 +1,10 @@ +package it.pagopa.selfcare.user.model; + +@SuppressWarnings("java:S1068") +public enum NotificationUserType { + + ACTIVE_USER, + SUSPEND_USER, + DELETE_USER; + +} diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java index 368eae09..090b8b31 100644 --- a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java @@ -29,6 +29,17 @@ public static TrackEventInput toTrackEventInput(UserNotificationToSend userNotif .build(); } + public static TrackEventInput toTrackEventInput(FdUserNotificationToSend fdUserNotificationToSend) { + return TrackEventInput.builder() + .documentKey(fdUserNotificationToSend.getId()) + .userId(Optional.ofNullable(fdUserNotificationToSend.getUser()).map(UserToNotify::getUserId).orElse(null)) + .institutionId(fdUserNotificationToSend.getInstitutionId()) + .productId(fdUserNotificationToSend.getProduct()) + .productRole(Optional.ofNullable(fdUserNotificationToSend.getUser()).map(UserToNotify::getProductRole).orElse(null)) + .build(); + } + + public static TrackEventInput toTrackEventInputForUserGroup(UserGroupNotificationToSend userGroupEntity) { TrackEventInputBuilder trackEventInputBuilder = TrackEventInput.builder() .documentKey(userGroupEntity.getId()) diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/UserToNotify.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/UserToNotify.java index 8030707f..6510cf6d 100644 --- a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/UserToNotify.java +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/UserToNotify.java @@ -1,14 +1,18 @@ package it.pagopa.selfcare.user.model; +import com.fasterxml.jackson.annotation.JsonInclude; import it.pagopa.selfcare.user.model.constants.OnboardedProductState; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.eclipse.microprofile.openapi.annotations.media.Schema; +import java.util.List; + @Data @AllArgsConstructor @NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) public class UserToNotify { private String userId; @@ -18,6 +22,7 @@ public class UserToNotify { @Schema(description = "Available values: MANAGER, DELEGATE, SUB_DELEGATE, OPERATOR, ADMIN_EA") private String role; private String productRole; + private List roles; private OnboardedProductState relationshipStatus; } diff --git a/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/FdUserNotificationToSendTest.java b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/FdUserNotificationToSendTest.java new file mode 100644 index 00000000..2937e266 --- /dev/null +++ b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/FdUserNotificationToSendTest.java @@ -0,0 +1,110 @@ +package it.pagopa.selfcare.user; + +import it.pagopa.selfcare.user.model.FdUserNotificationToSend; +import it.pagopa.selfcare.user.model.NotificationUserType; +import it.pagopa.selfcare.user.model.UserToNotify; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import java.time.OffsetDateTime; + +class FdUserNotificationToSendTest { + + @Test + void fdUserNotificationToSend_ACTIVE() { + FdUserNotificationToSend notification = new FdUserNotificationToSend(); + notification.setId("123"); + notification.setInstitutionId("inst-456"); + notification.setProduct("product-789"); + notification.setCreatedAt(OffsetDateTime.now().minusDays(1)); + notification.setUpdatedAt(OffsetDateTime.now()); + notification.setOnboardingTokenId("token-101112"); + notification.setType(NotificationUserType.ACTIVE_USER); + UserToNotify user = new UserToNotify(); + notification.setUser(user); + + assertEquals("123", notification.getId()); + assertEquals("inst-456", notification.getInstitutionId()); + assertEquals("product-789", notification.getProduct()); + assertNotNull(notification.getCreatedAt()); + assertNotNull(notification.getUpdatedAt()); + assertEquals("token-101112", notification.getOnboardingTokenId()); + assertEquals(NotificationUserType.ACTIVE_USER, notification.getType()); + assertEquals(user, notification.getUser()); + } + + @Test + void fdUserNotificationToSend_DELETE() { + FdUserNotificationToSend notification = new FdUserNotificationToSend(); + notification.setId("123"); + notification.setInstitutionId("inst-456"); + notification.setProduct("product-789"); + notification.setCreatedAt(OffsetDateTime.now().minusDays(1)); + notification.setUpdatedAt(OffsetDateTime.now()); + notification.setOnboardingTokenId("token-101112"); + notification.setType(NotificationUserType.DELETE_USER); + UserToNotify user = new UserToNotify(); + notification.setUser(user); + + assertEquals("123", notification.getId()); + assertEquals("inst-456", notification.getInstitutionId()); + assertEquals("product-789", notification.getProduct()); + assertNotNull(notification.getCreatedAt()); + assertNotNull(notification.getUpdatedAt()); + assertEquals("token-101112", notification.getOnboardingTokenId()); + assertEquals(NotificationUserType.DELETE_USER, notification.getType()); + assertEquals(user, notification.getUser()); + } + + @Test + void fdUserNotificationToSend_SUSPEND() { + FdUserNotificationToSend notification = new FdUserNotificationToSend(); + notification.setId("123"); + notification.setInstitutionId("inst-456"); + notification.setProduct("product-789"); + notification.setCreatedAt(OffsetDateTime.now().minusDays(1)); + notification.setUpdatedAt(OffsetDateTime.now()); + notification.setOnboardingTokenId("token-101112"); + notification.setType(NotificationUserType.SUSPEND_USER); + UserToNotify user = new UserToNotify(); + notification.setUser(user); + + assertEquals("123", notification.getId()); + assertEquals("inst-456", notification.getInstitutionId()); + assertEquals("product-789", notification.getProduct()); + assertNotNull(notification.getCreatedAt()); + assertNotNull(notification.getUpdatedAt()); + assertEquals("token-101112", notification.getOnboardingTokenId()); + assertEquals(NotificationUserType.SUSPEND_USER, notification.getType()); + assertEquals(user, notification.getUser()); + } + + @Test + void fdUserNotificationToSend_withNullFields_shouldHandleNullValues() { + FdUserNotificationToSend notification = new FdUserNotificationToSend(); + + assertNull(notification.getId()); + assertNull(notification.getInstitutionId()); + assertNull(notification.getProduct()); + assertNull(notification.getCreatedAt()); + assertNull(notification.getUpdatedAt()); + assertNull(notification.getOnboardingTokenId()); + assertNull(notification.getType()); + assertNull(notification.getUser()); + } + + @Test + void fdUserNotificationToSend_withPartialData_shouldSetFieldsCorrectly() { + FdUserNotificationToSend notification = new FdUserNotificationToSend(); + notification.setId("123"); + notification.setProduct("product-789"); + + assertEquals("123", notification.getId()); + assertNull(notification.getInstitutionId()); + assertEquals("product-789", notification.getProduct()); + assertNull(notification.getCreatedAt()); + assertNull(notification.getUpdatedAt()); + assertNull(notification.getOnboardingTokenId()); + assertNull(notification.getType()); + assertNull(notification.getUser()); + } +} diff --git a/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/TrackEventInputTest.java b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/TrackEventInputTest.java index 48970c6a..cd556aad 100644 --- a/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/TrackEventInputTest.java +++ b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/TrackEventInputTest.java @@ -1,9 +1,6 @@ package it.pagopa.selfcare.user; -import it.pagopa.selfcare.user.model.TrackEventInput; -import it.pagopa.selfcare.user.model.UserGroupNotificationToSend; -import it.pagopa.selfcare.user.model.UserNotificationToSend; -import it.pagopa.selfcare.user.model.UserToNotify; +import it.pagopa.selfcare.user.model.*; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -34,6 +31,26 @@ void toTrackEventInput_withUserNotification() { assertEquals("productRole", result.getProductRole()); } + @Test + void toTrackEventInput_withUserFdNotification() { + FdUserNotificationToSend userNotification = new FdUserNotificationToSend(); + userNotification.setId("docKey"); + UserToNotify user = new UserToNotify(); + user.setUserId("userId"); + user.setProductRole("productRole"); + userNotification.setUser(user); + userNotification.setInstitutionId("institutionId"); + userNotification.setProduct("productId"); + + TrackEventInput result = TrackEventInput.toTrackEventInput(userNotification); + + assertEquals("docKey", result.getDocumentKey()); + assertEquals("userId", result.getUserId()); + assertEquals("institutionId", result.getInstitutionId()); + assertEquals("productId", result.getProductId()); + assertEquals("productRole", result.getProductRole()); + } + @Test void toTrackEventInputForUserGroup_withUserGroupNotification() { UserGroupNotificationToSend userGroupNotification = new UserGroupNotificationToSend(); diff --git a/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/UserUtilsTest.java b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/UserUtilsTest.java index b6647ea9..f8cfe5f0 100644 --- a/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/UserUtilsTest.java +++ b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/UserUtilsTest.java @@ -9,10 +9,7 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -88,4 +85,118 @@ OnboardedProduct dummyOnboardedProduct(String productRole, OnboardedProductState onboardedProduct.setStatus(state); return onboardedProduct; } + + @Test + void getSASToken_withValidInputs_shouldReturnValidToken() { + String resourceUri = "https://example.com/resource"; + String keyName = "keyName"; + String key = "secretKey"; + + String sasToken = UserUtils.getSASToken(resourceUri, keyName, key); + + assertNotNull(sasToken); + assertTrue(sasToken.contains("SharedAccessSignature sr=")); + assertTrue(sasToken.contains("&sig=")); + assertTrue(sasToken.contains("&se=")); + assertTrue(sasToken.contains("&skn=")); + } + + @Test + void getSASToken_withInvalidEncoding_shouldHandleException() { + String resourceUri = "https://example.com/resource"; + String keyName = "keyName"; + String key = "secretKey"; + + String sasToken = UserUtils.getSASToken(resourceUri, keyName, key); + + assertNotNull(sasToken); + } + + @Test + void getHMAC256_withValidInputs_shouldReturnValidHash() { + String key = "secretKey"; + String input = "inputString"; + + String hash = UserUtils.getHMAC256(key, input); + + assertNotNull(hash); + } + + @Test + void retrieveFdProductIfItChanged_withValidProducts_shouldReturnMostRecentlyUpdatedProduct() { + List products = new ArrayList<>(); + OnboardedProduct product1 = new OnboardedProduct(); + product1.setProductId("1"); + product1.setUpdatedAt(OffsetDateTime.now().minusDays(1)); + product1.setCreatedAt(OffsetDateTime.now().minusDays(2)); + products.add(product1); + + OnboardedProduct product2 = new OnboardedProduct(); + product2.setProductId("2"); + product2.setUpdatedAt(OffsetDateTime.now()); + product2.setCreatedAt(OffsetDateTime.now().minusDays(1)); + products.add(product2); + + List productIdToCheck = List.of("1", "2"); + + OnboardedProduct result = UserUtils.retrieveFdProductIfItChanged(products, productIdToCheck); + + assertNotNull(result); + assertEquals("2", result.getProductId()); + } + + @Test + void retrieveFdProductIfItChanged_withEmptyProductList() { + List products = Collections.emptyList(); + List productIdToCheck = List.of("1", "2"); + + OnboardedProduct result = UserUtils.retrieveFdProductIfItChanged(products, productIdToCheck); + + assertNull(result); + } + + @Test + void retrieveFdProductIfItChanged_withNoFdProductIds() { + List products = new ArrayList<>(); + OnboardedProduct product1 = new OnboardedProduct(); + product1.setProductId("1"); + product1.setUpdatedAt(OffsetDateTime.now().minusDays(1)); + product1.setCreatedAt(OffsetDateTime.now().minusDays(2)); + products.add(product1); + + OnboardedProduct product2 = new OnboardedProduct(); + product2.setProductId("2"); + product2.setUpdatedAt(OffsetDateTime.now()); + product2.setCreatedAt(OffsetDateTime.now().minusDays(1)); + products.add(product2); + + List productIdToCheck = List.of("3", "4"); + + OnboardedProduct result = UserUtils.retrieveFdProductIfItChanged(products, productIdToCheck); + + assertNull(result); + } + + @Test + void retrieveFdProductIfItChanged_withProductsWithSameUpdatedAt() { + List products = new ArrayList<>(); + OnboardedProduct product1 = new OnboardedProduct(); + product1.setProductId("1"); + product1.setUpdatedAt(OffsetDateTime.now()); + product1.setCreatedAt(OffsetDateTime.now().minusDays(2)); + products.add(product1); + + OnboardedProduct product2 = new OnboardedProduct(); + product2.setProductId("2"); + product2.setUpdatedAt(OffsetDateTime.now()); + product2.setCreatedAt(OffsetDateTime.now().minusDays(1)); + products.add(product2); + + List productIdToCheck = List.of("1", "2"); + + OnboardedProduct result = UserUtils.retrieveFdProductIfItChanged(products, productIdToCheck); + + assertNotNull(result); + assertEquals("2", result.getProductId()); + } } diff --git a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java index 01194e55..913ba15a 100644 --- a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java +++ b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java @@ -8,6 +8,11 @@ public class EventsMetric { public static final String EVENTS_USER_INSTITUTION_PRODUCT_FAILURE = "EventsUserInstitutionProduct_failures"; public static final String EVENTS_USER_INSTITUTION_PRODUCT_SUCCESS = "EventsUserInstitutionProduct_success"; + public static final String FD_EVENTS_USER_INSTITUTION_FAILURE = "EventsUserInstitution_failures"; + public static final String FD_EVENTS_USER_INSTITUTION_SUCCESS = "EventsUserInstitution_success"; + public static final String FD_EVENTS_USER_INSTITUTION_PRODUCT_FAILURE = "EventsUserInstitutionProduct_failures"; + public static final String FD_EVENTS_USER_INSTITUTION_PRODUCT_SUCCESS = "EventsUserInstitutionProduct_success"; + public static final String EVENTS_USER_GROUP_FAILURE = "EventsUserGroup_failures"; public static final String EVENTS_USER_GROUP_SUCCESS = "EventsUserGroup_success"; public static final String EVENTS_USER_GROUP_PRODUCT_FAILURE = "EventsUserGroupProduct_failures"; diff --git a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java index 37a0a53f..820ae2aa 100644 --- a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java +++ b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java @@ -4,5 +4,7 @@ public class EventsName { public static final String EVENT_USER_MS_NAME = "USER_MS"; public static final String EVENT_USER_CDC_NAME = "USER_CDC"; + + public static final String FD_EVENT_USER_CDC_NAME = "FD_USER_CDC"; public static final String EVENT_USER_GROUP_CDC_NAME = "USER_GROUP_CDC"; }