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 e8655412..9766fff4 100644 --- a/application/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/application/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -2,6 +2,7 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.metrics.Metrics; import de.cotto.lndmanagej.model.Node; +import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,6 +10,15 @@ 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.List; +import java.util.Set; + +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.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_2; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_3; import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS_2; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static org.hamcrest.core.Is.is; @@ -27,6 +37,9 @@ class NodeControllerIT { @MockBean private NodeService nodeService; + @MockBean + private ChannelService channelService; + @MockBean @SuppressWarnings("unused") private Metrics metrics; @@ -41,9 +54,22 @@ class NodeControllerIT { @Test void getDetails() throws Exception { when(nodeService.getNode(PUBKEY_2)).thenReturn(new Node(PUBKEY_2, ALIAS_2, 0, true)); + when(channelService.getOpenChannelsWith(PUBKEY_2)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_2)); + List channelIds = List.of(CHANNEL_ID.toString(), CHANNEL_ID_2.toString()); mockMvc.perform(get(NODE_PREFIX + "/details")) - .andExpect(jsonPath("$.pubkey", is(PUBKEY_2.toString()))) + .andExpect(jsonPath("$.node", is(PUBKEY_2.toString()))) .andExpect(jsonPath("$.alias", is(ALIAS_2))) + .andExpect(jsonPath("$.channels", is(channelIds))) .andExpect(jsonPath("$.online", is(true))); } + + @Test + void getOpenChannelIds_for_peer() throws Exception { + when(channelService.getOpenChannelsWith(PUBKEY_2)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_3)); + List channelIds = List.of(CHANNEL_ID.toString(), CHANNEL_ID_3.toString()); + mockMvc.perform(get(NODE_PREFIX + "/open-channels")) + .andExpect(jsonPath("$.node", is(PUBKEY_2.toString()))) + .andExpect(jsonPath("$.channels", is(channelIds))); + } + } \ No newline at end of file diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java index 4ac9062d..33bd466f 100644 --- a/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java +++ b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java @@ -1,12 +1,15 @@ package de.cotto.lndmanagej.controller; import com.codahale.metrics.MetricRegistry; +import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; +import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration; import de.cotto.lndmanagej.metrics.Metrics; import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.LocalChannel; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; +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; @@ -14,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/channel/{channelId}") +@Import(ObjectMapperConfiguration.class) public class ChannelDetailsController { private final ChannelService channelService; private final NodeService nodeService; diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsDto.java b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsDto.java deleted file mode 100644 index e7ecb5a3..00000000 --- a/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.cotto.lndmanagej.controller; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import de.cotto.lndmanagej.model.ChannelId; -import de.cotto.lndmanagej.model.Pubkey; - -public record ChannelDetailsDto( - @JsonSerialize(using = ToStringSerializer.class) ChannelId channelId, - @JsonSerialize(using = ToStringSerializer.class) Pubkey remotePubkey, - String remoteAlias, - @JsonProperty("private") boolean privateChannel -) { -} 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 13e0fd5a..f9a99144 100644 --- a/application/src/main/java/de/cotto/lndmanagej/controller/NodeController.java +++ b/application/src/main/java/de/cotto/lndmanagej/controller/NodeController.java @@ -1,36 +1,68 @@ package de.cotto.lndmanagej.controller; import com.codahale.metrics.MetricRegistry; +import de.cotto.lndmanagej.controller.dto.ChannelsForNodeDto; +import de.cotto.lndmanagej.controller.dto.NodeDetailsDto; +import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration; import de.cotto.lndmanagej.metrics.Metrics; +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 de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; +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; +import java.util.List; +import java.util.stream.Collectors; + @RestController @RequestMapping("/api/node/{pubkey}") +@Import(ObjectMapperConfiguration.class) public class NodeController { private final NodeService nodeService; private final Metrics metrics; + private final ChannelService channelService; - public NodeController(NodeService nodeService, Metrics metrics) { + public NodeController(NodeService nodeService, ChannelService channelService, Metrics metrics) { this.nodeService = nodeService; this.metrics = metrics; + this.channelService = channelService; } @GetMapping("/alias") public String getAlias(Pubkey pubkey) { - metrics.mark(MetricRegistry.name(getClass(), "getAlias")); + mark("getAlias"); return nodeService.getAlias(pubkey); } @GetMapping("/details") public NodeDetailsDto getDetails(@PathVariable Pubkey pubkey) { - metrics.mark(MetricRegistry.name(getClass(), "getDetails")); + mark("getDetails"); Node node = nodeService.getNode(pubkey); - return new NodeDetailsDto(pubkey, node.alias(), node.online()); + return new NodeDetailsDto(pubkey, node.alias(), getChannelIdsForPubkey(pubkey), node.online()); } + + @GetMapping("/open-channels") + public ChannelsForNodeDto getOpenChannelIdsForPubkey(@PathVariable Pubkey pubkey) { + mark("getOpenChannelIdsForPubkey"); + List channels = getChannelIdsForPubkey(pubkey); + return new ChannelsForNodeDto(pubkey, channels); + } + + private List getChannelIdsForPubkey(Pubkey pubkey) { + return channelService.getOpenChannelsWith(pubkey).stream() + .map(Channel::getId) + .sorted() + .collect(Collectors.toList()); + } + + private void mark(String getDetails) { + metrics.mark(MetricRegistry.name(getClass(), getDetails)); + } + } diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java new file mode 100644 index 00000000..6a572c6c --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java @@ -0,0 +1,13 @@ +package de.cotto.lndmanagej.controller.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Pubkey; + +public record ChannelDetailsDto( + ChannelId channelId, + Pubkey remotePubkey, + String remoteAlias, + @JsonProperty("private") boolean privateChannel +) { +} diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelsForNodeDto.java b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelsForNodeDto.java new file mode 100644 index 00000000..dac12ad0 --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelsForNodeDto.java @@ -0,0 +1,12 @@ +package de.cotto.lndmanagej.controller.dto; + +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Pubkey; + +import java.util.List; + +public record ChannelsForNodeDto( + Pubkey node, + List channels +) { +} diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/NodeDetailsDto.java b/application/src/main/java/de/cotto/lndmanagej/controller/dto/NodeDetailsDto.java similarity index 67% rename from application/src/main/java/de/cotto/lndmanagej/controller/NodeDetailsDto.java rename to application/src/main/java/de/cotto/lndmanagej/controller/dto/NodeDetailsDto.java index b4a3cb68..d1970db8 100644 --- a/application/src/main/java/de/cotto/lndmanagej/controller/NodeDetailsDto.java +++ b/application/src/main/java/de/cotto/lndmanagej/controller/dto/NodeDetailsDto.java @@ -1,12 +1,16 @@ -package de.cotto.lndmanagej.controller; +package de.cotto.lndmanagej.controller.dto; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.Pubkey; +import java.util.List; + public record NodeDetailsDto( - @JsonSerialize(using = ToStringSerializer.class) Pubkey pubkey, + @JsonSerialize(using = ToStringSerializer.class) Pubkey node, String alias, + List channels, boolean online ) { } diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/dto/ObjectMapperConfiguration.java b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ObjectMapperConfiguration.java new file mode 100644 index 00000000..424cc886 --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ObjectMapperConfiguration.java @@ -0,0 +1,26 @@ +package de.cotto.lndmanagej.controller.dto; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Pubkey; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class ObjectMapperConfiguration { + public ObjectMapperConfiguration() { + // default constructor + } + + @Bean + @Primary + public ObjectMapper objectMapper() { + SimpleModule module = new SimpleModule("SimpleModule"); + module.addSerializer(Pubkey.class, new ToStringSerializer()); + module.addSerializer(ChannelId.class, new ToStringSerializer()); + return new ObjectMapper().registerModule(module); + } +} diff --git a/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java b/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java index 0e18e5e4..6a25555b 100644 --- a/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java @@ -1,5 +1,6 @@ package de.cotto.lndmanagej.controller; +import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; import de.cotto.lndmanagej.metrics.Metrics; import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; 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 d5eea610..ad016e9b 100644 --- a/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java @@ -1,7 +1,10 @@ package de.cotto.lndmanagej.controller; +import de.cotto.lndmanagej.controller.dto.ChannelsForNodeDto; +import de.cotto.lndmanagej.controller.dto.NodeDetailsDto; import de.cotto.lndmanagej.metrics.Metrics; import de.cotto.lndmanagej.model.Node; +import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -9,7 +12,17 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; +import java.util.Set; + +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.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_2; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_3; import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS_2; +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; import static org.mockito.ArgumentMatchers.argThat; @@ -27,6 +40,9 @@ class NodeControllerTest { @Mock private Metrics metrics; + @Mock + private ChannelService channelService; + @Test void getAlias() { when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); @@ -36,11 +52,40 @@ class NodeControllerTest { } @Test - void getNodeDetails() { - NodeDetailsDto expectedDetails = new NodeDetailsDto(PUBKEY_2, ALIAS_2, true); + void getNodeDetails_no_channels() { + NodeDetailsDto expectedDetails = new NodeDetailsDto(PUBKEY_2, ALIAS_2, List.of(), true); when(nodeService.getNode(PUBKEY_2)).thenReturn(new Node(PUBKEY_2, ALIAS_2, 0, true)); assertThat(nodeController.getDetails(PUBKEY_2)).isEqualTo(expectedDetails); verify(metrics).mark(argThat(name -> name.endsWith(".getDetails"))); } + + @Test + void getNodeDetails_with_channels() { + when(nodeService.getNode(PUBKEY_2)).thenReturn(new Node(PUBKEY_2, ALIAS_2, 0, false)); + when(channelService.getOpenChannelsWith(PUBKEY_2)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_3)); + NodeDetailsDto expectedDetails = new NodeDetailsDto( + PUBKEY_2, + ALIAS_2, + List.of(CHANNEL_ID, CHANNEL_ID_3), + false + ); + + assertThat(nodeController.getDetails(PUBKEY_2)).isEqualTo(expectedDetails); + } + + @Test + void getOpenChannelIds_for_peer() { + when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_3)); + assertThat(nodeController.getOpenChannelIdsForPubkey(PUBKEY)) + .isEqualTo(new ChannelsForNodeDto(PUBKEY, List.of(CHANNEL_ID, CHANNEL_ID_3))); + verify(metrics).mark(argThat(name -> name.endsWith(".getOpenChannelIdsForPubkey"))); + } + + @Test + void getOpenChannelIds_for_peer_ordered() { + when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL_2, LOCAL_OPEN_CHANNEL)); + assertThat(nodeController.getOpenChannelIdsForPubkey(PUBKEY)) + .isEqualTo(new ChannelsForNodeDto(PUBKEY, List.of(CHANNEL_ID, CHANNEL_ID_2))); + } } \ No newline at end of file