From a4b96474e8f71f0d8c0ac7a7f42789f7d48fe8cd Mon Sep 17 00:00:00 2001 From: Ruben Vardanyan Date: Tue, 27 Feb 2024 05:08:23 +0400 Subject: [PATCH] Disabling firebase push notification route tokens in case of unregistered or expired tokens. --- .../push/PushNotificationProcessorImpl.java | 17 +++++++- .../PushNotificationProcessorImplTest.java | 36 ++++++++++++++++ .../firebase/FirebasePushMessageSender.java | 12 ++++++ .../FirebasePushMessageSenderTest.java | 42 ++++++++++++++++++- ...otificationInvalidRouteTokenException.java | 15 +++++++ 5 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 notification-provider-spi/src/main/java/com/sflpro/notifier/spi/exception/PushNotificationInvalidRouteTokenException.java diff --git a/core/core-services-impl/src/main/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImpl.java b/core/core-services-impl/src/main/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImpl.java index f10bcd5b..3aba9fa5 100644 --- a/core/core-services-impl/src/main/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImpl.java +++ b/core/core-services-impl/src/main/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImpl.java @@ -6,10 +6,13 @@ import com.sflpro.notifier.db.entities.notification.push.PushNotification; import com.sflpro.notifier.db.entities.notification.push.PushNotificationProviderType; import com.sflpro.notifier.db.entities.notification.push.PushNotificationRecipient; +import com.sflpro.notifier.db.entities.notification.push.PushNotificationRecipientStatus; import com.sflpro.notifier.services.common.exception.ServicesRuntimeException; import com.sflpro.notifier.services.notification.exception.NotificationInvalidStateException; import com.sflpro.notifier.services.notification.push.PushNotificationProcessor; +import com.sflpro.notifier.services.notification.push.PushNotificationRecipientService; import com.sflpro.notifier.services.notification.push.PushNotificationService; +import com.sflpro.notifier.spi.exception.PushNotificationInvalidRouteTokenException; import com.sflpro.notifier.spi.push.PlatformType; import com.sflpro.notifier.spi.push.PushMessage; import com.sflpro.notifier.spi.push.PushMessageSender; @@ -55,6 +58,9 @@ public class PushNotificationProcessorImpl implements PushNotificationProcessor @Autowired private TemplateContentResolver templateContentResolver; + @Autowired + private PushNotificationRecipientService pushNotificationRecipientService; + PushNotificationProcessorImpl() { logger.debug("Initializing push notification processing service"); } @@ -80,7 +86,16 @@ public void processNotification(@Nonnull final Long notificationId, @Nonnull fin } // Mark push notification as processed updatePushNotificationState(notificationId, NotificationState.SENT); - } catch (final Exception ex) { + } + catch (final PushNotificationInvalidRouteTokenException ex) { + logger.warn("Failed to process push notification with id - {}, because of invalid token of recipient with id - {}", notificationId, pushNotification.getRecipient().getId()); + updatePushNotificationState(notificationId, NotificationState.FAILED); + pushNotificationRecipientService.updatePushNotificationRecipientStatus( + pushNotification.getRecipient().getId(), + PushNotificationRecipientStatus.DISABLED + ); + } + catch (final Exception ex) { final String message = "Error occurred while processing push notification with id - " + notificationId; updatePushNotificationState(notificationId, NotificationState.FAILED); throw new ServicesRuntimeException(message, ex); diff --git a/core/core-services-impl/src/test/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImplTest.java b/core/core-services-impl/src/test/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImplTest.java index 7a1b1531..fc7fb5b2 100644 --- a/core/core-services-impl/src/test/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImplTest.java +++ b/core/core-services-impl/src/test/java/com/sflpro/notifier/services/notification/impl/push/PushNotificationProcessorImplTest.java @@ -3,9 +3,12 @@ import com.sflpro.notifier.db.entities.notification.NotificationState; import com.sflpro.notifier.db.entities.notification.push.PushNotification; import com.sflpro.notifier.db.entities.notification.push.PushNotificationRecipient; +import com.sflpro.notifier.db.entities.notification.push.PushNotificationRecipientStatus; import com.sflpro.notifier.services.notification.exception.NotificationInvalidStateException; +import com.sflpro.notifier.services.notification.push.PushNotificationRecipientService; import com.sflpro.notifier.services.notification.push.PushNotificationService; import com.sflpro.notifier.services.test.AbstractServicesUnitTest; +import com.sflpro.notifier.spi.exception.PushNotificationInvalidRouteTokenException; import com.sflpro.notifier.spi.push.PushMessage; import com.sflpro.notifier.spi.push.PushMessageSender; import com.sflpro.notifier.spi.push.PushMessageSendingResult; @@ -41,6 +44,8 @@ public class PushNotificationProcessorImplTest extends AbstractServicesUnitTest @Mock private PushMessageSender pushMessageSender; + @Mock + private PushNotificationRecipientService pushNotificationRecipientService; /* Constructors */ public PushNotificationProcessorImplTest() { @@ -126,6 +131,37 @@ public void testProcessPushNotificationWhenExceptionOccursDuringProcessing() { verifyAll(); } + public void testProcessPushNotificationWhenInvalidTokenExceptionOccursDuringProcessing() { + // Test data + final Long notificationId = 1L; + final PushNotification notification = getServicesImplTestHelper().createPushNotification(); + notification.setId(notificationId); + notification.setState(NotificationState.CREATED); + final Long recipientId = 2L; + final PushNotificationRecipient recipient = getServicesImplTestHelper().createPushNotificationSnsRecipient(); + recipient.setId(recipientId); + notification.setRecipient(recipient); + final PushNotificationInvalidRouteTokenException exceptionDuringSending = new PushNotificationInvalidRouteTokenException( + UUID.randomUUID().toString(), "Exception for testing error flow", null + ); + // Reset + resetAll(); + // Expectations + expect(pushNotificationService.getPushNotificationForProcessing(eq(notificationId))).andReturn(notification).once(); + expect(pushNotificationService.updateNotificationState(notificationId, NotificationState.PROCESSING)).andReturn(notification).once(); + expect(pushMessageServiceProvider.lookupPushMessageSender(notification.getRecipient().getType())) + .andReturn(Optional.of(pushMessageSender)); + expect(pushMessageSender.send(isA(PushMessage.class))).andThrow(exceptionDuringSending); + expect(pushNotificationService.updateNotificationState(notificationId, NotificationState.FAILED)).andReturn(notification).once(); + expect(pushNotificationRecipientService.updatePushNotificationRecipientStatus(notification.getRecipient().getId(), PushNotificationRecipientStatus.DISABLED)); + // Replay + replayAll(); + // Run test scenario + pushNotificationProcessingService.processNotification(notificationId, Collections.emptyMap()); + // Verify + verifyAll(); + } + @Test public void testProcessPushNotification() { // Test data diff --git a/infra/infra-integrations/infra-integrations-push/src/main/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSender.java b/infra/infra-integrations/infra-integrations-push/src/main/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSender.java index ffa145bd..41836ecc 100644 --- a/infra/infra-integrations/infra-integrations-push/src/main/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSender.java +++ b/infra/infra-integrations/infra-integrations-push/src/main/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSender.java @@ -1,6 +1,7 @@ package com.sflpro.notifier.externalclients.push.firebase; import com.google.firebase.messaging.*; +import com.sflpro.notifier.spi.exception.PushNotificationInvalidRouteTokenException; import com.sflpro.notifier.spi.push.PlatformType; import com.sflpro.notifier.spi.push.PushMessage; import com.sflpro.notifier.spi.push.PushMessageSender; @@ -27,6 +28,8 @@ class FirebasePushMessageSender implements PushMessageSender { private static final Logger logger = LoggerFactory.getLogger(FirebasePushMessageSender.class); + private static final String UNREGISTERED_ERROR_CODE = "UNREGISTERED"; + private static final String TITLE = "title"; private static final String BODY = "body"; @@ -62,6 +65,15 @@ public PushMessageSendingResult send(final PushMessage message) { platformConfigurationHandler(message.platformType()).ifPresent(handler -> handler.accept(message, builder)); return PushMessageSendingResult.of(firebaseMessaging.send(builder.build())); } catch (final FirebaseMessagingException ex) { + final String errorCode = ex.getErrorCode(); + if(UNREGISTERED_ERROR_CODE.equals(errorCode)) { + logger.debug("Unable to send message with subject {}, firebase route token is not registered", message.subject()); + throw new PushNotificationInvalidRouteTokenException( + message.destinationRouteToken(), + "Firebase notification sender failed to send message with subject " + message.subject() + " with error" + errorCode, + ex + ); + } logger.error("Unable to send message with subject {}.", message.subject()); throw new MessageSendingFaildException("Filed to send message using firebase cloud messaging.", ex); } diff --git a/infra/infra-integrations/infra-integrations-push/src/test/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSenderTest.java b/infra/infra-integrations/infra-integrations-push/src/test/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSenderTest.java index 554b1a1e..9ce40301 100644 --- a/infra/infra-integrations/infra-integrations-push/src/test/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSenderTest.java +++ b/infra/infra-integrations/infra-integrations-push/src/test/java/com/sflpro/notifier/externalclients/push/firebase/FirebasePushMessageSenderTest.java @@ -4,19 +4,20 @@ import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; import com.sflpro.notifier.externalclients.push.test.AbstractPushNotificationUnitTest; +import com.sflpro.notifier.spi.exception.PushNotificationInvalidRouteTokenException; import com.sflpro.notifier.spi.push.PlatformType; import com.sflpro.notifier.spi.push.PushMessage; import com.sflpro.notifier.spi.push.PushMessageSender; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; +import org.mockito.Mockito; import java.util.HashMap; import java.util.Map; import java.util.Properties; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.*; @@ -177,6 +178,43 @@ public void testSendToIOSDeviceWithProvidedConfigs() throws FirebaseMessagingExc verifyZeroInteractions(defaultAndroidConfig, defaultApnsConfig); } + @Test + public void testSendWithUnregisteredTokenThrowsException() throws FirebaseMessagingException { + final Map properties = new HashMap<>(); + properties.put(uuid(), uuid()); + final FirebaseMessagingException firebaseException = mock(FirebaseMessagingException.class); + final PushMessage message = PushMessage.of( + uuid(), + uuid(), + uuid(), + PlatformType.GCM, + properties + ); + properties.put("title", message.subject()); + properties.put("body", message.body()); + final String messageId = uuid(); + final String ttlKey = "ttl"; + final String priorityKey = "priority"; + final String collapseKey = "collapseKey"; + final String restrictedPackageNameKey = "restrictedPackageName"; + when(defaultAndroidConfig.getProperty(ttlKey)).thenReturn("10"); + when(defaultAndroidConfig.getProperty(priorityKey)).thenReturn("HIGH"); + when(defaultAndroidConfig.getProperty(collapseKey)).thenReturn(uuid()); + when(defaultAndroidConfig.getProperty(restrictedPackageNameKey)).thenReturn(uuid()); + when(firebaseMessaging.send(isA(Message.class))).thenThrow(firebaseException); + when(firebaseException.getErrorCode()).thenReturn("UNREGISTERED"); + assertThatThrownBy(() -> pushMessageSender.send(message)) + .isInstanceOf(PushNotificationInvalidRouteTokenException.class) + .hasFieldOrPropertyWithValue("routeToken", message.destinationRouteToken()) + .hasFieldOrPropertyWithValue("cause", firebaseException); + verify(firebaseMessaging).send(isA(Message.class)); + verify(defaultAndroidConfig).getProperty(ttlKey); + verify(defaultAndroidConfig).getProperty(priorityKey); + verify(defaultAndroidConfig).getProperty(collapseKey); + verify(defaultAndroidConfig).getProperty(restrictedPackageNameKey); + verifyZeroInteractions(defaultApnsConfig); + } + private static void checkProperties(final PushMessage pushMessage, final Message message) { assertThat(message) .hasFieldOrPropertyWithValue("data", pushMessage.properties()) diff --git a/notification-provider-spi/src/main/java/com/sflpro/notifier/spi/exception/PushNotificationInvalidRouteTokenException.java b/notification-provider-spi/src/main/java/com/sflpro/notifier/spi/exception/PushNotificationInvalidRouteTokenException.java new file mode 100644 index 00000000..a689b75e --- /dev/null +++ b/notification-provider-spi/src/main/java/com/sflpro/notifier/spi/exception/PushNotificationInvalidRouteTokenException.java @@ -0,0 +1,15 @@ +package com.sflpro.notifier.spi.exception; + +public class PushNotificationInvalidRouteTokenException extends RuntimeException { + + private final String routeToken; + + public PushNotificationInvalidRouteTokenException(final String routeToken, final String message, final Throwable cause) { + super(message, cause); + this.routeToken = routeToken; + } + + public String getRouteToken() { + return routeToken; + } +}