From e141e167e036f272c4b1b4a91af71e04a25a6d0d Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Tue, 28 Dec 2021 10:58:07 +0100 Subject: [PATCH] add warning/info: number of online/offline changes in past week --- .../service/NodeWarningsService.java | 22 ++++- .../service/OnlinePeersService.java | 35 +++++-- .../service/NodeWarningsServiceTest.java | 17 ++++ .../service/OnlinePeersServiceTest.java | 93 ++++++++++++++++--- .../model/NodeOnlineChangesWarning.java | 4 + .../cotto/lndmanagej/model/OnlineReport.java | 10 +- .../model/NodeOnlineChangesWarningTest.java | 13 +++ .../lndmanagej/model/NodeWarningsTest.java | 4 +- .../lndmanagej/model/OnlineReportTest.java | 8 +- .../lndmanagej/model/NodeWarningFixtures.java | 2 + .../model/NodeWarningsFixtures.java | 4 +- .../model/OnlineReportFixtures.java | 4 +- .../onlinepeers/OnlinePeersDao.java | 2 +- .../persistence/OnlinePeersDaoImpl.java | 19 +++- .../persistence/OnlinePeersRepository.java | 4 +- .../persistence/OnlinePeersDaoImplTest.java | 48 +++++++--- .../controller/NodeControllerIT.java | 2 + .../controller/dto/OnlineReportDto.java | 9 +- .../controller/dto/OnlineReportDtoTest.java | 5 +- 19 files changed, 251 insertions(+), 54 deletions(-) create mode 100644 model/src/main/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarning.java create mode 100644 model/src/test/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarningTest.java diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/NodeWarningsService.java b/backend/src/main/java/de/cotto/lndmanagej/service/NodeWarningsService.java index b916a19a..4f0b47b6 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/NodeWarningsService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/NodeWarningsService.java @@ -1,5 +1,6 @@ package de.cotto.lndmanagej.service; +import de.cotto.lndmanagej.model.NodeOnlineChangesWarning; import de.cotto.lndmanagej.model.NodeOnlinePercentageWarning; import de.cotto.lndmanagej.model.NodeWarning; import de.cotto.lndmanagej.model.NodeWarnings; @@ -13,7 +14,8 @@ import java.util.stream.Stream; @Component public class NodeWarningsService { - private static final int THRESHOLD = 80; + private static final int ONLINE_PERCENTAGE_THRESHOLD = 80; + private static final int ONLINE_CHANGES_THRESHOLD = 50; private final OnlinePeersService onlinePeersService; @@ -22,19 +24,29 @@ public class NodeWarningsService { } public NodeWarnings getNodeWarnings(Pubkey pubkey) { - List warnings = Stream.of((Function>) this::getOnlineWarning) - .map(function -> function.apply(pubkey)) + List warnings = Stream.of( + (Function>) this::getOnlinePercentageWarning, + this::getOnlineChangesWarning + ).map(function -> function.apply(pubkey)) .flatMap(Optional::stream) .toList(); return new NodeWarnings(warnings); } - private Optional getOnlineWarning(Pubkey pubkey) { + private Optional getOnlinePercentageWarning(Pubkey pubkey) { int percentage = onlinePeersService.getOnlinePercentageLastWeek(pubkey); - if (percentage < THRESHOLD) { + if (percentage < ONLINE_PERCENTAGE_THRESHOLD) { return Optional.of(new NodeOnlinePercentageWarning(percentage)); } return Optional.empty(); } + private Optional getOnlineChangesWarning(Pubkey pubkey) { + int changes = onlinePeersService.getChangesLastWeek(pubkey); + if (changes > ONLINE_CHANGES_THRESHOLD) { + return Optional.of(new NodeOnlineChangesWarning(changes)); + } + return Optional.empty(); + } + } diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java b/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java index a29dc6c5..8f05d5f7 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java @@ -13,17 +13,18 @@ import java.time.Duration; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.List; import java.util.Objects; @Component public class OnlinePeersService { private static final int DAYS_FOR_OFFLINE_PERCENTAGE = 7; + private static final int DAYS_FOR_CHANGES = 7; private static final Duration CACHE_REFRESH = Duration.ofMinutes(1); private static final Duration CACHE_EXPIRY = Duration.ofMinutes(2); private final OnlinePeersDao dao; private final LoadingCache onlinePercentageCache; + private final LoadingCache changesCache; public OnlinePeersService(OnlinePeersDao dao) { this.dao = dao; @@ -31,6 +32,10 @@ public class OnlinePeersService { .withRefresh(CACHE_REFRESH) .withExpiry(CACHE_EXPIRY) .build(this::getOnlinePercentageLastWeekWithoutCache); + changesCache = new CacheBuilder() + .withRefresh(CACHE_REFRESH) + .withExpiry(CACHE_EXPIRY) + .build(this::getChangesLastWeekWithoutCache); } public OnlineReport getOnlineReport(Node node) { @@ -38,29 +43,30 @@ public class OnlinePeersService { OnlineStatus mostRecentOnlineStatus = dao.getMostRecentOnlineStatus(node.pubkey()) .orElse(new OnlineStatus(online, now())); int onlinePercentageLastWeek = getOnlinePercentageLastWeek(node.pubkey()); + int changesLastWeek = getChangesLastWeek(node.pubkey()); if (mostRecentOnlineStatus.online() == online) { - return OnlineReport.createFromStatus(mostRecentOnlineStatus, onlinePercentageLastWeek); + return OnlineReport.createFromStatus(mostRecentOnlineStatus, onlinePercentageLastWeek, changesLastWeek); } - return new OnlineReport(online, now(), onlinePercentageLastWeek); + return new OnlineReport(online, now(), onlinePercentageLastWeek, changesLastWeek); } public int getOnlinePercentageLastWeek(Pubkey pubkey) { return Objects.requireNonNull(onlinePercentageCache.get(pubkey)); } + public int getChangesLastWeek(Pubkey pubkey) { + return Objects.requireNonNull(changesCache.get(pubkey)); + } + private int getOnlinePercentageLastWeekWithoutCache(Pubkey pubkey) { Duration total = Duration.ZERO; Duration online = Duration.ZERO; ZonedDateTime intervalStart = ZonedDateTime.now(ZoneOffset.UTC); ZonedDateTime cutoff = intervalStart.minusDays(DAYS_FOR_OFFLINE_PERCENTAGE); - boolean shouldContinue = true; - List allForPeer = dao.getAllForPeer(pubkey); - for (int i = 0; i < allForPeer.size() && shouldContinue; i++) { - OnlineStatus onlineStatus = allForPeer.get(i); + for (OnlineStatus onlineStatus : dao.getAllForPeerUpToAgeInDays(pubkey, DAYS_FOR_OFFLINE_PERCENTAGE)) { ZonedDateTime intervalEnd = onlineStatus.since(); if (intervalEnd.isBefore(cutoff)) { intervalEnd = cutoff; - shouldContinue = false; } Duration difference = Duration.between(intervalEnd, intervalStart); total = total.plus(difference); @@ -79,6 +85,19 @@ public class OnlinePeersService { return (int) (offline.getSeconds() * 100.0 / total.getSeconds()); } + private int getChangesLastWeekWithoutCache(Pubkey pubkey) { + Boolean lastKnownStatus = null; + int changes = -1; + for (OnlineStatus onlineStatus : dao.getAllForPeerUpToAgeInDays(pubkey, DAYS_FOR_CHANGES)) { + boolean status = onlineStatus.online(); + if (lastKnownStatus == null || status != lastKnownStatus) { + changes++; + lastKnownStatus = status; + } + } + return Math.max(0, changes); + } + private ZonedDateTime now() { return ZonedDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); } diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/NodeWarningsServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/NodeWarningsServiceTest.java index 9a8d03af..3d21c319 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/NodeWarningsServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/NodeWarningsServiceTest.java @@ -1,5 +1,6 @@ package de.cotto.lndmanagej.service; +import de.cotto.lndmanagej.model.NodeOnlineChangesWarning; import de.cotto.lndmanagej.model.NodeOnlinePercentageWarning; import de.cotto.lndmanagej.model.NodeWarnings; import org.junit.jupiter.api.BeforeEach; @@ -24,6 +25,7 @@ class NodeWarningsServiceTest { @BeforeEach void setUp() { when(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).thenReturn(80); + when(onlinePeersService.getChangesLastWeek(PUBKEY)).thenReturn(50); } @Test @@ -33,6 +35,21 @@ class NodeWarningsServiceTest { .isEqualTo(new NodeWarnings(new NodeOnlinePercentageWarning(79))); } + @Test + void getNodeWarnings_online_changes_above_threshold() { + when(onlinePeersService.getChangesLastWeek(PUBKEY)).thenReturn(51); + assertThat(nodeWarningsService.getNodeWarnings(PUBKEY)) + .isEqualTo(new NodeWarnings(new NodeOnlineChangesWarning(51))); + } + + @Test + void getNodeWarnings_all_warnings() { + when(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).thenReturn(79); + when(onlinePeersService.getChangesLastWeek(PUBKEY)).thenReturn(51); + assertThat(nodeWarningsService.getNodeWarnings(PUBKEY)) + .isEqualTo(new NodeWarnings(new NodeOnlinePercentageWarning(79), new NodeOnlineChangesWarning(51))); + } + @Test void getNodeWarnings_ok() { assertThat(nodeWarningsService.getNodeWarnings(PUBKEY)).isEqualTo(NodeWarnings.NONE); diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java index 89f97028..4c51f8b7 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java @@ -1,6 +1,7 @@ package de.cotto.lndmanagej.service; import de.cotto.lndmanagej.model.OnlineReport; +import de.cotto.lndmanagej.model.OnlineReportFixtures; import de.cotto.lndmanagej.model.OnlineStatus; import de.cotto.lndmanagej.onlinepeers.OnlinePeersDao; import org.junit.jupiter.api.Test; @@ -16,7 +17,6 @@ import java.util.Optional; import static de.cotto.lndmanagej.model.NodeFixtures.NODE; import static de.cotto.lndmanagej.model.NodeFixtures.NODE_PEER; -import static de.cotto.lndmanagej.model.OnlineReportFixtures.ONLINE_REPORT; import static de.cotto.lndmanagej.model.OnlineStatusFixtures.ONLINE_STATUS; import static de.cotto.lndmanagej.model.OnlineStatusFixtures.ONLINE_STATUS_OFFLINE; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; @@ -24,9 +24,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.Offset.offset; import static org.mockito.Mockito.when; +@SuppressWarnings("CPD-START") @ExtendWith(MockitoExtension.class) class OnlinePeersServiceTest { private static final ZonedDateTime NOW = ZonedDateTime.now(ZoneOffset.UTC); + private static final int SEVEN_DAYS = 7; @InjectMocks private OnlinePeersService onlinePeersService; @@ -38,7 +40,8 @@ class OnlinePeersServiceTest { void with_time_if_given_status_matches_last_known_status() { when(dao.getMostRecentOnlineStatus(PUBKEY)).thenReturn(Optional.of(ONLINE_STATUS)); mockFor23PercentOffline(); - assertThat(onlinePeersService.getOnlineReport(NODE_PEER)).isEqualTo(ONLINE_REPORT); + assertThat(onlinePeersService.getOnlineReport(NODE_PEER)) + .isEqualTo(new OnlineReport(true, OnlineReportFixtures.TIMESTAMP, 77, 1)); } @Test @@ -71,28 +74,32 @@ class OnlinePeersServiceTest { @Test void getOnlinePercentageLastWeek_always_online() { ZonedDateTime early = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(true, early))); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)) + .thenReturn(List.of(new OnlineStatus(true, early))); assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(100); } @Test void getOnlinePercentageLastWeek_always_offline() { ZonedDateTime early = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(false, early))); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)) + .thenReturn(List.of(new OnlineStatus(false, early))); assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero(); } @Test void getOnlinePercentageLastWeek_limited_data_offline() { ZonedDateTime oneHourAgo = NOW.minusHours(1); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(false, oneHourAgo))); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)) + .thenReturn(List.of(new OnlineStatus(false, oneHourAgo))); assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero(); } @Test void getOnlinePercentageLastWeek_limited_data_online() { ZonedDateTime oneHourAgo = NOW.minusHours(1); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(true, oneHourAgo))); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)) + .thenReturn(List.of(new OnlineStatus(true, oneHourAgo))); assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(100); } @@ -100,7 +107,7 @@ class OnlinePeersServiceTest { void getOnlinePercentageLastWeek_limited_data_online_then_offline() { ZonedDateTime twoHoursAgo = NOW.minusHours(2); ZonedDateTime oneHourAgo = NOW.minusHours(1); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( new OnlineStatus(true, oneHourAgo), new OnlineStatus(false, twoHoursAgo) )); @@ -114,7 +121,7 @@ class OnlinePeersServiceTest { ZonedDateTime threeHoursAgo = NOW.minusHours(3); ZonedDateTime twoHoursAgo = NOW.minusHours(2); ZonedDateTime oneHourAgo = NOW.minusHours(1); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( new OnlineStatus(true, oneHourAgo), new OnlineStatus(false, twoHoursAgo), new OnlineStatus(true, threeHoursAgo), @@ -128,7 +135,7 @@ class OnlinePeersServiceTest { void getOnlinePercentageLastWeek_limited_data_offline_then_online() { ZonedDateTime twoHoursAgo = NOW.minusHours(2); ZonedDateTime oneHourAgo = NOW.minusHours(1); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( new OnlineStatus(false, oneHourAgo), new OnlineStatus(true, twoHoursAgo) )); @@ -140,7 +147,7 @@ class OnlinePeersServiceTest { ZonedDateTime twoYearsAgo = NOW.minusYears(2); ZonedDateTime oneYearAgo = NOW.minusYears(1); ZonedDateTime thirteenDaysAgo = NOW.minusDays(6); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( new OnlineStatus(true, thirteenDaysAgo), new OnlineStatus(false, oneYearAgo), new OnlineStatus(true, twoYearsAgo) @@ -154,6 +161,70 @@ class OnlinePeersServiceTest { assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isCloseTo(77, offset(1)); } + @Test + void getChangesLastWeek_no_data() { + assertThat(onlinePeersService.getChangesLastWeek(PUBKEY)).isZero(); + } + + @Test + void getChangesLastWeek_one_old_entry() { + ZonedDateTime oneYearAgo = NOW.minusYears(1); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( + new OnlineStatus(true, oneYearAgo) + )); + assertThat(onlinePeersService.getChangesLastWeek(PUBKEY)).isZero(); + } + + @Test + void getChangesLastWeek_one_recent_entry() { + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( + new OnlineStatus(true, oneHourAgo) + )); + assertThat(onlinePeersService.getChangesLastWeek(PUBKEY)).isZero(); + } + + @Test + void getChangesLastWeek_two_recent_entries() { + ZonedDateTime twoHoursAgo = NOW.minusHours(2); + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( + new OnlineStatus(false, oneHourAgo), + new OnlineStatus(true, twoHoursAgo) + )); + assertThat(onlinePeersService.getChangesLastWeek(PUBKEY)).isOne(); + } + + @Test + void getChangesLastWeek_many_recent_entries() { + ZonedDateTime fourHoursAgo = NOW.minusHours(4); + ZonedDateTime threeHoursAgo = NOW.minusHours(3); + ZonedDateTime twoHoursAgo = NOW.minusHours(2); + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( + new OnlineStatus(false, oneHourAgo), + new OnlineStatus(true, twoHoursAgo), + new OnlineStatus(false, threeHoursAgo), + new OnlineStatus(true, fourHoursAgo) + )); + assertThat(onlinePeersService.getChangesLastWeek(PUBKEY)).isEqualTo(3); + } + + @Test + void getChangesLastWeek_many_recent_entries_not_all_are_changes() { + ZonedDateTime fourHoursAgo = NOW.minusHours(4); + ZonedDateTime threeHoursAgo = NOW.minusHours(3); + ZonedDateTime twoHoursAgo = NOW.minusHours(2); + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( + new OnlineStatus(false, oneHourAgo), + new OnlineStatus(false, twoHoursAgo), + new OnlineStatus(false, threeHoursAgo), + new OnlineStatus(true, fourHoursAgo) + )); + assertThat(onlinePeersService.getChangesLastWeek(PUBKEY)).isOne(); + } + private void assertVeryRecentSince(OnlineReport report) { ZonedDateTime since = report.since(); assertThat(since).isAfter(NOW.minusSeconds(1)); @@ -163,7 +234,7 @@ class OnlinePeersServiceTest { private void mockFor23PercentOffline() { ZonedDateTime longAgo = NOW.minusDays(14); ZonedDateTime offlineSince = NOW.minusMinutes(2318); - when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + when(dao.getAllForPeerUpToAgeInDays(PUBKEY, SEVEN_DAYS)).thenReturn(List.of( new OnlineStatus(false, offlineSince), new OnlineStatus(true, longAgo) )); diff --git a/model/src/main/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarning.java b/model/src/main/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarning.java new file mode 100644 index 00000000..187f64af --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarning.java @@ -0,0 +1,4 @@ +package de.cotto.lndmanagej.model; + +public record NodeOnlineChangesWarning(int changes) implements NodeWarning { +} diff --git a/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java b/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java index 63887791..e024a431 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java @@ -2,8 +2,12 @@ package de.cotto.lndmanagej.model; import java.time.ZonedDateTime; -public record OnlineReport(boolean online, ZonedDateTime since, int onlinePercentageLastWeek) { - public static OnlineReport createFromStatus(OnlineStatus onlineStatus, int onlinePercentageLastWeek) { - return new OnlineReport(onlineStatus.online(), onlineStatus.since(), onlinePercentageLastWeek); +public record OnlineReport(boolean online, ZonedDateTime since, int onlinePercentageLastWeek, int changesLastWeek) { + public static OnlineReport createFromStatus( + OnlineStatus onlineStatus, + int onlinePercentageLastWeek, + int changesLastWeek + ) { + return new OnlineReport(onlineStatus.online(), onlineStatus.since(), onlinePercentageLastWeek, changesLastWeek); } } diff --git a/model/src/test/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarningTest.java b/model/src/test/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarningTest.java new file mode 100644 index 00000000..fe2abf61 --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/NodeOnlineChangesWarningTest.java @@ -0,0 +1,13 @@ +package de.cotto.lndmanagej.model; + +import org.junit.jupiter.api.Test; + +import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_CHANGES_WARNING; +import static org.assertj.core.api.Assertions.assertThat; + +class NodeOnlineChangesWarningTest { + @Test + void name() { + assertThat(NODE_ONLINE_CHANGES_WARNING.changes()).isEqualTo(123); + } +} \ No newline at end of file diff --git a/model/src/test/java/de/cotto/lndmanagej/model/NodeWarningsTest.java b/model/src/test/java/de/cotto/lndmanagej/model/NodeWarningsTest.java index 8f5484cc..d13abcb2 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/NodeWarningsTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/NodeWarningsTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import java.util.List; +import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_CHANGES_WARNING; import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_PERCENTAGE_WARNING; import static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS; import static org.assertj.core.api.Assertions.assertThat; @@ -11,7 +12,8 @@ import static org.assertj.core.api.Assertions.assertThat; class NodeWarningsTest { @Test void warnings() { - assertThat(NODE_WARNINGS.warnings()).containsExactly(NODE_ONLINE_PERCENTAGE_WARNING); + assertThat(NODE_WARNINGS.warnings()) + .containsExactly(NODE_ONLINE_PERCENTAGE_WARNING, NODE_ONLINE_CHANGES_WARNING); } @Test diff --git a/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java b/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java index 1417d0e3..43c9deef 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java @@ -26,8 +26,14 @@ class OnlineReportTest { assertThat(ONLINE_REPORT.onlinePercentageLastWeek()).isEqualTo(77); } + @Test + void changesLastWeek() { + assertThat(ONLINE_REPORT.changesLastWeek()).isEqualTo(5); + } + @Test void createFromOnlineStatus() { - assertThat(OnlineReport.createFromStatus(ONLINE_STATUS, 77)).isEqualTo(ONLINE_REPORT); + assertThat(OnlineReport.createFromStatus(ONLINE_STATUS, 77, 5)) + .isEqualTo(ONLINE_REPORT); } } \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningFixtures.java index 48486e1c..69b14c59 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningFixtures.java @@ -3,4 +3,6 @@ package de.cotto.lndmanagej.model; public class NodeWarningFixtures { public static final NodeOnlinePercentageWarning NODE_ONLINE_PERCENTAGE_WARNING = new NodeOnlinePercentageWarning(51); + public static final NodeOnlineChangesWarning NODE_ONLINE_CHANGES_WARNING = + new NodeOnlineChangesWarning(123); } diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningsFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningsFixtures.java index a7c87d4d..05e4cdca 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningsFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeWarningsFixtures.java @@ -1,9 +1,11 @@ package de.cotto.lndmanagej.model; +import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_CHANGES_WARNING; import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_PERCENTAGE_WARNING; public class NodeWarningsFixtures { public static final NodeWarnings NODE_WARNINGS = new NodeWarnings( - NODE_ONLINE_PERCENTAGE_WARNING + NODE_ONLINE_PERCENTAGE_WARNING, + NODE_ONLINE_CHANGES_WARNING ); } diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java index 75f28d97..ba0101f3 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java @@ -7,6 +7,6 @@ import static java.time.ZoneOffset.UTC; public class OnlineReportFixtures { public static final ZonedDateTime TIMESTAMP = LocalDateTime.of(2021, 12, 23, 1, 2, 3).atZone(UTC); - public static final OnlineReport ONLINE_REPORT = new OnlineReport(true, TIMESTAMP, 77); - public static final OnlineReport ONLINE_REPORT_OFFLINE = new OnlineReport(false, TIMESTAMP, 66); + public static final OnlineReport ONLINE_REPORT = new OnlineReport(true, TIMESTAMP, 77, 5); + public static final OnlineReport ONLINE_REPORT_OFFLINE = new OnlineReport(false, TIMESTAMP, 85, 123); } diff --git a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java index 9b58c630..cb2bc8f7 100644 --- a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java +++ b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java @@ -12,5 +12,5 @@ public interface OnlinePeersDao { Optional getMostRecentOnlineStatus(Pubkey pubkey); - List getAllForPeer(Pubkey pubkey); + List getAllForPeerUpToAgeInDays(Pubkey pubkey, int dayThreshold); } diff --git a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java index b3264c39..ca1e096f 100644 --- a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java +++ b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java @@ -6,7 +6,10 @@ import de.cotto.lndmanagej.onlinepeers.OnlinePeersDao; import org.springframework.stereotype.Component; import javax.transaction.Transactional; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -30,9 +33,19 @@ class OnlinePeersDaoImpl implements OnlinePeersDao { } @Override - public List getAllForPeer(Pubkey pubkey) { - return repository.findByPubkeyOrderByTimestampDesc(pubkey.toString()).stream() + public List getAllForPeerUpToAgeInDays(Pubkey pubkey, int maximumAgeInDays) { + ZonedDateTime threshold = ZonedDateTime.now(ZoneOffset.UTC).minusDays(maximumAgeInDays); + List result = new ArrayList<>(); + Iterator iterator = repository.findByPubkeyOrderByTimestampDesc(pubkey.toString()) .map(OnlinePeerJpaDto::toModel) - .toList(); + .iterator(); + while (iterator.hasNext()) { + OnlineStatus onlineStatus = iterator.next(); + result.add(onlineStatus); + if (onlineStatus.since().isBefore(threshold)) { + break; + } + } + return result; } } diff --git a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java index 9960bb14..4d127087 100644 --- a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java +++ b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java @@ -2,11 +2,11 @@ package de.cotto.lndmanagej.onlinepeers.persistence; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; import java.util.Optional; +import java.util.stream.Stream; public interface OnlinePeersRepository extends JpaRepository { Optional findTopByPubkeyOrderByTimestampDesc(String pubkey); - List findByPubkeyOrderByTimestampDesc(String pubkey); + Stream findByPubkeyOrderByTimestampDesc(String pubkey); } diff --git a/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java b/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java index 4bbdfc6a..d95fe28c 100644 --- a/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java +++ b/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java @@ -1,5 +1,6 @@ package de.cotto.lndmanagej.onlinepeers.persistence; +import de.cotto.lndmanagej.model.OnlineStatus; import de.cotto.lndmanagej.model.Pubkey; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -7,12 +8,13 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.List; +import java.time.temporal.ChronoUnit; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; -import static de.cotto.lndmanagej.model.OnlineStatusFixtures.ONLINE_STATUS; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static org.assertj.core.api.Assertions.assertThat; @@ -22,7 +24,8 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OnlinePeersDaoImplTest { - private static final ZonedDateTime TIMESTAMP = ONLINE_STATUS.since(); + private static final ZonedDateTime NOW = ZonedDateTime.now(ZoneOffset.UTC); + private static final int DAY_THRESHOLD = 10; @InjectMocks private OnlinePeersDaoImpl dao; @@ -32,13 +35,13 @@ class OnlinePeersDaoImplTest { @Test void saveOnlineStatus_online() { - dao.saveOnlineStatus(PUBKEY, true, TIMESTAMP); + dao.saveOnlineStatus(PUBKEY, true, NOW); verifySave(PUBKEY, true); } @Test void saveOnlineStatus_offline() { - dao.saveOnlineStatus(PUBKEY_2, false, TIMESTAMP); + dao.saveOnlineStatus(PUBKEY_2, false, NOW); verifySave(PUBKEY_2, false); } @@ -49,17 +52,38 @@ class OnlinePeersDaoImplTest { @Test void getMostRecentOnlineStatus() { - OnlinePeerJpaDto dto = new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP); + OnlinePeerJpaDto dto = new OnlinePeerJpaDto(PUBKEY, true, NOW); when(repository.findTopByPubkeyOrderByTimestampDesc(PUBKEY.toString())).thenReturn(Optional.of(dto)); - assertThat(dao.getMostRecentOnlineStatus(PUBKEY)).contains(ONLINE_STATUS); + assertThat(dao.getMostRecentOnlineStatus(PUBKEY)).contains(new OnlineStatus( + true, + NOW.truncatedTo(ChronoUnit.SECONDS) + )); } @Test - void getAllForPeer() { - OnlinePeerJpaDto dto1 = new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP); - OnlinePeerJpaDto dto2 = new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.plusSeconds(1)); - when(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).thenReturn(List.of(dto1, dto2)); - assertThat(dao.getAllForPeer(PUBKEY)).containsExactly(dto1.toModel(), dto2.toModel()); + void getAllForPeerAfterDays_threshold_covers_all_entries() { + OnlinePeerJpaDto dto1 = new OnlinePeerJpaDto(PUBKEY, true, NOW); + OnlinePeerJpaDto dto2 = new OnlinePeerJpaDto(PUBKEY, false, NOW.minusSeconds(1)); + when(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).thenReturn(Stream.of(dto1, dto2)); + assertThat(dao.getAllForPeerUpToAgeInDays(PUBKEY, DAY_THRESHOLD)) + .containsExactly(dto1.toModel(), dto2.toModel()); + } + + @Test + void getAllForPeerAfterDays_includes_first_entry_starting_after_threshold() { + OnlinePeerJpaDto dto1 = new OnlinePeerJpaDto(PUBKEY, true, NOW); + OnlinePeerJpaDto dto2 = new OnlinePeerJpaDto(PUBKEY, false, NOW.plusDays(DAY_THRESHOLD).minusYears(1)); + when(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).thenReturn(Stream.of(dto1, dto2)); + assertThat(dao.getAllForPeerUpToAgeInDays(PUBKEY, DAY_THRESHOLD)).hasSize(2); + } + + @Test + void getAllForPeerAfterDays_ignores_older_entries() { + OnlinePeerJpaDto dto1 = new OnlinePeerJpaDto(PUBKEY, true, NOW); + OnlinePeerJpaDto dto2 = new OnlinePeerJpaDto(PUBKEY, false, NOW.minusYears(1)); + OnlinePeerJpaDto dto3 = new OnlinePeerJpaDto(PUBKEY, false, NOW.minusYears(2)); + when(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).thenReturn(Stream.of(dto1, dto2, dto3)); + assertThat(dao.getAllForPeerUpToAgeInDays(PUBKEY, DAY_THRESHOLD)).hasSize(2); } private void verifySave(Pubkey pubkey, boolean expected) { diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java index 53bea253..1144b1e7 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -98,11 +98,13 @@ class NodeControllerIT { .andExpect(jsonPath("$.feeReport.earned", is("1234"))) .andExpect(jsonPath("$.feeReport.sourced", is("567"))) .andExpect(jsonPath("$.nodeWarnings[0].onlinePercentage", is(51))) + .andExpect(jsonPath("$.nodeWarnings[1].changes", is(123))) .andExpect(jsonPath("$.onChainCosts.openCosts", is("1000"))) .andExpect(jsonPath("$.onChainCosts.closeCosts", is("2000"))) .andExpect(jsonPath("$.onChainCosts.sweepCosts", is("3000"))) .andExpect(jsonPath("$.onlineReport.online", is(true))) .andExpect(jsonPath("$.onlineReport.onlinePercentageLastWeek", is(77))) + .andExpect(jsonPath("$.onlineReport.changesLastWeek", is(5))) .andExpect(jsonPath("$.onlineReport.since", is("2021-12-23T01:02:03Z"))); } diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java index a2e1931d..84a21b45 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java @@ -4,9 +4,14 @@ import de.cotto.lndmanagej.model.OnlineReport; import java.time.format.DateTimeFormatter; -public record OnlineReportDto(boolean online, String since, int onlinePercentageLastWeek) { +public record OnlineReportDto(boolean online, String since, int onlinePercentageLastWeek, int changesLastWeek) { public static OnlineReportDto createFromModel(OnlineReport onlineReport) { String formattedDateTime = onlineReport.since().format(DateTimeFormatter.ISO_INSTANT); - return new OnlineReportDto(onlineReport.online(), formattedDateTime, onlineReport.onlinePercentageLastWeek()); + return new OnlineReportDto( + onlineReport.online(), + formattedDateTime, + onlineReport.onlinePercentageLastWeek(), + onlineReport.changesLastWeek() + ); } } diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java index c97899c8..a34645f6 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java @@ -16,7 +16,8 @@ class OnlineReportDtoTest { new OnlineReportDto( ONLINE_REPORT.online(), ONLINE_REPORT.since().toString(), - ONLINE_REPORT.onlinePercentageLastWeek() + ONLINE_REPORT.onlinePercentageLastWeek(), + ONLINE_REPORT.changesLastWeek() ) ); } @@ -29,7 +30,7 @@ class OnlineReportDtoTest { @Test void since_zero_seconds() { ZonedDateTime timeWithZeroSeconds = ZonedDateTime.of(2021, 12, 23, 1, 2, 0, 0, ZoneOffset.UTC); - OnlineReport onlineReport = new OnlineReport(true, timeWithZeroSeconds, 77); + OnlineReport onlineReport = new OnlineReport(true, timeWithZeroSeconds, 77, 123); assertThat(OnlineReportDto.createFromModel(onlineReport).since()).isEqualTo("2021-12-23T01:02:00Z"); } } \ No newline at end of file