apnsTemplate;
@Scheduled(fixedDelayString = "${robert.push.server.scheduler.delay-in-ms}")
@Timed(value = "push.notifier.duration", description = "on going export duration", longTask = true)
@Counted(value = "push.notifier.calls", description = "count each time the scheduler sending notifications is triggered")
public void sendNotifications() {
+ pushInfoRepository.forEachNotificationToBeSent(pushInfo -> {
+ // set the next planned push to be sure the notification could not be sent 2
+ // times the same day
+ updateNextPlannedPush(pushInfo);
+ final var notification = buildWakeUpNotification(pushInfo.getToken());
+ apnsTemplate.sendNotification(notification, new WakeUpDeviceResponseHandler(pushInfo));
+ });
+
+ apnsTemplate.waitUntilNoActivity(robertPushServerProperties.getBatchTerminationGraceTime());
+ }
+
+ /**
+ * Updates the registered token with a new notification instant set to tomorrow.
+ */
+ private void updateNextPlannedPush(final PushInfo pushInfo) {
+ final var nextPushDate = generatePushDateTomorrowBetween(
+ robertPushServerProperties.getMinPushHour(),
+ robertPushServerProperties.getMaxPushHour(),
+ ZoneId.of(pushInfo.getTimezone())
+ );
+ pushInfoRepository.updateNextPlannedPushDate(pushInfo.getId(), nextPushDate);
+ }
- // use a RowCallBackHandler in order to process a large resultset on a per-row
- // basis.
- jdbcTemplate.query(
- "select * from push where active = true and deleted = false and next_planned_push <= now()",
- new PushNotificationRowCallbackHandler()
+ /**
+ * Builds the {@link com.eatthepath.pushy.apns.ApnsPushNotification} to be sent
+ * to the Apple server.
+ */
+ private SimpleApnsPushNotification buildWakeUpNotification(final String apnsToken) {
+ final var payload = new SimpleApnsPayloadBuilder()
+ .setContentAvailable(true)
+ .setBadgeNumber(0)
+ .build();
+
+ return new SimpleApnsPushNotification(
+ sanitizeTokenString(apnsToken).toLowerCase(),
+ robertPushServerProperties.getApns().getTopic(),
+ payload,
+ Instant.now().plus(DEFAULT_EXPIRATION_PERIOD),
+ DeliveryPriority.IMMEDIATE,
+ PushType.BACKGROUND
);
+ }
- apnsTemplate.waitUntilNoActivity(Duration.ofSeconds(10));
+ /**
+ * Generates a random instant tomorrow between the given hour bounds for the
+ * specified timezone.
+ *
+ * minPushHour can be greater than maxPushHour: its means notification period
+ * starts this evening and ends tommorrow. For instance min=20 and max=7 means
+ * notifications are send between today at 20:00 and tomorrow at 6:59.
+ */
+ static Instant generatePushDateTomorrowBetween(final int minPushHour, final int maxPushHour,
+ final ZoneId timezone) {
+ final var random = ThreadLocalRandom.current();
+ final int durationBetweenHours;
+ // In case config requires "between 6pm and 4am" which translates in minPushHour
+ // = 18 and maxPushHour = 4
+ if (maxPushHour < minPushHour) {
+ durationBetweenHours = 24 - minPushHour + maxPushHour;
+ } else {
+ durationBetweenHours = maxPushHour - minPushHour;
+ }
+ return ZonedDateTime.now(timezone).plusDays(1)
+ .withHour((random.nextInt(durationBetweenHours) + minPushHour) % 24)
+ .withMinute(random.nextInt(60))
+ .toInstant()
+ .truncatedTo(MINUTES);
}
+ /**
+ * Handles notification request response.
+ */
@RequiredArgsConstructor
- private class PushNotificationRowCallbackHandler implements RowCallbackHandler {
+ private class WakeUpDeviceResponseHandler implements FailoverApnsResponseHandler {
+
+ private final PushInfo pushInfo;
@Override
- public void processRow(final ResultSet resultSet) throws SQLException {
- PushInfo pushInfo = rowMapper.mapRow(resultSet, resultSet.getRow());
+ public void onSuccess() {
+ pushInfoRepository.updateSuccessfulPushSent(pushInfo.getId());
+ }
- // set the next planned push to be sure the notification could not be sent 2
- // times the same day
- PushInfoNotificationHandler handler = new PushInfoNotificationHandler(
- pushInfo,
- pushInfoDao,
- robertPushServerProperties.getApns().getTopic(),
- robertPushServerProperties.getMinPushHour(),
- robertPushServerProperties.getMaxPushHour()
- );
+ @Override
+ public void onRejection(final List reasons) {
+
+ pushInfoRepository.updateFailure(pushInfo.getId(), concat(reasons));
+ }
+
+ @Override
+ public void onError(final Throwable cause) {
+ pushInfoRepository.updateFailure(pushInfo.getId(), cause.getMessage());
+ }
- handler.updateNextPlannedPushToRandomTomorrow();
+ @Override
+ public void onInactive(final List reasons) {
+ pushInfoRepository.updateFailure(pushInfo.getId(), concat(reasons));
+ pushInfoRepository.disable(pushInfo.getId());
+ }
- apnsTemplate.sendNotification(handler);
+ private String concat(List reasons) {
+ return reasons.stream()
+ .map(RejectionReason::getValue)
+ .collect(joining(","));
}
}
}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/MicrometerApnsClientMetricsListener.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/MicrometerApnsClientMetricsListener.java
index 4f63b88fd97351049fd53d2c28db4d3a19facb2c..7f53a46a42ef9fe0450fa78f9b39945c7861b7b6 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/MicrometerApnsClientMetricsListener.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/MicrometerApnsClientMetricsListener.java
@@ -2,9 +2,9 @@ package fr.gouv.stopc.robert.pushnotif.scheduler.apns;
import com.eatthepath.pushy.apns.ApnsClient;
import com.eatthepath.pushy.apns.ApnsClientMetricsListener;
+import fr.gouv.stopc.robert.pushnotif.scheduler.apns.template.ApnsServerCoordinates;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import java.util.concurrent.atomic.AtomicInteger;
@@ -69,13 +69,16 @@ public class MicrometerApnsClientMetricsListener implements ApnsClientMetricsLis
* Constructs a new Micrometer metrics listener that adds metrics to the given
* registry with the given list of tags.
*
- * @param meterRegistry the registry to which to add metrics
- * @param host the apns server host
- * @param port the apns server port
+ * @param meterRegistry the registry to which to add metrics
+ * @param serverCoordinates the apns server host and port
*/
- public MicrometerApnsClientMetricsListener(final MeterRegistry meterRegistry, String host, int port) {
+ public MicrometerApnsClientMetricsListener(final MeterRegistry meterRegistry,
+ final ApnsServerCoordinates serverCoordinates) {
- Iterable tags = Tags.of("host", host, "port", "" + port);
+ final var tags = Tags.of(
+ "host", serverCoordinates.getHost(),
+ "port", String.valueOf(serverCoordinates.getPort())
+ );
this.writeFailures = meterRegistry.counter(WRITE_FAILURES_COUNTER_NAME, tags);
this.sentNotifications = meterRegistry.counter(SENT_NOTIFICATIONS_COUNTER_NAME, tags);
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/PushInfoNotificationHandler.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/PushInfoNotificationHandler.java
deleted file mode 100644
index 7aaef4e9bc5cef872e32a08e0d1a42bdce5c518e..0000000000000000000000000000000000000000
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/PushInfoNotificationHandler.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package fr.gouv.stopc.robert.pushnotif.scheduler.apns;
-
-import com.eatthepath.pushy.apns.DeliveryPriority;
-import com.eatthepath.pushy.apns.PushType;
-import com.eatthepath.pushy.apns.util.SimpleApnsPayloadBuilder;
-import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
-import fr.gouv.stopc.robert.pushnotif.scheduler.apns.template.NotificationHandler;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.PushInfoDao;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
-import lombok.RequiredArgsConstructor;
-
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.util.concurrent.ThreadLocalRandom;
-
-import static com.eatthepath.pushy.apns.util.SimpleApnsPushNotification.DEFAULT_EXPIRATION_PERIOD;
-import static com.eatthepath.pushy.apns.util.TokenUtil.sanitizeTokenString;
-import static java.time.temporal.ChronoUnit.MINUTES;
-import static org.apache.commons.lang3.StringUtils.truncate;
-
-@RequiredArgsConstructor
-public class PushInfoNotificationHandler implements NotificationHandler {
-
- private final PushInfo notificationData;
-
- private final PushInfoDao pushInfoDao;
-
- private final String apnsTopic;
-
- private final int minPushHour;
-
- private final int maxPushHour;
-
- @Override
- public String getAppleToken() {
- return notificationData.getToken();
- }
-
- @Override
- public void onSuccess() {
- notificationData.setLastSuccessfulPush(Instant.now());
- notificationData.setSuccessfulPushSent(notificationData.getSuccessfulPushSent() + 1);
- pushInfoDao.updateSuccessFulPushedNotif(notificationData);
- }
-
- @Override
- public void onRejection(final RejectionReason reason) {
- notificationData.setLastErrorCode(reason.getValue());
- notificationData.setLastFailurePush(Instant.now());
- notificationData.setFailedPushSent(notificationData.getFailedPushSent() + 1);
- pushInfoDao.updateFailurePushedNotif(notificationData);
- }
-
- @Override
- public void onError(final Throwable cause) {
- notificationData.setLastErrorCode(truncate(cause.getMessage(), 255));
- notificationData.setLastFailurePush(Instant.now());
- notificationData.setFailedPushSent(notificationData.getFailedPushSent() + 1);
- pushInfoDao.updateFailurePushedNotif(notificationData);
- }
-
- @Override
- public void disableToken() {
- notificationData.setActive(false);
- }
-
- @Override
- public SimpleApnsPushNotification buildNotification() {
-
- final String payload = new SimpleApnsPayloadBuilder()
- .setContentAvailable(true)
- .setBadgeNumber(0)
- .build();
-
- return new SimpleApnsPushNotification(
- sanitizeTokenString(getAppleToken()).toLowerCase(),
- apnsTopic,
- payload,
- Instant.now().plus(DEFAULT_EXPIRATION_PERIOD),
- DeliveryPriority.IMMEDIATE,
- PushType.BACKGROUND
- );
- }
-
- public void updateNextPlannedPushToRandomTomorrow() {
- notificationData.setNextPlannedPush(generateDateTomorrowBetweenBounds(notificationData.getTimezone()));
- pushInfoDao.updateNextPlannedPushDate(notificationData);
- }
-
- private Instant generateDateTomorrowBetweenBounds(final String timezone) {
-
- final var random = ThreadLocalRandom.current();
-
- final int durationBetweenHours;
- // In case config requires "between 6pm and 4am" which translates in minPushHour
- // = 18 and maxPushHour = 4
- if (maxPushHour < minPushHour) {
- durationBetweenHours = 24 - minPushHour + maxPushHour;
- } else {
- durationBetweenHours = maxPushHour - minPushHour;
- }
-
- return ZonedDateTime.now(ZoneId.of(timezone)).plusDays(1)
- .withHour(random.nextInt(durationBetweenHours) + minPushHour % 24)
- .withMinute(random.nextInt(60))
- .toInstant()
- .truncatedTo(MINUTES);
- }
-}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsOperations.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsOperations.java
index f68c386bc5c749df32934b0a3b4c9da6ec9055e4..b68cfb93ef62d89b09d50ecf2ed6e9322f3c40b9 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsOperations.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsOperations.java
@@ -1,10 +1,12 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
+import com.eatthepath.pushy.apns.ApnsPushNotification;
+
import java.time.Duration;
-public interface ApnsOperations extends AutoCloseable {
+public interface ApnsOperations extends AutoCloseable {
- void sendNotification(NotificationHandler handler);
+ void sendNotification(ApnsPushNotification notification, T handler);
void waitUntilNoActivity(Duration toleranceDuration);
}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsResponseHandler.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsResponseHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..baf5ec21e2f0177f7d98ef96729bcc22286891d3
--- /dev/null
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsResponseHandler.java
@@ -0,0 +1,35 @@
+package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
+
+import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
+import fr.gouv.stopc.robert.pushnotif.scheduler.configuration.RobertPushServerProperties;
+
+public interface ApnsResponseHandler {
+
+ /**
+ * Called when the notification request is accepted
+ */
+ void onSuccess();
+
+ /**
+ * Called when the notification request is rejected
+ *
+ * @param reason rejected push notification request response message
+ */
+ void onRejection(final RejectionReason reason);
+
+ /**
+ * Called when the notification request fails before reaching Apple server.
+ *
+ * @param cause error message
+ */
+ void onError(final Throwable cause);
+
+ /**
+ * Called when the notification request is rejected because one of inactive
+ * rejection reasons.
+ *
+ * @param reason rejected push notification request response message
+ * @see RobertPushServerProperties.Apns#getInactiveRejectionReason()
+ */
+ void onInactive(RejectionReason reason);
+}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsServerCoordinates.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsServerCoordinates.java
new file mode 100644
index 0000000000000000000000000000000000000000..9df0da6d3f2b1c06f3d791c1248e4099ae8552e4
--- /dev/null
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsServerCoordinates.java
@@ -0,0 +1,16 @@
+package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
+
+import lombok.Value;
+
+@Value
+public class ApnsServerCoordinates {
+
+ String host;
+
+ int port;
+
+ @Override
+ public String toString() {
+ return String.format("%s:%d", host, port);
+ }
+}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsTemplate.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsTemplate.java
index b3069cf7b9ceefd1afe336d80d046f546a17ef5c..7cffdd0d5e864ce2004f8c559d555dbca91c45e9 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsTemplate.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/ApnsTemplate.java
@@ -1,11 +1,13 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
import com.eatthepath.pushy.apns.ApnsClient;
+import com.eatthepath.pushy.apns.ApnsPushNotification;
import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
+import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason.UNKNOWN;
@@ -17,35 +19,43 @@ import static java.util.concurrent.TimeUnit.SECONDS;
*/
@Slf4j
@RequiredArgsConstructor
-public class ApnsTemplate implements ApnsOperations {
+public class ApnsTemplate implements ApnsOperations {
private final AtomicInteger pendingNotifications = new AtomicInteger(0);
+ private final ApnsServerCoordinates serverCoordinates;
+
private final ApnsClient apnsClient;
- public void sendNotification(final NotificationHandler notificationHandler) {
+ private final List inactiveRejectionReasons;
+
+ public void sendNotification(final ApnsPushNotification notification,
+ final ApnsResponseHandler responseHandler) {
pendingNotifications.incrementAndGet();
- final var sendNotificationFuture = apnsClient.sendNotification(notificationHandler.buildNotification());
+ final var sendNotificationFuture = apnsClient.sendNotification(notification);
sendNotificationFuture.whenComplete((response, cause) -> {
pendingNotifications.decrementAndGet();
if (response != null) {
if (response.isAccepted()) {
- notificationHandler.onSuccess();
+ responseHandler.onSuccess();
} else {
- notificationHandler.onRejection(
- response.getRejectionReason()
- .map(RejectionReason::fromValue)
- .orElse(UNKNOWN)
- );
+ final var rejection = response.getRejectionReason()
+ .map(RejectionReason::fromValue)
+ .orElse(UNKNOWN);
+ if (inactiveRejectionReasons.contains(rejection)) {
+ responseHandler.onInactive(rejection);
+ } else {
+ responseHandler.onRejection(rejection);
+ }
}
} else {
// Something went wrong when trying to send the notification to the
// APNs server. Note that this is distinct from a rejection from
// the server, and indicates that something went wrong when actually
// sending the notification or waiting for a reply.
- notificationHandler.onError(cause);
+ responseHandler.onError(cause);
}
}).exceptionally(e -> {
log.error("Unexpected error occurred", e);
@@ -62,11 +72,18 @@ public class ApnsTemplate implements ApnsOperations {
log.warn("Unable to wait until all notifications are sent", e);
}
} while (pendingNotifications.get() != 0);
+ log.info("{} has no more pending notifications");
}
@Override
public void close() throws Exception {
log.info("Shutting down {}, gracefully waiting 1 minute", this);
apnsClient.close().get(1, MINUTES);
+ log.info("{} is stopped", this);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ApnsTemplate(%s)", serverCoordinates);
}
}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/DelegateNotificationHandler.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/DelegateApnsResponseHandler.java
similarity index 51%
rename from robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/DelegateNotificationHandler.java
rename to robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/DelegateApnsResponseHandler.java
index 7c392e9dfabdc70cbd9e364e0e9ccaec1e19d36e..fcab3bd1886e618a76015e70d634b3c5f7411b83 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/DelegateNotificationHandler.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/DelegateApnsResponseHandler.java
@@ -1,21 +1,15 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
-import com.eatthepath.pushy.apns.ApnsPushNotification;
import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
import lombok.RequiredArgsConstructor;
/**
- * Base class for {@link NotificationHandler} decorators.
+ * Base class for {@link ApnsResponseHandler} decorators.
*/
@RequiredArgsConstructor
-public class DelegateNotificationHandler implements NotificationHandler {
+public class DelegateApnsResponseHandler implements ApnsResponseHandler {
- private final NotificationHandler delegate;
-
- @Override
- public String getAppleToken() {
- return delegate.getAppleToken();
- }
+ private final ApnsResponseHandler delegate;
@Override
public void onSuccess() {
@@ -33,12 +27,7 @@ public class DelegateNotificationHandler implements NotificationHandler {
}
@Override
- public void disableToken() {
- delegate.disableToken();
- }
-
- @Override
- public ApnsPushNotification buildNotification() {
- return delegate.buildNotification();
+ public void onInactive(RejectionReason reason) {
+ delegate.onInactive(reason);
}
}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/FailoverApnsResponseHandler.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/FailoverApnsResponseHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b1eb71a04c7102385611d4b2cd8888b0783b4b1
--- /dev/null
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/FailoverApnsResponseHandler.java
@@ -0,0 +1,37 @@
+package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
+
+import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
+import fr.gouv.stopc.robert.pushnotif.scheduler.configuration.RobertPushServerProperties;
+
+import java.util.List;
+
+public interface FailoverApnsResponseHandler {
+
+ /**
+ * Called when the notification request is accepted.
+ */
+ void onSuccess();
+
+ /**
+ * Called when the notification request is rejected.
+ *
+ * @param reasons rejected push response messages
+ */
+ void onRejection(List reasons);
+
+ /**
+ * Called when the notification request fails before reaching Apple server.
+ *
+ * @param reason error message
+ */
+ void onError(Throwable reason);
+
+ /**
+ * Called when the notification request is rejected because one of inactive
+ * rejection reasons.
+ *
+ * @param reasons rejected push response messages
+ * @see RobertPushServerProperties.Apns#getInactiveRejectionReason()
+ */
+ void onInactive(List reasons);
+}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/FailoverApnsTemplate.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/FailoverApnsTemplate.java
index f8f1fc1a2f4985be3c3dbaf96f84c59fd924ed5f..d3986d8aaa48e36e91f33128b7f4a0399dc1f1d7 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/FailoverApnsTemplate.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/FailoverApnsTemplate.java
@@ -1,65 +1,44 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
+import com.eatthepath.pushy.apns.ApnsPushNotification;
import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
+import static java.util.stream.Collectors.joining;
+
/**
* An APNS template able to defer notification sending to fallback servers
* depending on the reason returned from the previous server.
*/
@Slf4j
@RequiredArgsConstructor
-public class FailoverApnsTemplate implements ApnsOperations {
-
- private final List apnsDelegates;
+public class FailoverApnsTemplate implements ApnsOperations {
- private final List inactiveRejectionReasons;
+ private final List> apnsDelegates;
@Override
- public void sendNotification(final NotificationHandler notificationHandler) {
+ public void sendNotification(final ApnsPushNotification notification,
+ final FailoverApnsResponseHandler responseHandler) {
- final var apnsClientsQueue = new ConcurrentLinkedQueue<>(apnsDelegates);
-
- sendNotification(notificationHandler, apnsClientsQueue);
+ final var apnsTemplates = new ConcurrentLinkedQueue<>(apnsDelegates);
+ final var first = apnsTemplates.poll();
+ if (first != null) {
+ first.sendNotification(
+ notification,
+ new TryOnNextServerAfterInactiveResponseHandler(notification, apnsTemplates, responseHandler)
+ );
+ }
}
@Override
public void waitUntilNoActivity(final Duration toleranceDuration) {
- apnsDelegates.parallelStream().forEach(it -> it.waitUntilNoActivity(toleranceDuration));
- }
-
- private void sendNotification(final NotificationHandler notificationHandler,
- final ConcurrentLinkedQueue extends ApnsOperations> queue) {
-
- final var client = queue.poll();
- if (client != null) {
- client.sendNotification(new DelegateNotificationHandler(notificationHandler) {
-
- @Override
- public void onRejection(final RejectionReason reason) {
- if (inactiveRejectionReasons.contains(reason)) {
- // rejection reason means we must try on next APN server
- if (!queue.isEmpty()) {
- // try next apn client in the queue
- sendNotification(notificationHandler, queue);
- } else {
- // notification was rejected on every client, then disable token
- super.disableToken();
- super.onRejection(reason);
- }
- } else {
- // rejection reason means the notification must not be attempted on next APN
- // server
- super.onRejection(reason);
- }
- }
- });
- }
+ apnsDelegates.forEach(apnsTemplate -> apnsTemplate.waitUntilNoActivity(toleranceDuration));
}
@Override
@@ -67,9 +46,57 @@ public class FailoverApnsTemplate implements ApnsOperations {
apnsDelegates.parallelStream().forEach(delegate -> {
try {
delegate.close();
- } catch (Exception e) {
+ } catch (final Exception e) {
log.error("Unable to close {} gracefully", delegate, e);
}
});
}
+
+ @Override
+ public String toString() {
+ final var serverList = apnsDelegates.stream()
+ .map(Object::toString)
+ .collect(joining(","));
+ return String.format("Failover(%s)", serverList);
+ }
+
+ @RequiredArgsConstructor
+ private static class TryOnNextServerAfterInactiveResponseHandler implements ApnsResponseHandler {
+
+ private final List rejectionsHistory = new ArrayList<>();
+
+ private final ApnsPushNotification notification;
+
+ private final ConcurrentLinkedQueue> apnsTemplates;
+
+ private final FailoverApnsResponseHandler failoverResponseHandler;
+
+ @Override
+ public void onSuccess() {
+ failoverResponseHandler.onSuccess();
+ }
+
+ @Override
+ public void onRejection(final RejectionReason reason) {
+ rejectionsHistory.add(reason);
+ failoverResponseHandler.onRejection(rejectionsHistory);
+ }
+
+ @Override
+ public void onError(final Throwable cause) {
+ failoverResponseHandler.onError(cause);
+ }
+
+ @Override
+ public void onInactive(final RejectionReason reason) {
+ rejectionsHistory.add(reason);
+ final var nextApnsTemplate = apnsTemplates.poll();
+ if (null != nextApnsTemplate) {
+ // try next apns in the queue
+ nextApnsTemplate.sendNotification(notification, this);
+ } else {
+ failoverResponseHandler.onInactive(rejectionsHistory);
+ }
+ }
+ }
}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/MonitoringApnsTemplate.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/MonitoringApnsTemplate.java
index c8be6e7f5ff75a688007a7f0d500457da687eacb..f6fa369fb9551d06e5e3e367ebbe0fa4f6c44277 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/MonitoringApnsTemplate.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/MonitoringApnsTemplate.java
@@ -1,12 +1,12 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
+import com.eatthepath.pushy.apns.ApnsPushNotification;
import fr.gouv.stopc.robert.pushnotif.scheduler.apns.ApnsRequestOutcome;
import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
-import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
@@ -26,8 +26,7 @@ import static java.util.stream.Stream.concat;
* sending each notification and amount of pending notifications beeing sent.
*/
@Slf4j
-@ToString(onlyExplicitlyIncluded = true)
-public class MonitoringApnsTemplate implements ApnsOperations {
+public class MonitoringApnsTemplate implements ApnsOperations {
private final AtomicInteger pendingNotifications = new AtomicInteger(0);
@@ -37,21 +36,18 @@ public class MonitoringApnsTemplate implements ApnsOperations {
private static final String REJECTION_REASON_TAG_KEY = "rejectionReason";
- private final ApnsOperations delegate;
+ private final ApnsOperations delegate;
- @ToString.Include
private final String host;
- @ToString.Include
private final Integer port;
public MonitoringApnsTemplate(final ApnsTemplate delegate,
- final String host,
- final Integer port,
+ final ApnsServerCoordinates serverCoordinates,
final MeterRegistry meterRegistry) {
- this.host = host;
- this.port = port;
+ this.host = serverCoordinates.getHost();
+ this.port = serverCoordinates.getPort();
this.delegate = delegate;
final var successTags = Stream.of(
@@ -91,13 +87,14 @@ public class MonitoringApnsTemplate implements ApnsOperations {
}
@Override
- public void sendNotification(final NotificationHandler notificationHandler) {
+ public void sendNotification(final ApnsPushNotification notification,
+ final ApnsResponseHandler responseHandler) {
pendingNotifications.incrementAndGet();
final var sample = Timer.start();
- final var measuringHandler = new DelegateNotificationHandler(notificationHandler) {
+ final var measuringHandler = new DelegateApnsResponseHandler(responseHandler) {
@Override
public void onSuccess() {
@@ -120,9 +117,16 @@ public class MonitoringApnsTemplate implements ApnsOperations {
log.warn("Push Notification sent by {} failed", this, cause);
super.onError(cause);
}
+
+ @Override
+ public void onInactive(RejectionReason reason) {
+ pendingNotifications.decrementAndGet();
+ sample.stop(getTimer(REJECTED, reason));
+ super.onInactive(reason);
+ }
};
- delegate.sendNotification(measuringHandler);
+ delegate.sendNotification(notification, measuringHandler);
}
@Override
@@ -132,10 +136,14 @@ public class MonitoringApnsTemplate implements ApnsOperations {
@Override
public void close() throws Exception {
- log.info("Shutting down {} -----> shutting down delegate: {}", this, delegate);
delegate.close();
}
+ @Override
+ public String toString() {
+ return String.format("Monitoring(%s)", delegate);
+ }
+
/**
* returns the Timer matching various tags matching parameters.
*
@@ -146,8 +154,7 @@ public class MonitoringApnsTemplate implements ApnsOperations {
* @see ApnsRequestOutcome
* @see RejectionReason
*/
- public Timer getTimer(final ApnsRequestOutcome outcome,
- final RejectionReason rejectionReason) {
+ private Timer getTimer(final ApnsRequestOutcome outcome, final RejectionReason rejectionReason) {
return tagsToTimerMap.get(
Tags.of(
"host", host,
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/NotificationHandler.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/NotificationHandler.java
deleted file mode 100644
index f8e5d75479f839887c8de13227cde038c0d13b5a..0000000000000000000000000000000000000000
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/NotificationHandler.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
-
-import com.eatthepath.pushy.apns.ApnsClient;
-import com.eatthepath.pushy.apns.ApnsPushNotification;
-import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
-import fr.gouv.stopc.robert.pushnotif.scheduler.configuration.RobertPushServerProperties;
-
-public interface NotificationHandler {
-
- /**
- * @return Apple push notification device token
- */
- String getAppleToken();
-
- /**
- * Called when the notification request is accepted
- */
- void onSuccess();
-
- /**
- * Called when the notification request is rejected
- *
- * @param reason rejected push notification request response message
- */
- void onRejection(final RejectionReason reason);
-
- /**
- * Called when the notification request fails before reaching Apple server.
- *
- * @param reason error message
- */
- void onError(final Throwable reason);
-
- /**
- * Called when the notification request is rejected on every configured APN
- * server. In this app context, we have configured multiple APN servers. When a
- * push notif request fails because of specific configured errors:
- *
- * @see RobertPushServerProperties.Apns#inactiveRejectionReason The app tries
- * again on the next configured APN server. If the request fails on every
- * APN server, this method is called.
- * @param rejectionMessage rejected push notification request response message
- */
- void disableToken();
-
- /**
- * @param topic: Apple Push Notification topic
- * @return Push Notification for Apple Push Notification service
- * @see ApnsClient#sendNotification(ApnsPushNotification)
- */
- ApnsPushNotification buildNotification();
-}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/RateLimitingApnsTemplate.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/RateLimitingApnsTemplate.java
index c0ba74effe776625090e07957d8c308d270679d3..2224eb3fb87b08b6e63b8fb0e3c5457dd8cce1c1 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/RateLimitingApnsTemplate.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/apns/template/RateLimitingApnsTemplate.java
@@ -1,5 +1,6 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.apns.template;
+import com.eatthepath.pushy.apns.ApnsPushNotification;
import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
@@ -14,18 +15,18 @@ import java.util.concurrent.Semaphore;
* An APNS template decorator to limit notification rate.
*/
@Slf4j
-public class RateLimitingApnsTemplate implements ApnsOperations {
+public class RateLimitingApnsTemplate implements ApnsOperations {
private final LocalBucket rateLimitingBucket;
- private final ApnsOperations delegate;
+ private final ApnsOperations delegate;
private final Semaphore semaphore;
public RateLimitingApnsTemplate(
final int maxNotificationsPerSecond,
final int maxNumberOfPendingNotifications,
- final ApnsOperations delegate) {
+ final ApnsOperations delegate) {
this.delegate = delegate;
this.semaphore = new Semaphore(maxNumberOfPendingNotifications);
@@ -44,16 +45,17 @@ public class RateLimitingApnsTemplate implements ApnsOperations {
}
@Override
- public void sendNotification(final NotificationHandler notificationHandler) {
+ public void sendNotification(final ApnsPushNotification notification,
+ final ApnsResponseHandler responseHandler) {
try {
semaphore.acquire();
rateLimitingBucket.asBlocking().consume(1);
- } catch (InterruptedException e) {
+ } catch (final InterruptedException e) {
log.error("error during rate limiting process", e);
return;
}
- final var limitedHandler = new DelegateNotificationHandler(notificationHandler) {
+ final var limitedHandler = new DelegateApnsResponseHandler(responseHandler) {
@Override
public void onSuccess() {
@@ -73,7 +75,7 @@ public class RateLimitingApnsTemplate implements ApnsOperations {
super.onError(reason);
}
};
- delegate.sendNotification(limitedHandler);
+ delegate.sendNotification(notification, limitedHandler);
}
@Override
@@ -85,4 +87,9 @@ public class RateLimitingApnsTemplate implements ApnsOperations {
public void close() throws Exception {
delegate.close();
}
+
+ @Override
+ public String toString() {
+ return String.format("RateLimiting(%s)", delegate);
+ }
}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/ApnsClientConfiguration.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/ApnsClientConfiguration.java
index 296d537ee2df2328d23ca874d967e349214de980..00fc21f6f41b5b6b8aca474c8b34e151c187a831 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/ApnsClientConfiguration.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/ApnsClientConfiguration.java
@@ -25,14 +25,19 @@ public class ApnsClientConfiguration {
private final MeterRegistry meterRegistry;
- private ApnsOperations buildMeasureApnsTemplate(final RobertPushServerProperties.ApnsClient apnsClientProperties) {
+ private ApnsOperations buildMeasureApnsTemplate(
+ final RobertPushServerProperties.ApnsClient apnsClientProperties) {
- final var listener = new MicrometerApnsClientMetricsListener(
- meterRegistry,
+ final var apnsServerCoordinates = new ApnsServerCoordinates(
apnsClientProperties.getHost(),
apnsClientProperties.getPort()
);
+ final var listener = new MicrometerApnsClientMetricsListener(
+ meterRegistry,
+ apnsServerCoordinates
+ );
+
try (final var authTokenFile = this.robertPushServerProperties.getApns().getAuthTokenFile().getInputStream()) {
final var apnsClientBuilder = new ApnsClientBuilder()
.setApnsServer(apnsClientProperties.getHost(), apnsClientProperties.getPort())
@@ -51,10 +56,14 @@ public class ApnsClientConfiguration {
);
}
+ final var apnsTemplate = new ApnsTemplate(
+ apnsServerCoordinates,
+ apnsClientBuilder.build(),
+ robertPushServerProperties.getApns().getInactiveRejectionReason()
+ );
return new MonitoringApnsTemplate(
- new ApnsTemplate(apnsClientBuilder.build()),
- apnsClientProperties.getHost(),
- apnsClientProperties.getPort(),
+ apnsTemplate,
+ apnsServerCoordinates,
meterRegistry
);
@@ -65,7 +74,8 @@ public class ApnsClientConfiguration {
}
}
- private ApnsOperations buildRateLimitingTemplate(final ApnsOperations apnsOperations) {
+ private ApnsOperations buildRateLimitingTemplate(
+ final ApnsOperations apnsOperations) {
return new RateLimitingApnsTemplate(
robertPushServerProperties.getMaxNotificationsPerSecond(),
robertPushServerProperties.getMaxNumberOfPendingNotifications(),
@@ -74,14 +84,12 @@ public class ApnsClientConfiguration {
}
@Bean
- public ApnsOperations apnsTemplate() {
+ public ApnsOperations apnsTemplate() {
final var measuredRateLimitedApnsTemplates = robertPushServerProperties.getApns().getClients().stream()
.map(this::buildMeasureApnsTemplate)
.map(this::buildRateLimitingTemplate)
.collect(toUnmodifiableList());
- return new FailoverApnsTemplate(
- measuredRateLimitedApnsTemplates, robertPushServerProperties.getApns().getInactiveRejectionReason()
- );
+ return new FailoverApnsTemplate(measuredRateLimitedApnsTemplates);
}
}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/RobertPushServerProperties.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/RobertPushServerProperties.java
index 102422c129f624373bf6a095c8eb2807b2fdb290..d46a79e7ce2138f04481dd58cbb535632ff6c350 100644
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/RobertPushServerProperties.java
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/configuration/RobertPushServerProperties.java
@@ -13,6 +13,7 @@ import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
+import java.time.Duration;
import java.util.List;
@Value
@@ -35,6 +36,9 @@ public class RobertPushServerProperties {
@Positive
int maxNotificationsPerSecond;
+ @NotNull
+ Duration batchTerminationGraceTime;
+
@Valid
RobertPushServerProperties.Apns apns;
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/InstantTimestampConverter.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/InstantTimestampConverter.java
deleted file mode 100644
index 7e320a358aff075149066a16bc7c926344e3641c..0000000000000000000000000000000000000000
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/InstantTimestampConverter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package fr.gouv.stopc.robert.pushnotif.scheduler.data;
-
-import lombok.experimental.UtilityClass;
-
-import java.sql.Timestamp;
-import java.time.Instant;
-
-@UtilityClass
-public class InstantTimestampConverter {
-
- static Instant convertTimestampToInstant(final Timestamp timestamp) {
- return timestamp != null ? timestamp.toInstant() : null;
- }
-
- public static Timestamp convertInstantToTimestamp(Instant instant) {
- return instant == null ? null : Timestamp.from(instant);
- }
-
-}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/PushInfoDao.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/PushInfoDao.java
deleted file mode 100644
index 3df703d3e8a87ebba35d0ab6bac7a7925c07ae48..0000000000000000000000000000000000000000
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/PushInfoDao.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package fr.gouv.stopc.robert.pushnotif.scheduler.data;
-
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
-import lombok.RequiredArgsConstructor;
-import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
-import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-import static fr.gouv.stopc.robert.pushnotif.scheduler.data.InstantTimestampConverter.convertInstantToTimestamp;
-
-@Component
-@RequiredArgsConstructor
-public class PushInfoDao {
-
- private final NamedParameterJdbcTemplate jdbcTemplate;
-
- @Transactional
- public void updateNextPlannedPushDate(final PushInfo pushInfo) {
- final var params = new MapSqlParameterSource();
- params.addValue("id", pushInfo.getId());
- params.addValue(
- "nextPlannedPushDate", convertInstantToTimestamp(pushInfo.getNextPlannedPush())
- );
- jdbcTemplate.update("update push set next_planned_push = :nextPlannedPushDate where id = :id", params);
- }
-
- @Transactional
- public void updateSuccessFulPushedNotif(final PushInfo pushInfo) {
- final var params = new MapSqlParameterSource();
- params.addValue("id", pushInfo.getId());
- params.addValue(
- "lastSuccessfulPush", convertInstantToTimestamp(pushInfo.getLastSuccessfulPush())
- );
- params.addValue("successfulPushSent", pushInfo.getSuccessfulPushSent());
-
- jdbcTemplate.update(
- "update push set last_successful_push = :lastSuccessfulPush, " +
- "successful_push_sent = :successfulPushSent " +
- "where id = :id",
- params
- );
-
- }
-
- @Transactional
- public void updateFailurePushedNotif(final PushInfo pushInfo) {
- final var params = new MapSqlParameterSource();
- params.addValue("id", pushInfo.getId());
- params.addValue("active", pushInfo.isActive());
- params.addValue("lastFailurePush", convertInstantToTimestamp(pushInfo.getLastFailurePush()));
- params.addValue("failedPushSent", pushInfo.getFailedPushSent());
- params.addValue("lastErrorCode", pushInfo.getLastErrorCode());
-
- jdbcTemplate.update(
- "update push set active = :active, " +
- "last_failure_push = :lastFailurePush, " +
- "failed_push_sent = :failedPushSent, " +
- "last_error_code = :lastErrorCode " +
- "where id = :id",
- params
- );
- }
-
-}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/PushInfoRowMapper.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/PushInfoRowMapper.java
deleted file mode 100644
index 0e46fd1439c0cbf12faf5762f1c0254a37bde308..0000000000000000000000000000000000000000
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/PushInfoRowMapper.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package fr.gouv.stopc.robert.pushnotif.scheduler.data;
-
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
-import org.springframework.jdbc.core.RowMapper;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-
-import static fr.gouv.stopc.robert.pushnotif.scheduler.data.InstantTimestampConverter.convertTimestampToInstant;
-
-public class PushInfoRowMapper implements RowMapper {
-
- @Override
- public PushInfo mapRow(ResultSet resultSet, int i) throws SQLException {
- return PushInfo.builder()
- .id(resultSet.getLong("id"))
- .creationDate(convertTimestampToInstant(resultSet.getTimestamp("creation_date")))
- .locale(resultSet.getString("locale"))
- .timezone(resultSet.getString("timezone"))
- .token(resultSet.getString("token"))
- .active(resultSet.getBoolean("active"))
- .deleted(resultSet.getBoolean("deleted"))
- .successfulPushSent(resultSet.getInt("successful_push_sent"))
- .lastSuccessfulPush(convertTimestampToInstant(resultSet.getTimestamp("last_successful_push")))
- .failedPushSent(resultSet.getInt("failed_push_sent"))
- .lastFailurePush(convertTimestampToInstant(resultSet.getTimestamp("last_failure_push")))
- .lastErrorCode(resultSet.getString("last_error_code"))
- .nextPlannedPush(convertTimestampToInstant(resultSet.getTimestamp("next_planned_push")))
- .build();
- }
-}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/model/PushInfo.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/model/PushInfo.java
deleted file mode 100644
index 4f8d8a58baf747a5a9f4d0e088098fd96373fb52..0000000000000000000000000000000000000000
--- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/data/model/PushInfo.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package fr.gouv.stopc.robert.pushnotif.scheduler.data.model;
-
-import lombok.Builder;
-import lombok.Data;
-
-import java.time.Instant;
-
-@Data
-@Builder
-public class PushInfo {
-
- private Long id;
-
- private String token;
-
- private String timezone;
-
- private String locale;
-
- private Instant nextPlannedPush;
-
- private Instant lastSuccessfulPush;
-
- private Instant lastFailurePush;
-
- private String lastErrorCode;
-
- private int successfulPushSent;
-
- private int failedPushSent;
-
- private Instant creationDate;
-
- private boolean active;
-
- private boolean deleted;
-}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/repository/PushInfoRepository.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/repository/PushInfoRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..41f63104ca1f7b8438d899a78b47846203a0c8b6
--- /dev/null
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/repository/PushInfoRepository.java
@@ -0,0 +1,83 @@
+package fr.gouv.stopc.robert.pushnotif.scheduler.repository;
+
+import fr.gouv.stopc.robert.pushnotif.scheduler.repository.model.PushInfo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW;
+
+@Slf4j
+@Repository
+@RequiredArgsConstructor
+public class PushInfoRepository {
+
+ private final NamedParameterJdbcTemplate jdbcTemplate;
+
+ @Transactional
+ public void forEachNotificationToBeSent(final Consumer pushInfoHandler) {
+ jdbcTemplate.query(
+ "select * from push where active = true and deleted = false and next_planned_push <= now()",
+ rs -> {
+ final var pushInfo = PushInfo.builder()
+ .id(rs.getLong("id"))
+ .timezone(rs.getString("timezone"))
+ .token(rs.getString("token"))
+ .build();
+ pushInfoHandler.accept(pushInfo);
+ }
+ );
+ }
+
+ @Transactional(propagation = REQUIRES_NEW)
+ public void updateNextPlannedPushDate(final long id, final Instant nextPlannedPush) {
+ jdbcTemplate.update(
+ "update push set next_planned_push = :nextPlannedPushDate where id = :id", Map.of(
+ "id", id,
+ "nextPlannedPushDate", Timestamp.from(nextPlannedPush)
+ )
+ );
+ }
+
+ @Transactional(propagation = REQUIRES_NEW)
+ public void updateSuccessfulPushSent(final long id) {
+ jdbcTemplate.update(
+ "update push set last_successful_push = :lastSuccessfulPush, " +
+ "successful_push_sent = successful_push_sent + 1 " +
+ "where id = :id",
+ Map.of(
+ "id", id,
+ "lastSuccessfulPush", Timestamp.from(Instant.now())
+ )
+ );
+
+ }
+
+ @Transactional(propagation = REQUIRES_NEW)
+ public void updateFailure(final long id, final String failureDescription) {
+ jdbcTemplate.update(
+ "update push set " +
+ "last_failure_push = :lastFailurePush, " +
+ "failed_push_sent = failed_push_sent + 1, " +
+ "last_error_code = :lastErrorCode::char(255) " +
+ "where id = :id",
+ Map.of(
+ "id", id,
+ "lastFailurePush", Timestamp.from(Instant.now()),
+ "lastErrorCode", failureDescription
+ )
+ );
+ }
+
+ @Transactional(propagation = REQUIRES_NEW)
+ public void disable(final Long id) {
+ jdbcTemplate.update("update push set active = false where id = :id", Map.of("id", id));
+ }
+}
diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/repository/model/PushInfo.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/repository/model/PushInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..1bd18633de254bad3a0e9eab05e48a51cd847442
--- /dev/null
+++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/repository/model/PushInfo.java
@@ -0,0 +1,15 @@
+package fr.gouv.stopc.robert.pushnotif.scheduler.repository.model;
+
+import lombok.Builder;
+import lombok.Value;
+
+@Value
+@Builder
+public class PushInfo {
+
+ Long id;
+
+ String token;
+
+ String timezone;
+}
diff --git a/robert-push-notif-server-scheduler/src/main/resources/application.yml b/robert-push-notif-server-scheduler/src/main/resources/application.yml
index 8bad79aad6d40bbec8cb7ce9d27e487160aad2f8..ef430bdf46acf5478ecadcbba1809de86551f58e 100644
--- a/robert-push-notif-server-scheduler/src/main/resources/application.yml
+++ b/robert-push-notif-server-scheduler/src/main/resources/application.yml
@@ -23,11 +23,11 @@ robert.push.server:
min-push-hour: 8
max-push-hour: 20
- scheduler:
- delay-in-ms: 30000
+ scheduler.delay-in-ms: 30000
max-number-of-pending-notifications: 10000
max-notifications-per-second: 200
+ batch-termination-grace-time: 10s
apns:
inactive-rejection-reason: BadDeviceToken,DeviceTokenNotForTopic
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerNominalTest.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerNominalTest.java
index b94f213be60a17099341a6a3b7e44facbaf4a0f1..4ac10959e81b28d36022eb0a82ee7d5baae95e30 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerNominalTest.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerNominalTest.java
@@ -1,19 +1,13 @@
package fr.gouv.stopc.robert.pushnotif.scheduler;
-import com.eatthepath.pushy.apns.ApnsPushNotification;
import com.eatthepath.pushy.apns.DeliveryPriority;
import com.eatthepath.pushy.apns.PushType;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
-import fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager;
import fr.gouv.stopc.robert.pushnotif.scheduler.test.IntegrationTest;
-import fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager;
import io.micrometer.core.instrument.Tags;
-import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
-import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@@ -22,17 +16,21 @@ import static fr.gouv.stopc.robert.pushnotif.scheduler.apns.ApnsRequestOutcome.A
import static fr.gouv.stopc.robert.pushnotif.scheduler.apns.ApnsRequestOutcome.REJECTED;
import static fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason.*;
import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.*;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.FIRST;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.PRIMARY;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.SECONDARY;
import static fr.gouv.stopc.robert.pushnotif.scheduler.test.MetricsManager.assertCounterIncremented;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoWith;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.*;
import static java.time.Instant.now;
import static java.time.ZoneOffset.UTC;
-import static java.time.temporal.ChronoUnit.SECONDS;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.within;
+import static java.time.temporal.ChronoUnit.*;
+import static org.assertj.core.api.HamcrestCondition.matching;
+import static org.awaitility.Awaitility.await;
+import static org.exparity.hamcrest.date.InstantMatchers.after;
+import static org.exparity.hamcrest.date.InstantMatchers.within;
+import static org.hamcrest.Matchers.hasProperty;
@IntegrationTest
-@ActiveProfiles({ "dev", "one-apns-server" })
+@ActiveProfiles({ "test", "one-apns-server" })
@DirtiesContext
class SchedulerNominalTest {
@@ -40,11 +38,10 @@ class SchedulerNominalTest {
void should_correctly_update_push_status_when_send_notification_to_first_apn_server_with_successful_response() {
// Given
- givenPushInfoWith(b -> b.id(1L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
- givenPushInfoWith(
- b -> b.id(2L)
- .token("45f6aa01da5ddb387462c7eaf61bb78ad740f4707bebcf74f9b7c25d48e33589")
- .nextPlannedPush(LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC))
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
+ givenPushInfoForTokenAndNextPlannedPush(
+ "45f6aa01da5ddb387462c7eaf61bb78ad740f4707bebcf74f9b7c25d48e33589",
+ LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
);
// When - triggering of the scheduled task
@@ -52,64 +49,41 @@ class SchedulerNominalTest {
// Then
// Verify APNs servers
- Awaitility.await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
- assertThatMainServerAcceptedOne();
- assertThatMainServerRejectedNothing();
- assertThat(APNsMockServersManager.getNotifsAcceptedByMainServer().get(0))
- .as("Check the content of the notification received on the APNs server side")
- .satisfies(
- notif -> {
- assertThat(notif.getExpiration())
- .isCloseTo(now().plus(Duration.ofDays(1)), within(30, SECONDS));
- assertThat(notif.getPayload())
- .isEqualTo("{\"aps\":{\"badge\":0,\"content-available\":1}}");
- }
- ).extracting(
- ApnsPushNotification::getPushType,
- ApnsPushNotification::getPriority,
- ApnsPushNotification::getToken,
- ApnsPushNotification::getTopic
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThatNotifsAcceptedBy(PRIMARY)
+ .hasSize(1)
+ .first()
+ .hasFieldOrPropertyWithValue("pushType", PushType.BACKGROUND)
+ .hasFieldOrPropertyWithValue("priority", DeliveryPriority.IMMEDIATE)
+ .hasFieldOrPropertyWithValue(
+ "token",
+ "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"
)
- .containsExactly(
- PushType.BACKGROUND, DeliveryPriority.IMMEDIATE,
- "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", "test"
- );
+ .hasFieldOrPropertyWithValue("topic", "test")
+ .hasFieldOrPropertyWithValue("payload", "{\"aps\":{\"badge\":0,\"content-available\":1}}")
+ .is(matching(hasProperty("expiration", within(30, SECONDS, now().plus(1, DAYS)))));
// Verify Database
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as("Check the status of the notification that has been correctly sent to APNs server")
- .satisfies(pushInfo -> {
- assertThat(pushInfo.getLastSuccessfulPush())
- .as("Last successful push should have been updated")
- .isNotNull();
- assertThat(pushInfo.getNextPlannedPush()).isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
- );
- }
- ).extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastFailurePush,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent
- )
- .containsExactly(true, false, 0, null, null, 1);
-
- assertThat(PsqlManager.findByToken("45f6aa01da5ddb387462c7eaf61bb78ad740f4707bebcf74f9b7c25d48e33589"))
- .as("This notification is not pushed because its planned date is in future")
- .extracting(
- PushInfo::isActive, PushInfo::isDeleted, PushInfo::getFailedPushSent,
- PushInfo::getLastFailurePush,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent,
- PushInfo::getLastSuccessfulPush,
- PushInfo::getNextPlannedPush
- )
- .containsExactly(
- true, false, 0, null, null, 0, null, LocalDateTime
- .from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
- );
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", true)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastFailurePush", null)
+ .hasFieldOrPropertyWithValue("lastErrorCode", null)
+ .hasFieldOrPropertyWithValue("successfulPushSent", 1)
+ .is(matching(hasProperty("lastSuccessfulPush", within(1, MINUTES, now()))))
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
+
+ assertThatPushInfo("45f6aa01da5ddb387462c7eaf61bb78ad740f4707bebcf74f9b7c25d48e33589")
+ .hasFieldOrPropertyWithValue("active", true)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastFailurePush", null)
+ .hasFieldOrPropertyWithValue("lastErrorCode", null)
+ .hasFieldOrPropertyWithValue("successfulPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastSuccessfulPush", null)
+ .hasFieldOrPropertyWithValue("nextPlannedPush", now().plus(1, DAYS).truncatedTo(DAYS));
+
// Verify counters
assertCounterIncremented(
"pushy.notifications.sent.timer",
@@ -123,9 +97,9 @@ class SchedulerNominalTest {
void should_deactivate_notification_when_apns_server_replies_with_is_invalid_token_reason() {
// Given
- givenPushInfoWith(b -> b.id(3L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
givenApnsServerRejectsTokenIdWith(
- FIRST, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
+ PRIMARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
);
// When - triggering of the scheduled task
@@ -133,33 +107,22 @@ class SchedulerNominalTest {
// Then
// Verify servers
- Awaitility.await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
- assertThatMainServerAcceptedNothing();
- assertThatMainServerRejectedOne();
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(0);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(1);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(0);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(0);
+
// Verify database
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as(
- "Check the status of the notification that has been rejected by APNs server - notif is deactivated"
- )
- .satisfies(
- pushInfoFromBase -> {
- assertThat(pushInfoFromBase.getLastFailurePush())
- .as("Last successful push should have been updated")
- .isNotNull();
- assertThat(pushInfoFromBase.getNextPlannedPush()).isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1))
- .toInstant(UTC)
- );
- }
- ).extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent,
- PushInfo::getLastSuccessfulPush
- )
- .contains(false, false, 1, "BadDeviceToken", 0, null);
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", false)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 1)
+ .is(matching(hasProperty("lastFailurePush", within(1, MINUTES, now()))))
+ .hasFieldOrPropertyWithValue("lastErrorCode", "BadDeviceToken")
+ .hasFieldOrPropertyWithValue("successfulPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastSuccessfulPush", null)
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
// Verify counters
assertCounterIncremented(
@@ -174,9 +137,9 @@ class SchedulerNominalTest {
void should_not_deactivate_notification_when_apns_server_replies_with_no_invalid_token_reason() {
// Given
- givenPushInfoWith(b -> b.id(4L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
givenApnsServerRejectsTokenIdWith(
- FIRST, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_MESSAGE_ID
+ PRIMARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_MESSAGE_ID
);
// When - triggering of the scheduled task
@@ -184,33 +147,22 @@ class SchedulerNominalTest {
// Then
// Verify server
- Awaitility.await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
- assertThatMainServerAcceptedNothing();
- assertThatMainServerRejectedOne();
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(0);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(1);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(0);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(0);
// Verify database
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as(
- "Check the status of the notification that has been rejected by APNs server - notif is not deactivated"
- )
- .satisfies(
- pushInfo -> {
- assertThat(pushInfo.getLastFailurePush())
- .as("Last failure push should have been updated")
- .isNotNull();
- assertThat(pushInfo.getNextPlannedPush()).isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
- );
- }
- ).extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent,
- PushInfo::getLastSuccessfulPush
- )
- .contains(true, false, 1, "BadMessageId", 0, null);
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", true)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 1)
+ .is(matching(hasProperty("lastFailurePush", within(1, MINUTES, now()))))
+ .hasFieldOrPropertyWithValue("lastErrorCode", "BadMessageId")
+ .hasFieldOrPropertyWithValue("successfulPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastSuccessfulPush", null)
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
// Verify counters
assertCounterIncremented(
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerRandomDateGenerationTest.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerRandomDateGenerationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b8314742a6eabc84b4b03e0ce04990e9c72ca45e
--- /dev/null
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerRandomDateGenerationTest.java
@@ -0,0 +1,44 @@
+package fr.gouv.stopc.robert.pushnotif.scheduler;
+
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZoneId;
+
+import static fr.gouv.stopc.robert.pushnotif.scheduler.Scheduler.generatePushDateTomorrowBetween;
+import static java.time.ZoneOffset.UTC;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.oneOf;
+
+class SchedulerRandomDateGenerationTest {
+
+ @RepeatedTest(100)
+ void a_random_push_date_for_timezone_GMT0_should_be_between_request_bounds() {
+ final var nextPush = generatePushDateTomorrowBetween(10, 12, ZoneId.of("GMT"))
+ .atZone(UTC);
+ assertThat("random hour should be between 10 (included) and 12 (excluded)", nextPush.getHour(), oneOf(10, 11));
+ }
+
+ @RepeatedTest(100)
+ void a_random_push_date_for_timezone_EuropeParis_should_be_between_request_bounds_plus_2() {
+ final var nextPush = generatePushDateTomorrowBetween(10, 12, ZoneId.of("Europe/Paris"))
+ .atZone(ZoneId.of("Europe/Paris"));
+ assertThat("random hour should be between 10 (included) and 12 (excluded)", nextPush.getHour(), oneOf(10, 11));
+ }
+
+ @RepeatedTest(100)
+ void can_generate_a_push_date_between_tonight_and_tomorrow_morning() {
+ final var nextPush = generatePushDateTomorrowBetween(23, 2, ZoneId.of("Europe/Paris"))
+ .atZone(ZoneId.of("Europe/Paris"));
+ assertThat(
+ "random hour should be between 23h (included) and 2h (excluded)", nextPush.getHour(), oneOf(23, 0, 1)
+ );
+ }
+
+ @Test
+ void cant_generate_a_meaningful_time_when_min_max_are_equals() {
+ assertThatThrownBy(() -> generatePushDateTomorrowBetween(10, 10, ZoneId.of("Europe/Paris")))
+ .hasMessage("bound must be positive");
+ }
+}
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerVolumetryTest.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerVolumetryTest.java
index 095b7cf7b2f09bbbbed17a64bbe7ec2947c2e589..024439e683cdfec3db3112af1a4af236ec97c325 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerVolumetryTest.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerVolumetryTest.java
@@ -1,44 +1,42 @@
package fr.gouv.stopc.robert.pushnotif.scheduler;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
import fr.gouv.stopc.robert.pushnotif.scheduler.test.IntegrationTest;
-import fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager;
-import org.awaitility.Awaitility;
+import fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.PushInfo;
import org.junit.jupiter.api.Test;
-import org.springframework.test.context.ActiveProfiles;
-
-import java.util.concurrent.TimeUnit;
import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.*;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoWith;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.PRIMARY;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.SECONDARY;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.*;
import static java.util.UUID.randomUUID;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.LongStream.rangeClosed;
-import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
+import static org.awaitility.Awaitility.await;
@IntegrationTest
-@ActiveProfiles({ "dev" })
class SchedulerVolumetryTest {
private static final int PUSH_NOTIF_COUNT = 100;
// This test class is useful to do test with volumetry
- // @Disabled
@Test
void should_correctly_send_large_amount_of_notification_to_apns_servers() {
// Given
- rangeClosed(1, PUSH_NOTIF_COUNT).forEach(i -> givenPushInfoWith(b -> b.id(i).token(randomUUID().toString())));
+ rangeClosed(1, PUSH_NOTIF_COUNT).forEach(i -> givenPushInfoForToken(randomUUID().toString()));
// When -- triggering of the scheduled job
// Then
- Awaitility.await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
- assertThatMainServerAccepted(PUSH_NOTIF_COUNT);
- assertThatSecondServerAcceptedNothing();
- assertThatSecondServerRejectedNothing();
-
- assertThat(PsqlManager.findAll()).hasSize(PUSH_NOTIF_COUNT)
+ await().atMost(40, SECONDS).untilAsserted(() -> {
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(PUSH_NOTIF_COUNT);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(0);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(0);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(0);
+
+ assertThatAllPushInfo()
+ .hasSize(PUSH_NOTIF_COUNT)
.extracting(
PushInfo::isActive,
PushInfo::isDeleted,
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerWithTwoApnsServerTest.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerWithTwoApnsServerTest.java
index 574c4a5badeec2c426e87440153832f9f53778d2..0bdbaf535796c72ef945a3737cd4b1fd146f3bcd 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerWithTwoApnsServerTest.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/SchedulerWithTwoApnsServerTest.java
@@ -1,88 +1,68 @@
package fr.gouv.stopc.robert.pushnotif.scheduler;
-import com.eatthepath.pushy.apns.ApnsPushNotification;
import com.eatthepath.pushy.apns.DeliveryPriority;
import com.eatthepath.pushy.apns.PushType;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
import fr.gouv.stopc.robert.pushnotif.scheduler.test.IntegrationTest;
-import fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager;
-import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
-import org.springframework.test.context.ActiveProfiles;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
+import java.util.concurrent.TimeUnit;
import static fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason.*;
import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.*;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.FIRST;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.SECOND;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoWith;
-import static java.time.ZoneOffset.UTC;
-import static java.time.temporal.ChronoUnit.SECONDS;
-import static java.util.concurrent.TimeUnit.of;
-import static org.assertj.core.api.Assertions.assertThat;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.PRIMARY;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.SECONDARY;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.assertThatNotifsAcceptedBy;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.assertThatPushInfo;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoForToken;
+import static java.time.Instant.now;
+import static java.time.temporal.ChronoUnit.*;
import static org.assertj.core.api.Assertions.within;
+import static org.assertj.core.api.HamcrestCondition.matching;
+import static org.awaitility.Awaitility.await;
+import static org.exparity.hamcrest.date.InstantMatchers.after;
+import static org.exparity.hamcrest.date.InstantMatchers.within;
+import static org.hamcrest.Matchers.*;
@IntegrationTest
-@ActiveProfiles({ "dev" })
class SchedulerWithTwoApnsServerTest {
@Test
void should_correctly_update_push_status_when_send_notification_to_first_apn_server_with_successful_response() {
// Given
- givenPushInfoWith(b -> b.id(1L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
// When
// Then
- Awaitility.await().atMost(40, of(SECONDS)).untilAsserted(() -> {
- assertThatMainServerAcceptedOne();
- assertThatMainServerRejectedNothing();
- assertThatSecondServerRejectedNothing();
- assertThatSecondServerAcceptedNothing();
-
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as("Check the status of the notification that has been correctly sent to main APNs server")
- .satisfies(
- p -> {
- assertThat(p.getLastSuccessfulPush())
- .as("Last successful push should have been updated")
- .isNotNull();
- assertThat(p.getNextPlannedPush()).isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
- );
- }
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(1);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(0);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(0);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(0);
+
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", true)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastFailurePush", null)
+ .hasFieldOrPropertyWithValue("lastErrorCode", null)
+ .hasFieldOrPropertyWithValue("successfulPushSent", 1)
+ .is(matching(hasProperty("lastSuccessfulPush", within(1, MINUTES, now()))))
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
+
+ assertThatNotifsAcceptedBy(PRIMARY)
+ .hasSize(1)
+ .first()
+ .hasFieldOrPropertyWithValue(
+ "token",
+ "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"
)
- .extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastFailurePush,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent
- )
- .containsExactly(true, false, 0, null, null, 1);
-
- assertThat(getNotifsAcceptedByMainServer().get(0))
- .as("Check the content of the notification received on the main APNs server side")
- .satisfies(
- notif -> assertThat(notif.getExpiration())
- .isCloseTo(Instant.now().plus(Duration.ofDays(1)), within(30, SECONDS))
- ).extracting(
- ApnsPushNotification::getPushType,
- ApnsPushNotification::getPriority,
- ApnsPushNotification::getToken,
- ApnsPushNotification::getTopic,
- ApnsPushNotification::getPayload
- ).containsExactly(
- PushType.BACKGROUND, DeliveryPriority.IMMEDIATE,
- "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", "test",
- "{\"aps\":{\"badge\":0,\"content-available\":1}}"
- );
+ .hasFieldOrPropertyWithValue("pushType", PushType.BACKGROUND)
+ .hasFieldOrPropertyWithValue("priority", DeliveryPriority.IMMEDIATE)
+ .hasFieldOrPropertyWithValue("topic", "test")
+ .hasFieldOrPropertyWithValue("payload", "{\"aps\":{\"badge\":0,\"content-available\":1}}")
+ .is(matching(hasProperty("expiration", within(30, SECONDS, now().plus(1, DAYS)))));
});
}
@@ -90,42 +70,29 @@ class SchedulerWithTwoApnsServerTest {
void should_correctly_update_push_status_when_send_notification_to_first_apn_server_with_rejected_reason_other_than_invalid_token() {
// Given
- givenPushInfoWith(b -> b.id(1L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
givenApnsServerRejectsTokenIdWith(
- FIRST, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_TOPIC
+ PRIMARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_TOPIC
);
// When -- triggering of the scheduled job
// Then
- Awaitility.await().atMost(40, of(SECONDS)).untilAsserted(() -> {
- assertThatMainServerAcceptedNothing();
- assertThatMainServerRejectedOne();
- assertThatSecondServerAcceptedNothing();
- assertThatSecondServerRejectedNothing();
-
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as(
- "Check the status of the notification that has been rejected by main APNs server (reason other than invalid token)"
- )
- .satisfies(
- pushInfo -> {
- assertThat(pushInfo.getLastFailurePush())
- .as("Last failure push should have been updated")
- .isNotNull();
- assertThat(pushInfo.getNextPlannedPush()).isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
- );
- }
- ).extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent,
- PushInfo::getLastSuccessfulPush
- )
- .containsExactly(true, false, 1, "BadTopic", 0, null);
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(0);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(1);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(0);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(0);
+
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", true)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 1)
+ .is(matching(hasProperty("lastFailurePush", within(1, MINUTES, now()))))
+ .hasFieldOrPropertyWithValue("lastErrorCode", "BadTopic")
+ .hasFieldOrPropertyWithValue("successfulPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastSuccessfulPush", null)
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
});
}
@@ -133,60 +100,43 @@ class SchedulerWithTwoApnsServerTest {
void should_send_notification_to_second_apns_server_when_first_replies_invalid_token_response() {
// Given
- givenPushInfoWith(b -> b.id(4L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
givenApnsServerRejectsTokenIdWith(
- FIRST, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
+ PRIMARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
);
// When -- triggering of the scheduled job
// Then
- Awaitility.await().atMost(40, of(SECONDS)).untilAsserted(() -> {
- assertThatMainServerAcceptedNothing();
- assertThatMainServerRejectedOne();
- assertThatSecondServerAcceptedOne();
- assertThatSecondServerRejectedNothing();
-
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as("Check the status of the notification that has been correctly sent to secondary APNs server")
- .satisfies(
- pushInfo -> {
- assertThat(pushInfo.getLastSuccessfulPush())
- .as("Last successful push should have been updated")
- .isNotNull();
- assertThat(pushInfo.getNextPlannedPush())
- .isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1))
- .toInstant(UTC)
- );
- }
- ).extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastFailurePush,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(0);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(1);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(1);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(0);
+
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", true)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastFailurePush", null)
+ .hasFieldOrPropertyWithValue("lastErrorCode", null)
+ .hasFieldOrPropertyWithValue("successfulPushSent", 1)
+ .is(matching(hasProperty("lastSuccessfulPush", within(1, MINUTES, now()))))
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
+
+ assertThatNotifsAcceptedBy(SECONDARY)
+ .hasSize(1)
+ .first()
+ .hasFieldOrPropertyWithValue(
+ "token",
+ "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"
)
- .containsExactly(true, false, 0, null, null, 1);
-
- assertThat(getNotifsAcceptedBySecondServer().get(0))
- .as("Check the content of the notification received on the secondary APNs server side")
- .satisfies(
- notif -> assertThat(notif.getExpiration())
- .isCloseTo(Instant.now().plus(Duration.ofDays(1)), within(30, SECONDS))
- ).extracting(
- ApnsPushNotification::getPushType,
- ApnsPushNotification::getPriority,
- ApnsPushNotification::getToken,
- ApnsPushNotification::getTopic,
- ApnsPushNotification::getPayload
- )
- .containsExactly(
- PushType.BACKGROUND, DeliveryPriority.IMMEDIATE,
- "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", "test",
- "{\"aps\":{\"badge\":0,\"content-available\":1}}"
- );
+ .hasFieldOrPropertyWithValue("pushType", PushType.BACKGROUND)
+ .hasFieldOrPropertyWithValue("priority", DeliveryPriority.IMMEDIATE)
+ .hasFieldOrPropertyWithValue("topic", "test")
+ .hasFieldOrPropertyWithValue("payload", "{\"aps\":{\"badge\":0,\"content-available\":1}}")
+ .is(matching(hasProperty("expiration", within(30, SECONDS, now().plus(1, DAYS)))));
});
}
@@ -194,46 +144,34 @@ class SchedulerWithTwoApnsServerTest {
void should_deactivate_notification_when_both_server_replies_invalid_token_response() {
// Given
- givenPushInfoWith(b -> b.id(3L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
givenApnsServerRejectsTokenIdWith(
- FIRST, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
+ PRIMARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", DEVICE_TOKEN_NOT_FOR_TOPIC
);
givenApnsServerRejectsTokenIdWith(
- SECOND, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
+ SECONDARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
);
// When -- triggering of the scheduled job
// Then
- Awaitility.await().atMost(40, of(SECONDS)).untilAsserted(() -> {
-
- assertThatMainServerAcceptedNothing();
- assertThatMainServerRejectedOne();
- assertThatSecondServerAcceptedNothing();
- assertThatSecondServerRejectedOne();
-
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as("Check the status of the notification that has been rejected by all APNs server")
- .satisfies(
- pushInfo -> {
- assertThat(pushInfo.getLastFailurePush())
- .as("Last failure push should have been updated")
- .isNotNull();
- assertThat(pushInfo.getNextPlannedPush()).isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
- );
- }
- )
- .extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent,
- PushInfo::getLastSuccessfulPush
- )
- .containsExactly(false, false, 1, "BadDeviceToken", 0, null);
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(0);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(1);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(0);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(1);
+
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", false)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 1)
+ .is(matching(hasProperty("lastFailurePush", within(1, MINUTES, now()))))
+ .hasFieldOrPropertyWithValue("lastErrorCode", "DeviceTokenNotForTopic,BadDeviceToken")
+ .hasFieldOrPropertyWithValue("successfulPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastSuccessfulPush", null)
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
});
}
@@ -241,46 +179,33 @@ class SchedulerWithTwoApnsServerTest {
void should_correctly_update_push_status_when_send_notification_to_second_apn_server_with_rejected_reason_other_than_invalid_token() {
// Given
- givenPushInfoWith(b -> b.id(1L).token("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"));
+ givenPushInfoForToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad");
givenApnsServerRejectsTokenIdWith(
- FIRST, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
+ PRIMARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", BAD_DEVICE_TOKEN
);
givenApnsServerRejectsTokenIdWith(
- SECOND, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", PAYLOAD_EMPTY
+ SECONDARY, "740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad", PAYLOAD_EMPTY
);
// When -- triggering of the scheduled job
// Then
- Awaitility.await().atMost(40, of(SECONDS)).untilAsserted(() -> {
- assertThatMainServerAcceptedNothing();
- assertThatMainServerRejectedOne();
- assertThatSecondServerAcceptedNothing();
- assertThatSecondServerRejectedOne();
-
- assertThat(PsqlManager.findByToken("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad"))
- .as(
- "Check the status of the notification that has been rejected by main APNs server (reason other than invalid token)"
- )
- .satisfies(
- pushInfo -> {
- assertThat(pushInfo.getLastFailurePush())
- .as("Last failure push should have been updated")
- .isNotNull();
- assertThat(pushInfo.getNextPlannedPush()).isAfter(
- LocalDateTime.from(LocalDate.now().atStartOfDay().plusDays(1)).toInstant(UTC)
- );
- }
- )
- .extracting(
- PushInfo::isActive,
- PushInfo::isDeleted,
- PushInfo::getFailedPushSent,
- PushInfo::getLastErrorCode,
- PushInfo::getSuccessfulPushSent,
- PushInfo::getLastSuccessfulPush
- )
- .containsExactly(true, false, 1, "PayloadEmpty", 0, null);
+ await().atMost(40, TimeUnit.SECONDS).untilAsserted(() -> {
+
+ assertThatNotifsAcceptedBy(PRIMARY).hasSize(0);
+ assertThatNotifsRejectedBy(PRIMARY).hasSize(1);
+ assertThatNotifsAcceptedBy(SECONDARY).hasSize(0);
+ assertThatNotifsRejectedBy(SECONDARY).hasSize(1);
+
+ assertThatPushInfo("740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad")
+ .hasFieldOrPropertyWithValue("active", true)
+ .hasFieldOrPropertyWithValue("deleted", false)
+ .hasFieldOrPropertyWithValue("failedPushSent", 1)
+ .is(matching(hasProperty("lastFailurePush", within(1, MINUTES, now()))))
+ .hasFieldOrPropertyWithValue("lastErrorCode", "BadDeviceToken,PayloadEmpty")
+ .hasFieldOrPropertyWithValue("successfulPushSent", 0)
+ .hasFieldOrPropertyWithValue("lastSuccessfulPush", null)
+ .is(matching(hasProperty("nextPlannedPush", after(now().plus(1, DAYS).truncatedTo(DAYS)))));
});
}
}
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting1sTest.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting1sTest.java
index 93665686545410ca244d87d337a06a23845d00d4..4cfb54ef2c9f70a03bc6f5e2908cb34271e48abe 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting1sTest.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting1sTest.java
@@ -1,18 +1,17 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.ratelimiting;
import fr.gouv.stopc.robert.pushnotif.scheduler.Scheduler;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
import fr.gouv.stopc.robert.pushnotif.scheduler.test.IntegrationTest;
-import fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import java.time.Duration;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoWith;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.*;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.assertThatAllPushInfo;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoForToken;
import static java.time.Instant.now;
import static java.util.UUID.randomUUID;
import static java.util.stream.LongStream.rangeClosed;
@@ -20,9 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
@IntegrationTest
-@ActiveProfiles("dev")
-@TestPropertySource(properties = { "robert.push.server.max-notifications-per-second=1",
- "robert.push.server.scheduler.delay-in-ms=10000000000" })
+@TestPropertySource(properties = {
+ "robert.push.server.max-notifications-per-second=1",
+ "robert.push.server.scheduler.delay-in-ms=10000000000"
+})
class SchedulerRateLimiting1sTest {
@Autowired
@@ -34,7 +34,7 @@ class SchedulerRateLimiting1sTest {
// Given
rangeClosed(1, notificationsNumber)
- .forEach(i -> givenPushInfoWith(p -> p.id(i).token(randomUUID().toString())));
+ .forEach(i -> givenPushInfoForToken(randomUUID().toString()));
// When
final var before = now();
@@ -42,7 +42,8 @@ class SchedulerRateLimiting1sTest {
final var after = now();
// Then
- assertThat(PsqlManager.findAll()).hasSize(notificationsNumber)
+ assertThatAllPushInfo()
+ .hasSize(notificationsNumber)
.extracting(
PushInfo::isActive,
PushInfo::isDeleted,
@@ -54,7 +55,8 @@ class SchedulerRateLimiting1sTest {
)
.containsOnly(tuple(true, false, 0, null, null, 1, 0));
+ final var expectedDuration = Duration.ofSeconds(notificationsNumber);
assertThat(Duration.between(before, after))
- .isGreaterThanOrEqualTo(Duration.ofSeconds(notificationsNumber));
+ .isGreaterThanOrEqualTo(expectedDuration.minusSeconds(1));
}
}
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting2sTest.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting2sTest.java
index c7468e65f61acf6e5acfb577c0aafc929c53e527..17a7973b3ec31c84bcbf923e90f8e1602da05d57 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting2sTest.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/ratelimiting/SchedulerRateLimiting2sTest.java
@@ -1,18 +1,17 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.ratelimiting;
import fr.gouv.stopc.robert.pushnotif.scheduler.Scheduler;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
import fr.gouv.stopc.robert.pushnotif.scheduler.test.IntegrationTest;
-import fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import java.time.Duration;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoWith;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.*;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.assertThatAllPushInfo;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.PsqlManager.givenPushInfoForToken;
import static java.time.Instant.now;
import static java.util.UUID.randomUUID;
import static java.util.stream.LongStream.rangeClosed;
@@ -20,9 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
@IntegrationTest
-@ActiveProfiles("dev")
-@TestPropertySource(properties = { "robert.push.server.max-notifications-per-second=2",
- "robert.push.server.scheduler.delay-in-ms=10000000000" })
+@TestPropertySource(properties = {
+ "robert.push.server.max-notifications-per-second=2",
+ "robert.push.server.scheduler.delay-in-ms=10000000000"
+})
class SchedulerRateLimiting2sTest {
@Autowired
@@ -34,7 +34,7 @@ class SchedulerRateLimiting2sTest {
// Given
rangeClosed(1, notificationsNumber)
- .forEach(i -> givenPushInfoWith(p -> p.id(i).token(randomUUID().toString())));
+ .forEach(i -> givenPushInfoForToken(randomUUID().toString()));
// When
final var before = now();
@@ -42,7 +42,8 @@ class SchedulerRateLimiting2sTest {
final var after = now();
// Then
- assertThat(PsqlManager.findAll()).hasSize(notificationsNumber)
+ assertThatAllPushInfo()
+ .hasSize(notificationsNumber)
.extracting(
PushInfo::isActive,
PushInfo::isDeleted,
@@ -54,7 +55,8 @@ class SchedulerRateLimiting2sTest {
)
.containsOnly(tuple(true, false, 0, null, null, 1, 0));
+ final var expectedDuration = Duration.ofSeconds(notificationsNumber / 2);
assertThat(Duration.between(before, after))
- .isGreaterThanOrEqualTo(Duration.ofSeconds(notificationsNumber / 2));
+ .isGreaterThanOrEqualTo(expectedDuration.minusSeconds(1));
}
}
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/APNsMockServersManager.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/APNsMockServersManager.java
index b836440fb9928a711e240574193c81acbd4ab809..6eb93a34837562359040bca6f1878a20252b1a64 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/APNsMockServersManager.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/APNsMockServersManager.java
@@ -2,6 +2,7 @@ package fr.gouv.stopc.robert.pushnotif.scheduler.test;
import com.eatthepath.pushy.apns.ApnsPushNotification;
import fr.gouv.stopc.robert.pushnotif.scheduler.apns.RejectionReason;
+import org.assertj.core.api.ListAssert;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
@@ -9,8 +10,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.FIRST;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.SECOND;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.PRIMARY;
+import static fr.gouv.stopc.robert.pushnotif.scheduler.test.APNsMockServersManager.ServerId.SECONDARY;
+import static java.util.stream.Collectors.joining;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -20,8 +22,8 @@ import static org.assertj.core.api.Assertions.assertThat;
public class APNsMockServersManager implements TestExecutionListener {
private static final Map servers = Map.of(
- FIRST, new ApnsMockServerDecorator(2198),
- SECOND, new ApnsMockServerDecorator(2197)
+ PRIMARY, new ApnsMockServerDecorator(2198),
+ SECONDARY, new ApnsMockServerDecorator(2197)
);
public static void givenApnsServerRejectsTokenIdWith(final ServerId serverId,
@@ -34,56 +36,34 @@ public class APNsMockServersManager implements TestExecutionListener {
public void beforeTestExecution(final TestContext testContext) throws Exception {
TestExecutionListener.super.beforeTestExecution(testContext);
servers.values().forEach(ApnsMockServerDecorator::resetMock);
- servers.get(FIRST).clear();
- servers.get(SECOND).clear();
+ servers.values().forEach(ApnsMockServerDecorator::clear);
}
- public static List getNotifsAcceptedBySecondServer() {
- return new ArrayList<>(servers.get(SECOND).getAcceptedPushNotifications());
+ public static ListAssert assertThatNotifsAcceptedBy(final ServerId apnsServerId) {
+ final var notifs = new ArrayList<>(servers.get(apnsServerId).getAcceptedPushNotifications());
+ return assertThat(notifs)
+ .describedAs("Notifications accepted by APNS %s server:\n%s", apnsServerId, describe(notifs));
}
- public static List getNotifsAcceptedByMainServer() {
- return new ArrayList<>(servers.get(FIRST).getAcceptedPushNotifications());
+ public static ListAssert assertThatNotifsRejectedBy(final ServerId apnsServerId) {
+ final var notifs = new ArrayList<>(servers.get(apnsServerId).getRejectedPushNotifications());
+ return assertThat(notifs)
+ .describedAs("Notifications rejected by APNS %s server:\n%s", apnsServerId, describe(notifs));
}
- public static void assertThatMainServerAcceptedOne() {
- assertThat(servers.get(FIRST).getAcceptedPushNotifications()).hasSize(1);
- }
-
- public static void assertThatMainServerAccepted(final int nbNotifications) {
- assertThat(servers.get(FIRST).getAcceptedPushNotifications()).hasSize(nbNotifications);
- }
-
- public static void assertThatMainServerAcceptedNothing() {
- assertThat(servers.get(FIRST).getAcceptedPushNotifications()).isEmpty();
- }
-
- public static void assertThatMainServerRejectedOne() {
- assertThat(servers.get(FIRST).getRejectedPushNotifications()).hasSize(1);
- }
-
- public static void assertThatMainServerRejectedNothing() {
- assertThat(servers.get(FIRST).getRejectedPushNotifications()).isEmpty();
- }
-
- public static void assertThatSecondServerAcceptedOne() {
- assertThat(servers.get(SECOND).getAcceptedPushNotifications()).hasSize(1);
- }
-
- public static void assertThatSecondServerAcceptedNothing() {
- assertThat(servers.get(SECOND).getAcceptedPushNotifications()).isEmpty();
- }
-
- public static void assertThatSecondServerRejectedOne() {
- assertThat(servers.get(SECOND).getRejectedPushNotifications()).hasSize(1);
- }
-
- public static void assertThatSecondServerRejectedNothing() {
- assertThat(servers.get(SECOND).getRejectedPushNotifications()).isEmpty();
+ private static String describe(List notifs) {
+ return notifs.stream()
+ .map(
+ n -> String.format(
+ " - token=%s, topic=%s, pushType=%s, priority=%s, expiration=%s", n.getToken(),
+ n.getTopic(), n.getPushType(), n.getPriority(), n.getExpiration()
+ )
+ )
+ .collect(joining("\n"));
}
public enum ServerId {
- FIRST,
- SECOND
+ PRIMARY,
+ SECONDARY
}
}
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/IntegrationTest.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/IntegrationTest.java
index 3e86d09dbe8f386e4df4d1d8b2134821f1984d75..27ba97b805104ac25a3263fdbafe5c438f3e5e4e 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/IntegrationTest.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/IntegrationTest.java
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestExecutionListeners;
import java.lang.annotation.Retention;
@@ -18,6 +19,7 @@ import static org.springframework.test.context.TestExecutionListeners.MergeMode.
@DirtiesContext
@Retention(RUNTIME)
@SpringBootTest(webEnvironment = RANDOM_PORT)
+@ActiveProfiles({ "test" })
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@TestExecutionListeners(listeners = { APNsMockServersManager.class, PsqlManager.class,
MetricsManager.class }, mergeMode = MERGE_WITH_DEFAULTS)
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/MetricsManager.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/MetricsManager.java
index 8c022272c83da4d000292adb3a78ca40a95dafeb..43ed08b92e2130507c5979b05369b50b2f83316f 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/MetricsManager.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/MetricsManager.java
@@ -34,6 +34,15 @@ public class MetricsManager implements TestExecutionListener {
.collect(Collectors.toMap(Timer::getId, Timer::count));
}
+ @Override
+ public void afterTestMethod(TestContext testContext) {
+ final var pendingNotifications = meterRegistry.get("pushy.notifications.pending")
+ .gauge();
+ assertThat(pendingNotifications.value())
+ .describedAs("'pushy.notifications.pending' gauge metric")
+ .isEqualTo(0.0);
+ }
+
public static AbstractLongAssert> assertCounterIncremented(final String name, final long increment,
final Tags tags) {
final var timer = meterRegistry.timer(name, tags.and(SERVER_INFORMATION_TAGS));
diff --git a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/PsqlManager.java b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/PsqlManager.java
index 685fe35ba3a26463a715e5cfbaf7538c7010d725..d7a5e5eb2ad58e57080fb2393f4ab7d374d021cf 100644
--- a/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/PsqlManager.java
+++ b/robert-push-notif-server-scheduler/src/test/java/fr/gouv/stopc/robert/pushnotif/scheduler/test/PsqlManager.java
@@ -1,10 +1,11 @@
package fr.gouv.stopc.robert.pushnotif.scheduler.test;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.PushInfoRowMapper;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo;
-import fr.gouv.stopc.robert.pushnotif.scheduler.data.model.PushInfo.PushInfoBuilder;
+import lombok.Builder;
+import lombok.Value;
+import org.assertj.core.api.ListAssert;
+import org.assertj.core.api.ObjectAssert;
import org.flywaydb.core.Flyway;
-import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.test.context.TestContext;
@@ -13,16 +14,17 @@ import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.util.List;
import java.util.Map;
import java.util.Random;
-import java.util.function.Function;
-import static fr.gouv.stopc.robert.pushnotif.scheduler.data.InstantTimestampConverter.convertInstantToTimestamp;
-import static java.time.Instant.now;
import static java.time.ZoneOffset.UTC;
+import static org.assertj.core.api.Assertions.assertThat;
public class PsqlManager implements TestExecutionListener {
@@ -30,28 +32,8 @@ public class PsqlManager implements TestExecutionListener {
private static final PushInfoRowMapper pushInfoRowMapper = new PushInfoRowMapper();
- private static int insert(final PushInfo pushInfo) {
- final MapSqlParameterSource parameters = new MapSqlParameterSource();
- parameters.addValue("id", pushInfo.getId());
- parameters.addValue("creation_date", convertInstantToTimestamp(pushInfo.getCreationDate()));
- parameters.addValue("locale", pushInfo.getLocale());
- parameters.addValue("timezone", pushInfo.getTimezone());
- parameters.addValue("token", pushInfo.getToken());
- parameters.addValue("active", pushInfo.isActive());
- parameters.addValue("deleted", pushInfo.isDeleted());
- parameters.addValue("successful_push_sent", pushInfo.getSuccessfulPushSent());
- parameters.addValue("last_successful_push", convertInstantToTimestamp(pushInfo.getLastSuccessfulPush()));
- parameters.addValue("failed_push_sent", pushInfo.getFailedPushSent());
- parameters.addValue("last_failure_push", convertInstantToTimestamp(pushInfo.getLastFailurePush()));
- parameters.addValue("last_error_code", pushInfo.getLastErrorCode());
- parameters.addValue("next_planned_push", convertInstantToTimestamp(pushInfo.getNextPlannedPush()));
- return new SimpleJdbcInsert(jdbcTemplate.getJdbcTemplate())
- .withTableName("push")
- .execute(parameters);
- }
-
private static final JdbcDatabaseContainer POSTGRES = new PostgreSQLContainer(
- DockerImageName.parse("postgres:9.6")
+ DockerImageName.parse("postgres:13.7")
);
static {
@@ -74,44 +56,94 @@ public class PsqlManager implements TestExecutionListener {
jdbcTemplate = testContext.getApplicationContext().getBean(NamedParameterJdbcTemplate.class);
}
- static PushInfoBuilder pushinfoBuilder = PushInfo.builder()
- .id(10000000L)
- .active(true)
- .deleted(false)
- .token("00000000")
- .locale("fr-FR")
- .creationDate(now())
- .successfulPushSent(0)
- .failedPushSent(0)
- .timezone("Europe/Paris");
-
- public static void givenPushInfoWith(final Function testSpecificBuilder) {
- insert(
- testSpecificBuilder.apply(
- pushinfoBuilder
- /*
- * Set next planned push date outside of static builder to have varying
- * getRandomNumberInRange results but let test specific builder override it if
- * needed
- */
- .nextPlannedPush(
- LocalDateTime.from(
- LocalDate.now().atStartOfDay().plusHours(new Random().nextInt(24))
- .plusMinutes(new Random().nextInt(60)).minusDays(1)
- )
- .toInstant(UTC)
- )
- ).build()
+ public static void givenPushInfoForToken(String token) {
+ givenPushInfoForTokenAndNextPlannedPush(
+ token,
+ LocalDateTime.from(
+ LocalDate.now().atStartOfDay().plusHours(new Random().nextInt(24))
+ .plusMinutes(new Random().nextInt(60)).minusDays(1)
+ )
+ .toInstant(UTC)
);
}
- public static PushInfo findByToken(final String token) {
- final var parameters = Map.of("token", token);
- return jdbcTemplate.queryForObject("select * from push where token = :token", parameters, pushInfoRowMapper);
+ public static void givenPushInfoForTokenAndNextPlannedPush(String token, Instant nextPlannedPush) {
+ new SimpleJdbcInsert(jdbcTemplate.getJdbcTemplate())
+ .withTableName("push")
+ .usingGeneratedKeyColumns("id")
+ .execute(
+ Map.of(
+ "creation_date", Timestamp.from(Instant.now()),
+ "locale", "fr-FR",
+ "timezone", "Europe/Paris",
+ "token", token,
+ "active", true,
+ "deleted", false,
+ "successful_push_sent", 0,
+ "failed_push_sent", 0,
+ "next_planned_push", Timestamp.from(nextPlannedPush)
+ )
+ );
+ }
+
+ public static ListAssert assertThatAllPushInfo() {
+ return assertThat(jdbcTemplate.query("select * from push", Map.of(), PushInfoRowMapper.INSTANCE))
+ .as("all push data stored");
}
- public static List findAll() {
- return jdbcTemplate.query("select * from push ", pushInfoRowMapper);
+ public static ObjectAssert assertThatPushInfo(final String token) {
+ final var push = jdbcTemplate.queryForObject(
+ "select * from push where token = :token", Map.of("token", token),
+ PushInfoRowMapper.INSTANCE
+ );
+ return assertThat(push)
+ .describedAs("push data for token %s", token);
}
+ @Value
+ @Builder
+ public static class PushInfo {
+
+ String token;
+
+ boolean active;
+
+ boolean deleted;
+
+ int successfulPushSent;
+
+ Instant lastSuccessfulPush;
+
+ int failedPushSent;
+
+ Instant lastFailurePush;
+
+ String lastErrorCode;
+
+ Instant nextPlannedPush;
+ }
+
+ private static class PushInfoRowMapper implements RowMapper {
+
+ private static final PushInfoRowMapper INSTANCE = new PushInfoRowMapper();
+
+ @Override
+ public PushInfo mapRow(final ResultSet rs, final int rowNum) throws SQLException {
+ return PushInfo.builder()
+ .token(rs.getString("token"))
+ .active(rs.getBoolean("active"))
+ .deleted(rs.getBoolean("deleted"))
+ .successfulPushSent(rs.getInt("successful_push_sent"))
+ .lastSuccessfulPush(toInstant(rs.getTimestamp("last_successful_push")))
+ .failedPushSent(rs.getInt("failed_push_sent"))
+ .lastFailurePush(toInstant(rs.getTimestamp("last_failure_push")))
+ .lastErrorCode(rs.getString("last_error_code"))
+ .nextPlannedPush(toInstant(rs.getTimestamp("next_planned_push")))
+ .build();
+ }
+
+ private static Instant toInstant(final Timestamp timestamp) {
+ return timestamp != null ? timestamp.toInstant() : null;
+ }
+ }
}
diff --git a/robert-push-notif-server-scheduler/src/test/resources/application-dev.yml b/robert-push-notif-server-scheduler/src/test/resources/application-test.yml
similarity index 70%
rename from robert-push-notif-server-scheduler/src/test/resources/application-dev.yml
rename to robert-push-notif-server-scheduler/src/test/resources/application-test.yml
index 602c8763f39de5f012770f787ec4edd470bffdd3..7b1f80008930bbeb408f0df6a647a0f6919b5663 100644
--- a/robert-push-notif-server-scheduler/src/test/resources/application-dev.yml
+++ b/robert-push-notif-server-scheduler/src/test/resources/application-test.yml
@@ -1,26 +1,20 @@
-logging:
- file:
- name: ./target/logs/test.log
- level:
- fr.gouv.stopc: DEBUG
-
robert.push.server:
# Min/Max Push Notification Hours
min-push-hour: 8
max-push-hour: 10
+ scheduler.delay-in-ms: 1000
+ batch-termination-grace-time: 1s
apns:
clients:
- host: localhost
port: 2198
- host: localhost
port: 2197
- inactive-rejection-reason: BadDeviceToken
auth-token-file: classpath:/apns/token-auth-private-key.p8
auth-key-id: key-id
team-id: team-id
topic: test
-
- #path to the trusted certificate chain
trusted-client-certificate-chain: classpath:/apns/ca.pem
- scheduler.delay-in-ms: 1000
+logging.level:
+ org.flywaydb.core.internal.command.DbMigrate: WARN
diff --git a/robert-push-notif-server-ws-rest/pom.xml b/robert-push-notif-server-ws-rest/pom.xml
index 6d0563ae28f2104dcc7d333a8020ce1456d06b17..fd09a88bfdb3c9c54125fafff81c410348668486 100644
--- a/robert-push-notif-server-ws-rest/pom.xml
+++ b/robert-push-notif-server-ws-rest/pom.xml
@@ -17,11 +17,6 @@
Rest Webservice Module
- UTF-8
-
- UTF-8
-
- 1.17.1
5.4.0
@@ -32,22 +27,22 @@
org.springframework.boot
spring-boot-starter-web
-
org.springframework.cloud
- spring-cloud-starter-vault-config
-
-
-
- org.springframework.boot
+ spring-cloud-starter-vault-config
+
+
+ org.springframework.boot
spring-boot-starter-validation
-
org.springframework.boot
spring-boot-starter-actuator
-
+
+ io.micrometer
+ micrometer-registry-prometheus
+
org.postgresql
postgresql
@@ -63,12 +58,6 @@
rest-assured
test
-
-
-
- io.micrometer
- micrometer-registry-prometheus
-
@@ -119,13 +108,6 @@
org.springframework.boot
spring-boot-maven-plugin
-
-
-
- build-info
-
-
-
diff --git a/robert-push-notif-server-ws-rest/src/test/java/fr/gouv/stopc/robert/pushnotif/server/ws/test/PsqlManager.java b/robert-push-notif-server-ws-rest/src/test/java/fr/gouv/stopc/robert/pushnotif/server/ws/test/PsqlManager.java
index 4108be49648836b4f7a40762eea6068dd687f20e..f10f6a6ee01fd4106005c8411124f41f776c8ef8 100644
--- a/robert-push-notif-server-ws-rest/src/test/java/fr/gouv/stopc/robert/pushnotif/server/ws/test/PsqlManager.java
+++ b/robert-push-notif-server-ws-rest/src/test/java/fr/gouv/stopc/robert/pushnotif/server/ws/test/PsqlManager.java
@@ -22,7 +22,7 @@ public class PsqlManager implements TestExecutionListener {
private static JdbcTemplate jdbcTemplate;
private static final JdbcDatabaseContainer POSTGRES = new PostgreSQLContainer(
- DockerImageName.parse("postgres:9.6")
+ DockerImageName.parse("postgres:13.7")
);
public static Instant defaultNextPlannedPushDate = LocalDateTime.now().toInstant(ZoneOffset.UTC);
diff --git a/robert-push-notif-server-ws-rest/src/test/resources/application-dev.yml b/robert-push-notif-server-ws-rest/src/test/resources/application-dev.yml
index 0cdf5c6c32edf0564126f85617c440c0dfb744c4..6ea0b9186bec882c02124fc0cd1fc5254f88cc91 100644
--- a/robert-push-notif-server-ws-rest/src/test/resources/application-dev.yml
+++ b/robert-push-notif-server-ws-rest/src/test/resources/application-dev.yml
@@ -4,7 +4,6 @@ spring:
hibernate:
jdbc:
time_zone: UTC
-logging:
- level:
- org.flywaydb.core.internal: OFF
-
+ cloud.vault.enabled: false
+logging.level:
+ org.flywaydb.core.internal: OFF