Merge branch 'main' into fix-5746

This commit is contained in:
Carsten Otto
2022-06-03 10:59:37 +02:00
14 changed files with 530 additions and 32 deletions

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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()) {

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View 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);
}
}

View File

@@ -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));
}
}

View File

@@ -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\"}"));
}
}

View File

@@ -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);
}
}

View File

@@ -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(), "");
}
}

View File

@@ -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));
}
}

View File

@@ -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, ""));
}
}