diff --git a/application/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java b/application/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java new file mode 100644 index 00000000..bf86bdb1 --- /dev/null +++ b/application/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java @@ -0,0 +1,46 @@ +package de.cotto.lndmanagej.controller; + +import de.cotto.lndmanagej.metrics.Metrics; +import de.cotto.lndmanagej.service.ChannelService; +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.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; +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 = ChannelDetailsController.class) +class ChannelDetailsControllerIT { + private static final String CHANNEL_PREFIX = "/api/channel/" + CHANNEL_ID.getShortChannelId(); + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ChannelService channelService; + + @MockBean + @SuppressWarnings("unused") + private Metrics metrics; + + @Test + void not_found() throws Exception { + mockMvc.perform(get(CHANNEL_PREFIX + "/details")) + .andExpect(status().isNotFound()); + } + + @Test + void details() throws Exception { + when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); + mockMvc.perform(get(CHANNEL_PREFIX + "/details")) + .andExpect(content().json("{\"channelId\":\"" + CHANNEL_ID.getShortChannelId() + "\"}")); + } +} \ 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 new file mode 100644 index 00000000..19c219c6 --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java @@ -0,0 +1,33 @@ +package de.cotto.lndmanagej.controller; + +import com.codahale.metrics.MetricRegistry; +import de.cotto.lndmanagej.metrics.Metrics; +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.LocalChannel; +import de.cotto.lndmanagej.service.ChannelService; +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/channel/{channelId}") +public class ChannelDetailsController { + private final ChannelService channelService; + private final Metrics metrics; + + public ChannelDetailsController(ChannelService channelService, Metrics metrics) { + this.channelService = channelService; + this.metrics = metrics; + } + + @GetMapping("/details") + public ChannelDetailsDto getChannelDetails(@PathVariable ChannelId channelId) throws NotFoundException { + metrics.mark(MetricRegistry.name(getClass(), "getChannelDetails")); + LocalChannel localChannel = channelService.getLocalChannel(channelId).orElse(null); + if (localChannel == null) { + throw new NotFoundException(); + } + return new ChannelDetailsDto(localChannel.getId()); + } +} diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsDto.java b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsDto.java new file mode 100644 index 00000000..596b6269 --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsDto.java @@ -0,0 +1,8 @@ +package de.cotto.lndmanagej.controller; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import de.cotto.lndmanagej.model.ChannelId; + +public record ChannelDetailsDto(@JsonSerialize(using = ToStringSerializer.class) ChannelId channelId) { +} diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/NotFoundException.java b/application/src/main/java/de/cotto/lndmanagej/controller/NotFoundException.java new file mode 100644 index 00000000..18fad1c9 --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/controller/NotFoundException.java @@ -0,0 +1,7 @@ +package de.cotto.lndmanagej.controller; + +public class NotFoundException extends Exception { + public NotFoundException() { + super(); + } +} diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/NotFoundExceptionHandler.java b/application/src/main/java/de/cotto/lndmanagej/controller/NotFoundExceptionHandler.java new file mode 100644 index 00000000..3298ef4d --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/controller/NotFoundExceptionHandler.java @@ -0,0 +1,20 @@ +package de.cotto.lndmanagej.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class NotFoundExceptionHandler extends ResponseEntityExceptionHandler { + public NotFoundExceptionHandler() { + super(); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleException(@SuppressWarnings("unused") NotFoundException exception) { + return ResponseEntity + .notFound() + .build(); + } +} \ No newline at end of file diff --git a/application/src/main/java/de/cotto/lndmanagej/service/ChannelService.java b/application/src/main/java/de/cotto/lndmanagej/service/ChannelService.java index c43de9ac..e0d9fa70 100644 --- a/application/src/main/java/de/cotto/lndmanagej/service/ChannelService.java +++ b/application/src/main/java/de/cotto/lndmanagej/service/ChannelService.java @@ -23,12 +23,14 @@ import java.util.stream.Stream; public class ChannelService { private static final int CACHE_EXPIRY_MINUTES = 1; + private final GrpcChannels grpcChannels; private final LoadingCache> channelsCache; private final LoadingCache> closedChannelsCache; private final LoadingCache> forceClosingChannelsCache; private final LoadingCache> waitingCloseChannelsCache; public ChannelService(GrpcChannels grpcChannels, GrpcClosedChannels grpcClosedChannels) { + this.grpcChannels = grpcChannels; channelsCache = new CacheBuilder() .withExpiryMinutes(CACHE_EXPIRY_MINUTES) .build(grpcChannels::getChannels); @@ -44,13 +46,23 @@ public class ChannelService { } public boolean isClosed(ChannelId channelId) { - return getClosedChannels().stream().anyMatch(c -> c.getId().equals(channelId)); + return getClosedChannel(channelId).isPresent(); + } + + public Optional getLocalChannel(ChannelId channelId) { + return getAllLocalChannels() + .filter(c -> channelId.equals(c.getId())) + .findFirst(); } public Set getOpenChannels() { return channelsCache.getUnchecked(""); } + public Optional getOpenChannel(ChannelId channelId) { + return grpcChannels.getChannel(channelId); + } + public Set getClosedChannels() { return closedChannelsCache.getUnchecked(""); } @@ -65,6 +77,12 @@ public class ChannelService { return forceClosingChannelsCache.getUnchecked(""); } + public Optional getForceClosingChannel(ChannelId channelId) { + return getForceClosingChannels().stream() + .filter(c -> channelId.equals(c.getId())) + .findFirst(); + } + public Set getWaitingCloseChannels() { return waitingCloseChannelsCache.getUnchecked(""); } @@ -75,18 +93,12 @@ public class ChannelService { .collect(Collectors.toSet()); } - public Set getAllChannelsWith(Pubkey pubkey) { + public Set getAllChannelsWith(Pubkey peer) { return getAllLocalChannels() - .filter(c -> c.getRemotePubkey().equals(pubkey)) + .filter(c -> peer.equals(c.getRemotePubkey())) .collect(Collectors.toSet()); } - public Optional getLocalChannel(ChannelId channelId) { - return getAllLocalChannels() - .filter(c -> c.getId().equals(channelId)) - .findFirst(); - } - public Stream getAllLocalChannels() { Set openChannels = getOpenChannels(); Set waitingCloseChannels = getWaitingCloseChannels(); diff --git a/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java b/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java new file mode 100644 index 00000000..91df7129 --- /dev/null +++ b/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java @@ -0,0 +1,45 @@ +package de.cotto.lndmanagej.controller; + +import de.cotto.lndmanagej.metrics.Metrics; +import de.cotto.lndmanagej.service.ChannelService; +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.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ChannelDetailsControllerTest { + @InjectMocks + private ChannelDetailsController channelDetailsController; + + @Mock + private ChannelService channelService; + + @Mock + private Metrics metrics; + + @Test + void getChannelDetails_channel_not_found() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> channelDetailsController.getChannelDetails(CHANNEL_ID)); + } + + @Test + void getChannelDetails() throws NotFoundException { + when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); + assertThat(channelDetailsController.getChannelDetails(CHANNEL_ID)) + .isEqualTo(new ChannelDetailsDto(CHANNEL_ID)); + verify(metrics).mark(argThat(name -> name.endsWith(".getChannelDetails"))); + } +} \ No newline at end of file diff --git a/application/src/test/java/de/cotto/lndmanagej/controller/NotFoundExceptionHandlerTest.java b/application/src/test/java/de/cotto/lndmanagej/controller/NotFoundExceptionHandlerTest.java new file mode 100644 index 00000000..62a9d528 --- /dev/null +++ b/application/src/test/java/de/cotto/lndmanagej/controller/NotFoundExceptionHandlerTest.java @@ -0,0 +1,23 @@ +package de.cotto.lndmanagej.controller; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class NotFoundExceptionHandlerTest { + private static final NotFoundException EXCEPTION = new NotFoundException(); + + @InjectMocks + private NotFoundExceptionHandler exceptionHandler; + + @Test + void mapsToNotFound() { + assertThat(exceptionHandler.handleException(EXCEPTION).getStatusCode()) + .isEqualTo(HttpStatus.NOT_FOUND); + } +} \ No newline at end of file diff --git a/application/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java b/application/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java index 9a47e52b..12f35499 100644 --- a/application/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java @@ -8,6 +8,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Optional; import java.util.Set; import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; @@ -16,8 +17,6 @@ import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_2; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_3; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_TO_NODE_3; -import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL; -import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL_2; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL_2; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL_TO_NODE_3; @@ -55,6 +54,46 @@ class ChannelServiceTest { assertThat(channelService.isClosed(CHANNEL_ID)).isTrue(); } + @Test + void getLocalChannel_unknown() { + assertThat(channelService.getLocalChannel(CHANNEL_ID)).isEmpty(); + } + + @Test + void getLocalChannel_open() { + when(grpcChannels.getChannels()).thenReturn(Set.of(LOCAL_OPEN_CHANNEL_2, LOCAL_OPEN_CHANNEL)); + assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(LOCAL_OPEN_CHANNEL); + } + + @Test + void getLocalChannel_waiting_close_channel() { + when(grpcChannels.getWaitingCloseChannels()).thenReturn(Set.of(WAITING_CLOSE_CHANNEL, WAITING_CLOSE_CHANNEL_2)); + assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(WAITING_CLOSE_CHANNEL); + } + + @Test + void getLocalChannel_force_closing_channel() { + when(grpcChannels.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL, FORCE_CLOSING_CHANNEL_2)); + assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(FORCE_CLOSING_CHANNEL); + } + + @Test + void getLocalChannel_closed() { + when(grpcClosedChannels.getClosedChannels()).thenReturn(Set.of(CLOSED_CHANNEL)); + assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(CLOSED_CHANNEL); + } + + @Test + void getOpenChannel() { + when(grpcChannels.getChannel(CHANNEL_ID_2)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); + assertThat(channelService.getOpenChannel(CHANNEL_ID_2)).contains(LOCAL_OPEN_CHANNEL); + } + + @Test + void getOpenChannel_not_open() { + assertThat(channelService.getOpenChannel(CHANNEL_ID_2)).isEmpty(); + } + @Test void getOpenChannelsWith_by_pubkey() { when(grpcChannels.getChannels()).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_3)); @@ -104,6 +143,20 @@ class ChannelServiceTest { .containsExactlyInAnyOrder(FORCE_CLOSING_CHANNEL, FORCE_CLOSING_CHANNEL_2); } + @Test + void getForceClosingChannel() { + when(grpcChannels.getForceClosingChannels()) + .thenReturn(Set.of(FORCE_CLOSING_CHANNEL, FORCE_CLOSING_CHANNEL_2)); + assertThat(channelService.getForceClosingChannel(CHANNEL_ID_2)).contains(FORCE_CLOSING_CHANNEL_2); + } + + @Test + void getForceClosingChannel_not_found() { + when(grpcChannels.getForceClosingChannels()) + .thenReturn(Set.of(FORCE_CLOSING_CHANNEL)); + assertThat(channelService.getForceClosingChannel(CHANNEL_ID_2)).isEmpty(); + } + @Test void getWaitingCloseChannels() { when(grpcChannels.getWaitingCloseChannels()) @@ -129,39 +182,4 @@ class ChannelServiceTest { WAITING_CLOSE_CHANNEL ); } - - @Test - void getLocalChannel_unknown() { - assertThat(channelService.getLocalChannel(CHANNEL_ID)).isEmpty(); - } - - @Test - void getLocalChannel_local_open_channel() { - when(grpcChannels.getChannels()).thenReturn(Set.of(LOCAL_OPEN_CHANNEL_2, LOCAL_OPEN_CHANNEL)); - assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(LOCAL_OPEN_CHANNEL); - } - - @Test - void getLocalChannel_waiting_close_channel() { - when(grpcChannels.getWaitingCloseChannels()).thenReturn(Set.of(WAITING_CLOSE_CHANNEL, WAITING_CLOSE_CHANNEL_2)); - assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(WAITING_CLOSE_CHANNEL); - } - - @Test - void getLocalChannel_coop_closed_channel() { - when(grpcClosedChannels.getClosedChannels()).thenReturn(Set.of(CLOSED_CHANNEL, CLOSED_CHANNEL_2)); - assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(CLOSED_CHANNEL); - } - - @Test - void getLocalChannel_force_closing_channel() { - when(grpcChannels.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL, FORCE_CLOSING_CHANNEL_2)); - assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(FORCE_CLOSING_CHANNEL); - } - - @Test - void getLocalChannel_force_closed_channel() { - when(grpcClosedChannels.getClosedChannels()).thenReturn(Set.of(FORCE_CLOSED_CHANNEL, FORCE_CLOSED_CHANNEL_2)); - assertThat(channelService.getLocalChannel(CHANNEL_ID)).contains(FORCE_CLOSED_CHANNEL); - } } \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java index 1c5809da..326b0868 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java @@ -11,6 +11,4 @@ public class ChannelIdFixtures { public static final ChannelId CHANNEL_ID_4 = ChannelId.fromCompactForm(CHANNEL_ID_COMPACT_4); public static final long CHANNEL_ID_SHORT = CHANNEL_ID.getShortChannelId(); public static final long CHANNEL_ID_2_SHORT = CHANNEL_ID_2.getShortChannelId(); - public static final long CHANNEL_ID_3_SHORT = CHANNEL_ID_3.getShortChannelId(); - public static final long CHANNEL_ID_4_SHORT = CHANNEL_ID_4.getShortChannelId(); }