mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2026-01-29 18:54:29 +01:00
Merge branch 'main' into fix-5746
This commit is contained in:
@@ -13,10 +13,7 @@ import de.cotto.lndmanagej.model.ForceClosingChannel;
|
||||
import de.cotto.lndmanagej.model.LocalChannel;
|
||||
import de.cotto.lndmanagej.model.LocalOpenChannel;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.model.WaitingCloseChannel;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
import de.cotto.lndmanagej.transactions.service.TransactionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -36,14 +33,12 @@ public class ChannelService {
|
||||
private static final Duration CACHE_REFRESH = Duration.ofSeconds(30);
|
||||
|
||||
private final GrpcChannels grpcChannels;
|
||||
private final TransactionService transactionService;
|
||||
private final LoadingCache<Object, Set<LocalOpenChannel>> localOpenChannelsCache;
|
||||
private final LoadingCache<Object, Map<ChannelId, ClosedChannel>> closedChannelsCache;
|
||||
private final LoadingCache<Object, Set<ForceClosingChannel>> forceClosingChannelsCache;
|
||||
private final LoadingCache<Object, Set<WaitingCloseChannel>> waitingCloseChannelsCache;
|
||||
|
||||
public ChannelService(
|
||||
TransactionService transactionService,
|
||||
GrpcChannels grpcChannels,
|
||||
GrpcClosedChannels grpcClosedChannels
|
||||
) {
|
||||
@@ -52,7 +47,6 @@ public class ChannelService {
|
||||
.withRefresh(CACHE_REFRESH)
|
||||
.withExpiry(CACHE_EXPIRY)
|
||||
.build(grpcChannels::getChannels);
|
||||
this.transactionService = transactionService;
|
||||
closedChannelsCache = new CacheBuilder()
|
||||
.withRefresh(CACHE_REFRESH)
|
||||
.withExpiry(CACHE_EXPIRY)
|
||||
@@ -174,8 +168,7 @@ public class ChannelService {
|
||||
).parallel().map(Supplier::get).flatMap(Collection::stream);
|
||||
}
|
||||
|
||||
public Optional<Integer> getOpenHeight(Channel channel) {
|
||||
TransactionHash openTransactionHash = channel.getChannelPoint().getTransactionHash();
|
||||
return transactionService.getTransaction(openTransactionHash).map(Transaction::blockHeight);
|
||||
public int getOpenHeight(Channel channel) {
|
||||
return channel.getId().getBlockHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,7 @@ public class FlowService {
|
||||
getSumOfAmounts(forwardingEventsService.getEventsWithOutgoingChannel(channelId, maxAge));
|
||||
List<ForwardingEvent> incomingEvents = forwardingEventsService.getEventsWithIncomingChannel(channelId, maxAge);
|
||||
Coins forwardedReceived = getSumOfAmounts(incomingEvents);
|
||||
Coins forwardingFeesReceived = getSumOfFees(incomingEvents
|
||||
);
|
||||
Coins forwardingFeesReceived = getSumOfFees(incomingEvents);
|
||||
Coins rebalanceSent = rebalanceService.getAmountFromChannel(channelId, maxAge);
|
||||
Coins rebalanceReceived = rebalanceService.getAmountToChannel(channelId, maxAge);
|
||||
Coins rebalanceSupportSent = rebalanceService.getSupportAsSourceAmountFromChannel(channelId, maxAge);
|
||||
|
||||
@@ -63,7 +63,6 @@ public class NodeFlowWarningsProvider implements NodeWarningsProvider {
|
||||
private int getDaysToConsider(Pubkey pubkey) {
|
||||
OptionalInt openHeightOldestOpenChannel = channelService.getOpenChannelsWith(pubkey).stream()
|
||||
.map(channelService::getOpenHeight)
|
||||
.flatMap(Optional::stream)
|
||||
.mapToInt(h -> h)
|
||||
.max();
|
||||
if (openHeightOldestOpenChannel.isEmpty()) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.model.Channel;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.FeeReport;
|
||||
import de.cotto.lndmanagej.model.LocalOpenChannel;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.model.Rating;
|
||||
import de.cotto.lndmanagej.model.RebalanceReport;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class RatingService {
|
||||
private static final int EXPECTED_MINUTES_PER_BLOCK = 10;
|
||||
private static final double MINUTES_PER_DAY = 24 * 60;
|
||||
private static final int MIN_AGE_FOR_ANALYSIS_DAYS = 30;
|
||||
private static final Duration DURATION_FOR_ANALYSIS = Duration.ofDays(MIN_AGE_FOR_ANALYSIS_DAYS);
|
||||
|
||||
private final ChannelService channelService;
|
||||
private final OwnNodeService ownNodeService;
|
||||
private final FeeService feeService;
|
||||
private final RebalanceService rebalanceService;
|
||||
private final PolicyService policyService;
|
||||
|
||||
public RatingService(
|
||||
ChannelService channelService,
|
||||
OwnNodeService ownNodeService,
|
||||
FeeService feeService,
|
||||
RebalanceService rebalanceService,
|
||||
PolicyService policyService
|
||||
) {
|
||||
this.channelService = channelService;
|
||||
this.ownNodeService = ownNodeService;
|
||||
this.feeService = feeService;
|
||||
this.rebalanceService = rebalanceService;
|
||||
this.policyService = policyService;
|
||||
}
|
||||
|
||||
public Rating getRatingForPeer(Pubkey peer) {
|
||||
return channelService.getOpenChannelsWith(peer).stream()
|
||||
.map(Channel::getId).map(this::getRatingForChannel)
|
||||
.flatMap(Optional::stream)
|
||||
.reduce(Rating.EMPTY, Rating::add);
|
||||
}
|
||||
|
||||
public Optional<Rating> getRatingForChannel(ChannelId channelId) {
|
||||
int ageInDays = getAgeInDays(channelId);
|
||||
if (ageInDays < MIN_AGE_FOR_ANALYSIS_DAYS) {
|
||||
return Optional.of(Rating.EMPTY);
|
||||
}
|
||||
LocalOpenChannel localOpenChannel = channelService.getOpenChannel(channelId).orElse(null);
|
||||
if (localOpenChannel == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
FeeReport feeReport =
|
||||
feeService.getFeeReportForChannel(channelId, DURATION_FOR_ANALYSIS);
|
||||
RebalanceReport rebalanceReport =
|
||||
rebalanceService.getReportForChannel(channelId, DURATION_FOR_ANALYSIS);
|
||||
long feeRate = policyService.getMinimumFeeRateTo(localOpenChannel.getRemotePubkey()).orElse(0L);
|
||||
long localAvailableMilliSat = localOpenChannel.getBalanceInformation().localAvailable().milliSatoshis();
|
||||
|
||||
long rating = 1;
|
||||
rating += feeReport.earned().milliSatoshis();
|
||||
rating += feeReport.sourced().milliSatoshis();
|
||||
rating += rebalanceReport.supportAsSourceAmount().milliSatoshis() / 10;
|
||||
rating += rebalanceReport.supportAsTargetAmount().milliSatoshis() / 10;
|
||||
rating += (long) (1.0 * feeRate * localAvailableMilliSat / 1_000 / 1_000_000 / 10);
|
||||
return Optional.of(new Rating(rating));
|
||||
}
|
||||
|
||||
private int getAgeInDays(ChannelId channelId) {
|
||||
int channelAgeInBlocks = ownNodeService.getBlockHeight() - channelId.getBlockHeight();
|
||||
return (int) Math.ceil(channelAgeInBlocks * 1.0 * EXPECTED_MINUTES_PER_BLOCK / MINUTES_PER_DAY);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.grpc.GrpcChannels;
|
||||
import de.cotto.lndmanagej.grpc.GrpcClosedChannels;
|
||||
import de.cotto.lndmanagej.transactions.service.TransactionService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -31,8 +30,6 @@ import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
|
||||
import static de.cotto.lndmanagej.model.WaitingCloseChannelFixtures.WAITING_CLOSE_CHANNEL;
|
||||
import static de.cotto.lndmanagej.model.WaitingCloseChannelFixtures.WAITING_CLOSE_CHANNEL_2;
|
||||
import static de.cotto.lndmanagej.model.WaitingCloseChannelFixtures.WAITING_CLOSE_CHANNEL_TO_NODE_3;
|
||||
import static de.cotto.lndmanagej.transactions.model.TransactionFixtures.BLOCK_HEIGHT;
|
||||
import static de.cotto.lndmanagej.transactions.model.TransactionFixtures.TRANSACTION;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -41,9 +38,6 @@ class ChannelServiceTest {
|
||||
@InjectMocks
|
||||
private ChannelService channelService;
|
||||
|
||||
@Mock
|
||||
private TransactionService transactionService;
|
||||
|
||||
@Mock
|
||||
private GrpcChannels grpcChannels;
|
||||
|
||||
@@ -275,17 +269,8 @@ class ChannelServiceTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOpenHeight_unknown_transaction() {
|
||||
when(transactionService.getTransaction(LOCAL_OPEN_CHANNEL.getChannelPoint().getTransactionHash()))
|
||||
.thenReturn(Optional.empty());
|
||||
assertThat(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOpenHeight() {
|
||||
when(transactionService.getTransaction(LOCAL_OPEN_CHANNEL.getChannelPoint().getTransactionHash()))
|
||||
.thenReturn(Optional.of(TRANSACTION));
|
||||
assertThat(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL)).contains(BLOCK_HEIGHT);
|
||||
assertThat(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL)).isEqualTo(712_345);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class NodeFlowWarningsProviderTest {
|
||||
private static final int EXPECTED_BLOCKS_PER_DAY = 144;
|
||||
|
||||
@InjectMocks
|
||||
private NodeFlowWarningsProvider warningsProvider;
|
||||
|
||||
@@ -55,7 +56,7 @@ class NodeFlowWarningsProviderTest {
|
||||
@Test
|
||||
void getNodeWarnings_no_flow_old_channel() {
|
||||
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL));
|
||||
when(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL)).thenReturn(Optional.of(BLOCK_HEIGHT));
|
||||
when(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL)).thenReturn(BLOCK_HEIGHT);
|
||||
assertThat(warningsProvider.getNodeWarnings(PUBKEY)).containsExactly(new NodeNoFlowWarning(90));
|
||||
}
|
||||
|
||||
@@ -105,13 +106,13 @@ class NodeFlowWarningsProviderTest {
|
||||
when(configurationService.getIntegerValue(NODE_FLOW_MAXIMUM_DAYS_TO_CONSIDER))
|
||||
.thenReturn(Optional.of(120));
|
||||
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL_2));
|
||||
when(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL_2)).thenReturn(Optional.of(BLOCK_HEIGHT));
|
||||
when(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL_2)).thenReturn(BLOCK_HEIGHT);
|
||||
assertThat(warningsProvider.getNodeWarnings(PUBKEY)).containsExactly(new NodeNoFlowWarning(120));
|
||||
}
|
||||
|
||||
private void mockOpenChannelWithAgeInBlocks(int channelAgeInBlocks) {
|
||||
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL));
|
||||
when(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL)).thenReturn(Optional.of(BLOCK_HEIGHT));
|
||||
when(channelService.getOpenHeight(LOCAL_OPEN_CHANNEL)).thenReturn(BLOCK_HEIGHT);
|
||||
when(ownNodeService.getBlockHeight()).thenReturn(BLOCK_HEIGHT + channelAgeInBlocks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.FeeReport;
|
||||
import de.cotto.lndmanagej.model.Rating;
|
||||
import de.cotto.lndmanagej.model.RebalanceReport;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL;
|
||||
import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_2;
|
||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assumptions.assumeThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RatingServiceTest {
|
||||
private static final Duration DURATION_FOR_ANALYSIS = Duration.ofDays(30);
|
||||
|
||||
@InjectMocks
|
||||
private RatingService ratingService;
|
||||
|
||||
@Mock
|
||||
private ChannelService channelService;
|
||||
|
||||
@Mock
|
||||
private OwnNodeService ownNodeService;
|
||||
|
||||
@Mock
|
||||
private FeeService feeService;
|
||||
|
||||
@Mock
|
||||
private RebalanceService rebalanceService;
|
||||
|
||||
@Mock
|
||||
private PolicyService policyService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
int daysAhead = LOCAL_OPEN_CHANNEL_2.getId().getBlockHeight() + 100 * 24 * 60 / 10;
|
||||
lenient().when(ownNodeService.getBlockHeight()).thenReturn(daysAhead);
|
||||
lenient().when(feeService.getFeeReportForChannel(any(), any())).thenReturn(FeeReport.EMPTY);
|
||||
lenient().when(rebalanceService.getReportForChannel(any(), any())).thenReturn(RebalanceReport.EMPTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForPeer_no_channel() {
|
||||
assertThat(ratingService.getRatingForPeer(PUBKEY)).isEqualTo(Rating.EMPTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForPeer_channel_too_young() {
|
||||
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL));
|
||||
assertThat(ratingService.getRatingForPeer(PUBKEY)).isEqualTo(Rating.EMPTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForPeer_one_channel() {
|
||||
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL));
|
||||
when(channelService.getOpenChannel(LOCAL_OPEN_CHANNEL.getId())).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL));
|
||||
assertThat(ratingService.getRatingForPeer(PUBKEY)).isEqualTo(new Rating(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForPeer_two_channels() {
|
||||
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_2));
|
||||
when(channelService.getOpenChannel(LOCAL_OPEN_CHANNEL.getId())).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL));
|
||||
when(channelService.getOpenChannel(LOCAL_OPEN_CHANNEL_2.getId())).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_2));
|
||||
assertThat(ratingService.getRatingForPeer(PUBKEY)).isEqualTo(new Rating(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForChannel_not_found() {
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForChannel_channel_too_young() {
|
||||
when(ownNodeService.getBlockHeight()).thenReturn(CHANNEL_ID.getBlockHeight() + 29 * 24 * 60 / 10);
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(Rating.EMPTY);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetRatingForChannel {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(channelService.getOpenChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL));
|
||||
mockOutgoingFeeRate(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void idle() {
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(new Rating(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void earned() {
|
||||
Coins feesEarned = Coins.ofMilliSatoshis(123);
|
||||
when(feeService.getFeeReportForChannel(CHANNEL_ID, DURATION_FOR_ANALYSIS))
|
||||
.thenReturn(new FeeReport(feesEarned, Coins.NONE));
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(new Rating(1 + 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sourced() {
|
||||
Coins feesSourced = Coins.ofMilliSatoshis(123);
|
||||
when(feeService.getFeeReportForChannel(CHANNEL_ID, DURATION_FOR_ANALYSIS))
|
||||
.thenReturn(new FeeReport(Coins.NONE, feesSourced));
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(new Rating(1 + 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rebalance_source_support() {
|
||||
RebalanceReport rebalanceReport = new RebalanceReport(
|
||||
Coins.NONE,
|
||||
Coins.NONE,
|
||||
Coins.NONE,
|
||||
Coins.NONE,
|
||||
Coins.ofMilliSatoshis(1_234),
|
||||
Coins.NONE
|
||||
);
|
||||
lenient().when(rebalanceService.getReportForChannel(CHANNEL_ID, DURATION_FOR_ANALYSIS))
|
||||
.thenReturn(rebalanceReport);
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(new Rating(1 + 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rebalance_target_support() {
|
||||
RebalanceReport rebalanceReport = new RebalanceReport(
|
||||
Coins.NONE,
|
||||
Coins.NONE,
|
||||
Coins.NONE,
|
||||
Coins.NONE,
|
||||
Coins.NONE,
|
||||
Coins.ofMilliSatoshis(1_234)
|
||||
);
|
||||
lenient().when(rebalanceService.getReportForChannel(CHANNEL_ID, DURATION_FOR_ANALYSIS))
|
||||
.thenReturn(rebalanceReport);
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(new Rating(1 + 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
void potential_forward_fees() {
|
||||
int feeRate = 123_456;
|
||||
mockOutgoingFeeRate(feeRate);
|
||||
long balanceMilliSat = LOCAL_OPEN_CHANNEL.getBalanceInformation().localAvailable().milliSatoshis();
|
||||
long maxEarnings = (long) (1.0 * feeRate * balanceMilliSat / 1_000 / 1_000_000.0);
|
||||
assumeThat(maxEarnings).isGreaterThanOrEqualTo(10);
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(new Rating(1 + maxEarnings / 10));
|
||||
}
|
||||
}
|
||||
|
||||
private void mockOutgoingFeeRate(long feeRate) {
|
||||
lenient().when(policyService.getMinimumFeeRateTo(LOCAL_OPEN_CHANNEL.getRemotePubkey()))
|
||||
.thenReturn(Optional.of(feeRate));
|
||||
}
|
||||
}
|
||||
31
model/src/main/java/de/cotto/lndmanagej/model/Rating.java
Normal file
31
model/src/main/java/de/cotto/lndmanagej/model/Rating.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record Rating(Optional<Long> rating) {
|
||||
public static final Rating EMPTY = new Rating(Optional.empty());
|
||||
|
||||
public Rating(long rating) {
|
||||
this(Optional.of(rating));
|
||||
}
|
||||
|
||||
public Rating add(Rating other) {
|
||||
Long thisRating = rating.orElse(null);
|
||||
if (thisRating == null) {
|
||||
return other;
|
||||
}
|
||||
Long otherRating = other.rating.orElse(null);
|
||||
if (otherRating == null) {
|
||||
return this;
|
||||
}
|
||||
return new Rating(thisRating + otherRating);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return rating.isEmpty();
|
||||
}
|
||||
|
||||
public long getRating() {
|
||||
return rating.orElse(0L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RatingTest {
|
||||
@Test
|
||||
void empty() {
|
||||
assertThat(Rating.EMPTY).isEqualTo(new Rating(Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEmpty() {
|
||||
assertThat(Rating.EMPTY.isEmpty()).isTrue();
|
||||
assertThat(new Rating(1).isEmpty()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRating() {
|
||||
assertThat(Rating.EMPTY.getRating()).isEqualTo(0);
|
||||
assertThat(new Rating(1).getRating()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void add_empty_and_empty() {
|
||||
assertThat(Rating.EMPTY.add(Rating.EMPTY)).isEqualTo(Rating.EMPTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void add_empty_and_something() {
|
||||
Rating something = new Rating(1);
|
||||
assertThat(Rating.EMPTY.add(something)).isEqualTo(something);
|
||||
}
|
||||
|
||||
@Test
|
||||
void add_something_and_empty() {
|
||||
Rating something = new Rating(1);
|
||||
assertThat(something.add(Rating.EMPTY)).isEqualTo(something);
|
||||
}
|
||||
|
||||
@Test
|
||||
void add_something_and_something() {
|
||||
Rating something = new Rating(1);
|
||||
assertThat(something.add(something)).isEqualTo(new Rating(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.cotto.lndmanagej.controller;
|
||||
|
||||
import de.cotto.lndmanagej.model.ChannelIdResolver;
|
||||
import de.cotto.lndmanagej.model.Rating;
|
||||
import de.cotto.lndmanagej.service.RatingService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(controllers = RatingController.class)
|
||||
class RatingControllerIT {
|
||||
private static final String PREFIX = "/api/";
|
||||
private static final String RATING = "/rating";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
@SuppressWarnings("unused")
|
||||
private ChannelIdResolver channelIdResolver;
|
||||
|
||||
@MockBean
|
||||
private RatingService ratingService;
|
||||
|
||||
@Test
|
||||
void getRatingForPeer() throws Exception {
|
||||
when(ratingService.getRatingForPeer(PUBKEY)).thenReturn(new Rating(123));
|
||||
mockMvc.perform(get(PREFIX + "/peer/" + PUBKEY + RATING))
|
||||
.andExpect(content().json("{\"rating\": 123, \"message\": \"\"}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForPeer_no_rating() throws Exception {
|
||||
when(ratingService.getRatingForPeer(PUBKEY)).thenReturn(Rating.EMPTY);
|
||||
mockMvc.perform(get(PREFIX + "/peer/" + PUBKEY + RATING))
|
||||
.andExpect(content().json("{\"rating\": 0, \"message\": \"Unable to compute rating\"}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForChannel() throws Exception {
|
||||
when(ratingService.getRatingForChannel(CHANNEL_ID)).thenReturn(Optional.of(new Rating(123)));
|
||||
mockMvc.perform(get(PREFIX + "/channel/" + CHANNEL_ID + RATING))
|
||||
.andExpect(content().json("{\"rating\": 123, \"message\": \"\"}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForChannel_not_found() throws Exception {
|
||||
mockMvc.perform(get(PREFIX + "/channel/" + CHANNEL_ID + RATING))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForChannel_empty() throws Exception {
|
||||
when(ratingService.getRatingForChannel(CHANNEL_ID)).thenReturn(Optional.of(Rating.EMPTY));
|
||||
mockMvc.perform(get(PREFIX + "/channel/" + CHANNEL_ID + RATING))
|
||||
.andExpect(content().json("{\"rating\": 0, \"message\": \"Unable to compute rating\"}"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.cotto.lndmanagej.controller;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration;
|
||||
import de.cotto.lndmanagej.controller.dto.RatingDto;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.model.Rating;
|
||||
import de.cotto.lndmanagej.service.RatingService;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@Import(ObjectMapperConfiguration.class)
|
||||
@RequestMapping("/api/")
|
||||
public class RatingController {
|
||||
private final RatingService ratingService;
|
||||
|
||||
public RatingController(RatingService ratingService) {
|
||||
this.ratingService = ratingService;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GetMapping("/peer/{peer}/rating")
|
||||
public RatingDto getRatingForPeer(@PathVariable Pubkey peer) {
|
||||
Rating rating = ratingService.getRatingForPeer(peer);
|
||||
return RatingDto.fromModel(rating);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GetMapping("/channel/{channelId}/rating")
|
||||
public RatingDto getRatingForChannel(ChannelId channelId) throws NotFoundException {
|
||||
return ratingService.getRatingForChannel(channelId)
|
||||
.map(RatingDto::fromModel)
|
||||
.orElseThrow(NotFoundException::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.cotto.lndmanagej.controller.dto;
|
||||
|
||||
import de.cotto.lndmanagej.model.Rating;
|
||||
|
||||
public record RatingDto(long rating, String message) {
|
||||
public static RatingDto fromModel(Rating rating) {
|
||||
if (rating.isEmpty()) {
|
||||
return new RatingDto(rating.getRating(), "Unable to compute rating");
|
||||
}
|
||||
return new RatingDto(rating.getRating(), "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.cotto.lndmanagej.controller;
|
||||
|
||||
import de.cotto.lndmanagej.controller.dto.RatingDto;
|
||||
import de.cotto.lndmanagej.model.Rating;
|
||||
import de.cotto.lndmanagej.service.RatingService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RatingControllerTest {
|
||||
@InjectMocks
|
||||
private RatingController ratingController;
|
||||
|
||||
@Mock
|
||||
private RatingService ratingService;
|
||||
|
||||
@Test
|
||||
void getRatingForPeer_empty() {
|
||||
when(ratingService.getRatingForPeer(PUBKEY)).thenReturn(Rating.EMPTY);
|
||||
assertThat(ratingController.getRatingForPeer(PUBKEY)).isEqualTo(RatingDto.fromModel(Rating.EMPTY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForPeer() {
|
||||
Rating rating = new Rating(123);
|
||||
when(ratingService.getRatingForPeer(PUBKEY)).thenReturn(rating);
|
||||
assertThat(ratingController.getRatingForPeer(PUBKEY)).isEqualTo(RatingDto.fromModel(rating));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForChannel_channel_not_found() {
|
||||
assertThatExceptionOfType(NotFoundException.class).isThrownBy(
|
||||
() -> ratingController.getRatingForChannel(CHANNEL_ID)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRatingForChannel() throws Exception {
|
||||
Rating rating = new Rating(1);
|
||||
when(ratingService.getRatingForChannel(CHANNEL_ID)).thenReturn(Optional.of(rating));
|
||||
assertThat(ratingController.getRatingForChannel(CHANNEL_ID)).isEqualTo(RatingDto.fromModel(rating));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.cotto.lndmanagej.controller.dto;
|
||||
|
||||
import de.cotto.lndmanagej.model.Rating;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RatingDtoTest {
|
||||
@Test
|
||||
void fromModel_empty() {
|
||||
assertThat(RatingDto.fromModel(Rating.EMPTY)).isEqualTo(new RatingDto(0, "Unable to compute rating"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromModel() {
|
||||
assertThat(RatingDto.fromModel(new Rating(1))).isEqualTo(new RatingDto(1, ""));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user