diff --git a/pom.xml b/pom.xml index b338704af0314ac8babfe403b01a90e1c98f0af0..a2036d2ea4fc1d33a8df585ab04a59264a545878 100644 --- a/pom.xml +++ b/pom.xml @@ -9,10 +9,9 @@ org.springframework.boot spring-boot-starter-parent - 2.3.2.RELEASE + 2.3.12.RELEASE - fr.gouv.stopc robert-push-notif-server 0-SNAPSHOT @@ -26,17 +25,13 @@ - 11 - 1.18.24 - 3.1.1 - 2.1.1 - 2.0.1.Final - 42.2.12 3.11 - 1.15.3 - 2.17.0 - 5.2.20.RELEASE - Hoxton.SR5 + 11 + 2.17.1 + 42.5.0 + Hoxton.SR12 + 5.2.22.RELEASE + 1.17.3 @@ -94,36 +89,6 @@ pom import - - - javax.inject - javax.inject - 1 - - - - javax.ws.rs - javax.ws.rs-api - ${javax-rs.version} - - - - javax.validation - validation-api - ${javax.validation.version} - - - - org.postgresql - postgresql - ${postgresql.version} - - - - org.apache.commons - commons-lang3 - ${commons-lang3.version} - @@ -224,11 +189,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - ${maven.jar.plugin.version} - diff --git a/robert-push-notif-server-scheduler/pom.xml b/robert-push-notif-server-scheduler/pom.xml index d8a03e58ca5556d5975d57a641b0efca4c590a71..da585cbc18b8cdfbb427472cc5089538d4694b66 100644 --- a/robert-push-notif-server-scheduler/pom.xml +++ b/robert-push-notif-server-scheduler/pom.xml @@ -13,72 +13,71 @@ robert-push-notif-server-scheduler + + 7.6.0 + 2.0.8 + 0.15.1 + + - org.springframework.cloud - spring-cloud-starter-vault-config + org.springframework.boot + spring-boot-starter-web - org.springframework.boot - spring-boot-starter-jdbc + spring-boot-starter-validation - org.springframework.boot - spring-boot-starter-validation + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + org.springframework.cloud + spring-cloud-starter-vault-config + + + org.springframework.boot + spring-boot-starter-jdbc - org.springframework spring-jdbc - org.postgresql postgresql runtime - com.eatthepath pushy - 0.15.1 + ${pushy.version} - com.github.vladimir-bukhtoyarov bucket4j-core - 7.5.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - io.micrometer - micrometer-registry-prometheus - runtime + ${bucket4j.version} org.apache.commons commons-lang3 - org.awaitility awaitility test + + org.exparity + hamcrest-date + ${hamcrest-date.version} + diff --git a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/Scheduler.java b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/Scheduler.java index 8219536459a48a550ac19f1bd435bb06a7df616a..03d9e4e2937d95a45f39bbe42240f07bb84c7128 100644 --- a/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/Scheduler.java +++ b/robert-push-notif-server-scheduler/src/main/java/fr/gouv/stopc/robert/pushnotif/scheduler/Scheduler.java @@ -1,74 +1,151 @@ package fr.gouv.stopc.robert.pushnotif.scheduler; -import fr.gouv.stopc.robert.pushnotif.scheduler.apns.PushInfoNotificationHandler; +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.RejectionReason; import fr.gouv.stopc.robert.pushnotif.scheduler.apns.template.ApnsOperations; +import fr.gouv.stopc.robert.pushnotif.scheduler.apns.template.FailoverApnsResponseHandler; import fr.gouv.stopc.robert.pushnotif.scheduler.configuration.RobertPushServerProperties; -import fr.gouv.stopc.robert.pushnotif.scheduler.data.PushInfoDao; -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.repository.PushInfoRepository; +import fr.gouv.stopc.robert.pushnotif.scheduler.repository.model.PushInfo; import io.micrometer.core.annotation.Counted; import io.micrometer.core.annotation.Timed; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +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 java.util.stream.Collectors.joining; @Slf4j @Service @RequiredArgsConstructor public class Scheduler { - private final JdbcTemplate jdbcTemplate; - - private final PushInfoRowMapper rowMapper = new PushInfoRowMapper(); - - private final PushInfoDao pushInfoDao; + private final PushInfoRepository pushInfoRepository; private final RobertPushServerProperties robertPushServerProperties; - private final ApnsOperations apnsTemplate; + private final ApnsOperations 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 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