diff --git a/application/build.gradle b/application/build.gradle index 529c8868..ae447963 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -19,14 +19,11 @@ jacocoTestCoverageVerification { violationRules { rules.forEach {rule -> rule.limits.forEach {limit -> - if (limit.counter == 'CLASS') { - limit.minimum = 0.6 - } if (limit.counter == 'INSTRUCTION') { - limit.minimum = 0.94 + limit.minimum = 0.97 } if (limit.counter == 'METHOD') { - limit.minimum = 0.7 + limit.minimum = 0.95 } } } diff --git a/application/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java b/application/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java index b52c831b..1d670c82 100644 --- a/application/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/application/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -1,6 +1,6 @@ package de.cotto.lndmanagej.controller; -import de.cotto.lndmanagej.grpc.GrpcNodeInfo; +import de.cotto.lndmanagej.service.NodeService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -8,12 +8,17 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import java.util.List; + +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3; import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS; -import static de.cotto.lndmanagej.model.NodeFixtures.NODE; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; +import static org.hamcrest.core.Is.is; 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.jsonPath; @WebMvcTest(controllers = NodeController.class) class NodeControllerIT { @@ -21,11 +26,11 @@ class NodeControllerIT { private MockMvc mockMvc; @MockBean - private GrpcNodeInfo grpcNodeInfo; + private NodeService nodeService; @Test void getAlias() throws Exception { - when(grpcNodeInfo.getNode(PUBKEY)).thenReturn(NODE); + when(nodeService.getAlias(PUBKEY)).thenReturn(ALIAS); mockMvc.perform(get("/api/node/" + PUBKEY + "/alias")).andExpect(content().string(ALIAS)); } @@ -33,4 +38,12 @@ class NodeControllerIT { void getAlias_error() throws Exception { mockMvc.perform(get("/api/node/xxx/alias")).andExpect(MockMvcResultMatchers.status().isBadRequest()); } + + @Test + void getOpenChannelIds() throws Exception { + when(nodeService.getOpenChannelIds(PUBKEY)).thenReturn(List.of(CHANNEL_ID, CHANNEL_ID_3)); + mockMvc.perform(get("/api/node/" + PUBKEY + "/open-channels")) + .andExpect(jsonPath("$[0]", is(CHANNEL_ID.shortChannelId()))) + .andExpect(jsonPath("$[1]", is(CHANNEL_ID_3.shortChannelId()))); + } } \ No newline at end of file diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/NodeController.java b/application/src/main/java/de/cotto/lndmanagej/controller/NodeController.java index 22df065a..8eb83379 100644 --- a/application/src/main/java/de/cotto/lndmanagej/controller/NodeController.java +++ b/application/src/main/java/de/cotto/lndmanagej/controller/NodeController.java @@ -1,23 +1,34 @@ package de.cotto.lndmanagej.controller; -import de.cotto.lndmanagej.grpc.GrpcNodeInfo; +import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.Pubkey; +import de.cotto.lndmanagej.service.NodeService; 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 -@RequestMapping("/api/node/") -public class NodeController { - private final GrpcNodeInfo grpcNodeInfo; +import java.util.List; +import java.util.stream.Collectors; - public NodeController(GrpcNodeInfo grpcNodeInfo) { - this.grpcNodeInfo = grpcNodeInfo; +@RestController +@RequestMapping("/api/node/{pubkey}/") +public class NodeController { + private final NodeService nodeService; + + public NodeController(NodeService nodeService) { + this.nodeService = nodeService; } - @GetMapping("/{pubkey}/alias") + @GetMapping("/alias") public String getAlias(@PathVariable Pubkey pubkey) { - return grpcNodeInfo.getNode(pubkey).alias(); + return nodeService.getAlias(pubkey); + } + + @GetMapping("/open-channels") + public List getOpenChannelIds(@PathVariable Pubkey pubkey) { + return nodeService.getOpenChannelIds(pubkey).stream() + .map(ChannelId::shortChannelId) + .collect(Collectors.toList()); } } diff --git a/application/src/main/java/de/cotto/lndmanagej/service/NodeService.java b/application/src/main/java/de/cotto/lndmanagej/service/NodeService.java new file mode 100644 index 00000000..b65690b6 --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/service/NodeService.java @@ -0,0 +1,40 @@ +package de.cotto.lndmanagej.service; + +import de.cotto.lndmanagej.grpc.GrpcChannels; +import de.cotto.lndmanagej.grpc.GrpcNodeInfo; +import de.cotto.lndmanagej.model.Channel; +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Node; +import de.cotto.lndmanagej.model.Pubkey; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class NodeService { + private final GrpcNodeInfo grpcNodeInfo; + private final GrpcChannels grpcChannels; + + public NodeService(GrpcNodeInfo grpcNodeInfo, GrpcChannels grpcChannels) { + this.grpcNodeInfo = grpcNodeInfo; + this.grpcChannels = grpcChannels; + } + + public List getOpenChannelIds(Pubkey pubkey) { + Node node = getNode(pubkey); + return grpcChannels.getChannels().stream() + .filter(c -> c.getNodes().contains(node)) + .map(Channel::getId) + .sorted() + .collect(Collectors.toList()); + } + + public String getAlias(Pubkey pubkey) { + return getNode(pubkey).alias(); + } + + private Node getNode(Pubkey pubkey) { + return grpcNodeInfo.getNode(pubkey); + } +} diff --git a/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java b/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java index 956c0734..a43b464d 100644 --- a/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java @@ -1,14 +1,17 @@ package de.cotto.lndmanagej.controller; -import de.cotto.lndmanagej.grpc.GrpcNodeInfo; +import de.cotto.lndmanagej.service.NodeService; 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.List; + +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3; import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS; -import static de.cotto.lndmanagej.model.NodeFixtures.NODE; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -19,17 +22,20 @@ class NodeControllerTest { private NodeController nodeController; @Mock - private GrpcNodeInfo grpcNodeInfo; + private NodeService nodeService; @Test void getAlias() { - when(grpcNodeInfo.getNode(PUBKEY)).thenReturn(NODE); + when(nodeService.getAlias(PUBKEY)).thenReturn(ALIAS); assertThat(nodeController.getAlias(PUBKEY)).isEqualTo(ALIAS); } @Test - void getAlias_uppercase_pubkey() { - when(grpcNodeInfo.getNode(PUBKEY)).thenReturn(NODE); - assertThat(nodeController.getAlias(PUBKEY)).isEqualTo(ALIAS); + void getOpenChannelIds() { + when(nodeService.getOpenChannelIds(PUBKEY)).thenReturn(List.of(CHANNEL_ID, CHANNEL_ID_3)); + assertThat(nodeController.getOpenChannelIds(PUBKEY)).containsExactly( + CHANNEL_ID.shortChannelId(), + CHANNEL_ID_3.shortChannelId() + ); } } \ No newline at end of file diff --git a/application/src/test/java/de/cotto/lndmanagej/service/NodeServiceTest.java b/application/src/test/java/de/cotto/lndmanagej/service/NodeServiceTest.java new file mode 100644 index 00000000..5192b8ba --- /dev/null +++ b/application/src/test/java/de/cotto/lndmanagej/service/NodeServiceTest.java @@ -0,0 +1,66 @@ +package de.cotto.lndmanagej.service; + +import de.cotto.lndmanagej.grpc.GrpcChannels; +import de.cotto.lndmanagej.grpc.GrpcNodeInfo; +import de.cotto.lndmanagej.model.Channel; +import de.cotto.lndmanagej.model.ChannelFixtures; +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.Set; + +import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL; +import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_3; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3; +import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE_2; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE_3; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class}) +class NodeServiceTest { + @InjectMocks + private NodeService nodeService; + + @Mock + private GrpcNodeInfo grpcNodeInfo; + + @Mock + private GrpcChannels grpcChannels; + + @Test + void getAlias() { + when(grpcNodeInfo.getNode(PUBKEY)).thenReturn(NODE); + assertThat(nodeService.getAlias(PUBKEY)).isEqualTo(ALIAS); + } + + @Test + void getOpenChannelIds() { + when(grpcNodeInfo.getNode(PUBKEY)).thenReturn(NODE_2); + when(grpcChannels.getChannels()).thenReturn(Set.of(CHANNEL, CHANNEL_3)); + assertThat(nodeService.getOpenChannelIds(PUBKEY)).containsExactly(CHANNEL_ID, CHANNEL_ID_3); + } + + @Test + void getOpenChannelIds_ordered() { + when(grpcNodeInfo.getNode(PUBKEY)).thenReturn(NODE_2); + when(grpcChannels.getChannels()).thenReturn(Set.of(CHANNEL_3, CHANNEL)); + assertThat(nodeService.getOpenChannelIds(PUBKEY)).containsExactly(CHANNEL_ID, CHANNEL_ID_3); + } + + @Test + void getOpenChannelIds_ignores_channel_to_other_node() { + when(grpcNodeInfo.getNode(PUBKEY)).thenReturn(NODE_2); + Channel channel2 = ChannelFixtures.create(NODE, NODE_3, CHANNEL_ID_2); + when(grpcChannels.getChannels()).thenReturn(Set.of(CHANNEL, channel2, CHANNEL_3)); + assertThat(nodeService.getOpenChannelIds(PUBKEY)).containsExactly(CHANNEL_ID, CHANNEL_ID_3); + } +} \ No newline at end of file diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java index 500dda55..da86c747 100644 --- a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java @@ -7,7 +7,8 @@ import de.cotto.lndmanagej.model.Pubkey; import org.springframework.stereotype.Component; import java.util.Set; -import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toSet; @Component public class GrpcChannels { @@ -26,7 +27,9 @@ public class GrpcChannels { } public Set getChannels() { - return grpcService.getChannels().stream().map(this::toChannel).collect(Collectors.toSet()); + return grpcService.getChannels().stream() + .map(this::toChannel) + .collect(toSet()); } private Channel toChannel(lnrpc.Channel lndChannel) { diff --git a/model/src/main/java/de/cotto/lndmanagej/model/ChannelId.java b/model/src/main/java/de/cotto/lndmanagej/model/ChannelId.java index dbefc291..b343eedd 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/ChannelId.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/ChannelId.java @@ -1,6 +1,6 @@ package de.cotto.lndmanagej.model; -public record ChannelId(long shortChannelId) { +public record ChannelId(long shortChannelId) implements Comparable { private static final int EXPECTED_NUMBER_OF_SEGMENTS = 3; private static final long NOT_BEFORE = 430_103_660_018_532_352L; // January 1st 2016 @@ -28,4 +28,9 @@ public record ChannelId(long shortChannelId) { public String toString() { return String.valueOf(shortChannelId); } + + @Override + public int compareTo(ChannelId other) { + return Long.compare(shortChannelId, other.shortChannelId); + } } diff --git a/model/src/test/java/de/cotto/lndmanagej/model/ChannelIdTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ChannelIdTest.java index 61e53582..2c04500e 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/ChannelIdTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/ChannelIdTest.java @@ -104,6 +104,27 @@ class ChannelIdTest { } } + @Test + void testComparable_smaller() { + ChannelId channelId1 = ChannelId.fromShortChannelId(774_909_407_114_231_931L); + ChannelId channelId2 = ChannelId.fromShortChannelId(774_909_407_114_231_932L); + assertThat(channelId1.compareTo(channelId2)).isLessThan(0); + } + + @Test + void testComparable_same() { + ChannelId channelId1 = ChannelId.fromShortChannelId(774_909_407_114_231_931L); + ChannelId channelId2 = ChannelId.fromShortChannelId(774_909_407_114_231_931L); + assertThat(channelId1.compareTo(channelId2)).isEqualTo(0); + } + + @Test + void testComparable_larger() { + ChannelId channelId1 = ChannelId.fromShortChannelId(774_909_407_114_231_932L); + ChannelId channelId2 = ChannelId.fromShortChannelId(774_909_407_114_231_931L); + assertThat(channelId1.compareTo(channelId2)).isGreaterThan(0); + } + @Test void testEquals() { EqualsVerifier.forClass(ChannelId.class).verify(); diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java index 9872139b..495339fd 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java @@ -1,15 +1,28 @@ package de.cotto.lndmanagej.model; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3; import static de.cotto.lndmanagej.model.NodeFixtures.NODE; import static de.cotto.lndmanagej.model.NodeFixtures.NODE_2; -public class ChannelFixtures { +public final class ChannelFixtures { public static final Coins CAPACITY = Coins.ofSatoshis(21_000_000L); - public static final Channel CHANNEL = Channel.builder() - .withChannelId(ChannelIdFixtures.CHANNEL_ID) - .withCapacity(CAPACITY) - .withNode1(NODE) - .withNode2(NODE_2) - .build(); + public static final Channel CHANNEL = create(NODE, NODE_2, CHANNEL_ID); + public static final Channel CHANNEL_2 = create(NODE, NODE_2, CHANNEL_ID_2); + public static final Channel CHANNEL_3 = create(NODE, NODE_2, CHANNEL_ID_3); + + private ChannelFixtures() { + // do not instantiate + } + + public static Channel create(Node node1, Node node2, ChannelId channelId) { + return Channel.builder() + .withChannelId(channelId) + .withCapacity(CAPACITY) + .withNode1(node1) + .withNode2(node2) + .build(); + } } diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeFixtures.java index 7a6acab3..8614b885 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeFixtures.java @@ -4,10 +4,12 @@ import java.time.Instant; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3; public class NodeFixtures { public static final String ALIAS = "Node"; public static final String ALIAS_2 = "Another Node"; + public static final String ALIAS_3 = "Yet Another Node"; public static final int LAST_UPDATE = (int) Instant.now().getEpochSecond(); public static final Node NODE = Node.builder() @@ -22,6 +24,12 @@ public class NodeFixtures { .withLastUpdate(LAST_UPDATE) .build(); + public static final Node NODE_3 = Node.builder() + .withPubkey(PUBKEY_3) + .withAlias(ALIAS_3) + .withLastUpdate(LAST_UPDATE) + .build(); + public static final Node NODE_WITHOUT_ALIAS = Node.builder() .withPubkey(PUBKEY) .withLastUpdate(LAST_UPDATE) diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/PubkeyFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/PubkeyFixtures.java index 32e7b843..49dcdef4 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/PubkeyFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/PubkeyFixtures.java @@ -5,4 +5,6 @@ public class PubkeyFixtures { Pubkey.create("027abc123abc123abc123abc123123abc123abc123abc123abc123abc123abc123"); public static final Pubkey PUBKEY_2 = Pubkey.create("03fff0000000000000000000000000000000000000000000000000000000000000"); + public static final Pubkey PUBKEY_3 = + Pubkey.create("03fff1111111111111111111111111111111111111111111111111111111111111"); }