diff --git a/mantis-control-plane/mantis-control-plane-server/src/main/java/io/mantisrx/master/api/akka/route/v1/ResourceClustersNonLeaderRedirectRoute.java b/mantis-control-plane/mantis-control-plane-server/src/main/java/io/mantisrx/master/api/akka/route/v1/ResourceClustersNonLeaderRedirectRoute.java index 7cc50b625..1466d86a8 100644 --- a/mantis-control-plane/mantis-control-plane-server/src/main/java/io/mantisrx/master/api/akka/route/v1/ResourceClustersNonLeaderRedirectRoute.java +++ b/mantis-control-plane/mantis-control-plane-server/src/main/java/io/mantisrx/master/api/akka/route/v1/ResourceClustersNonLeaderRedirectRoute.java @@ -292,13 +292,42 @@ private Route listClusters() { return withFuture(gateway.listActiveClusters()); } - private Route getActiveJobOverview(ClusterID clusterID, Optional startingIndex, + private Route getActiveJobOverview( + ClusterID clusterID, + Optional startingIndex, Optional pageSize) { - CompletableFuture jobsOverview = - gateway.getClusterFor(clusterID).getActiveJobOverview( - startingIndex.map(Integer::parseInt), - pageSize.map(Integer::parseInt)); - return withFuture(jobsOverview); + + Optional parsedStartingIndex = + parseNonNegativeInteger(startingIndex); + Optional parsedPageSize = + parseNonNegativeInteger(pageSize); + + return validate( + () -> startingIndex.isEmpty() || parsedStartingIndex.isPresent(), + "startingIndex must be a non-negative integer", + () -> validate( + () -> pageSize.isEmpty() || parsedPageSize.isPresent(), + "pageSize must be a non-negative integer", + () -> withFuture( + gateway.getClusterFor(clusterID) + .getActiveJobOverview( + parsedStartingIndex, + parsedPageSize)))); + } + + private Optional parseNonNegativeInteger( + Optional parameterValue) { + + return parameterValue.flatMap(value -> { + try { + int parsedValue = Integer.parseInt(value); + return parsedValue >= 0 + ? Optional.of(parsedValue) + : Optional.empty(); + } catch (NumberFormatException exception) { + return Optional.empty(); + } + }); } private Route getResourceOverview(ClusterID clusterID) { diff --git a/mantis-control-plane/mantis-control-plane-server/src/test/java/io/mantisrx/master/api/akka/route/v1/ResourceClusterNonLeaderRedirectRouteTest.java b/mantis-control-plane/mantis-control-plane-server/src/test/java/io/mantisrx/master/api/akka/route/v1/ResourceClusterNonLeaderRedirectRouteTest.java index 892221bfa..578b623d6 100644 --- a/mantis-control-plane/mantis-control-plane-server/src/test/java/io/mantisrx/master/api/akka/route/v1/ResourceClusterNonLeaderRedirectRouteTest.java +++ b/mantis-control-plane/mantis-control-plane-server/src/test/java/io/mantisrx/master/api/akka/route/v1/ResourceClusterNonLeaderRedirectRouteTest.java @@ -82,6 +82,10 @@ import org.junit.BeforeClass; import org.junit.Test; import org.mockito.ArgumentMatchers; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verifyNoInteractions; + +import akka.http.javadsl.testkit.TestRouteResult; public class ResourceClusterNonLeaderRedirectRouteTest extends JUnitRouteTest { private static final UnitTestResourceProviderAdapter resourceProviderAdapter = @@ -152,6 +156,7 @@ public void testGetActiveJobOverview() { PagedActiveJobOverview overview1 = new PagedActiveJobOverview(ImmutableList.of(), 0); PagedActiveJobOverview overview2 = new PagedActiveJobOverview(ImmutableList.of("test"), 1); PagedActiveJobOverview overview3 = new PagedActiveJobOverview(ImmutableList.of("test"), 99); + PagedActiveJobOverview zeroPageSizeOverview = new PagedActiveJobOverview(ImmutableList.of("test"), 1); ResourceCluster resourceCluster = mock(ResourceCluster.class); when(resourceCluster.getActiveJobOverview(Optional.empty(), Optional.empty())) @@ -161,6 +166,10 @@ public void testGetActiveJobOverview() { when(resourceCluster.getActiveJobOverview(Optional.empty(), Optional.of(99))) .thenReturn(CompletableFuture.completedFuture(overview3)); when(resourceClusters.getClusterFor(ClusterID.of("myCluster"))).thenReturn(resourceCluster); + when(resourceCluster.getActiveJobOverview( + Optional.of(0), + Optional.of(0))) + .thenReturn(CompletableFuture.completedFuture(zeroPageSizeOverview)); testRouteWithNoopAdapter.run(HttpRequest.GET( "/api/v1/resourceClusters/myCluster/activeJobOverview")) @@ -176,6 +185,24 @@ public void testGetActiveJobOverview() { "/api/v1/resourceClusters/myCluster/activeJobOverview?pageSize=99")) .assertStatusCode(200) .assertEntityAs(Jackson.unmarshaller(PagedActiveJobOverview.class), overview3); + + testRouteWithNoopAdapter.run(HttpRequest.GET( + "/api/v1/resourceClusters/myCluster/activeJobOverview" + + "?startingIndex=0&pageSize=0")) + .assertStatusCode(StatusCodes.OK) + .assertEntityAs( + Jackson.unmarshaller(PagedActiveJobOverview.class), + zeroPageSizeOverview); + } + + @Test + public void testGetActiveJobOverviewRejectsNegativeStartingIndex() { + testRouteWithNoopAdapter.run(HttpRequest.GET( + "/api/v1/resourceClusters/myCluster/activeJobOverview" + + "?startingIndex=-1&pageSize=9")) + .assertStatusCode(StatusCodes.BAD_REQUEST); + + verifyNoInteractions(resourceClusters); } @Test @@ -402,6 +429,67 @@ public void testResourceClusterUpgradeRoutes() throws IOException { .build()); } + @Test + public void testGetActiveJobOverviewRejectsInvalidStartingIndex() { + assertInvalidActiveJobOverviewQuery( + "startingIndex=abc", + "startingIndex must be a non-negative integer"); + + assertInvalidActiveJobOverviewQuery( + "startingIndex=-1", + "startingIndex must be a non-negative integer"); + + assertInvalidActiveJobOverviewQuery( + "startingIndex=2147483648", + "startingIndex must be a non-negative integer"); + + assertInvalidActiveJobOverviewQuery( + "startingIndex=", + "startingIndex must be a non-negative integer"); + + verifyNoInteractions(resourceClusters); + } + + @Test + public void testGetActiveJobOverviewRejectsInvalidPageSize() { + assertInvalidActiveJobOverviewQuery( + "pageSize=abc", + "pageSize must be a non-negative integer"); + + assertInvalidActiveJobOverviewQuery( + "pageSize=-1", + "pageSize must be a non-negative integer"); + + assertInvalidActiveJobOverviewQuery( + "pageSize=2147483648", + "pageSize must be a non-negative integer"); + + assertInvalidActiveJobOverviewQuery( + "pageSize=", + "pageSize must be a non-negative integer"); + + verifyNoInteractions(resourceClusters); + } + + private void assertInvalidActiveJobOverviewQuery( + String query, + String expectedErrorMessage) { + + TestRouteResult result = testRouteWithNoopAdapter.run( + HttpRequest.GET( + "/api/v1/resourceClusters/myCluster/activeJobOverview?" + + query)); + + result.assertStatusCode(StatusCodes.BAD_REQUEST); + + String responseBody = result.entityString(); + + assertTrue( + responseBody, + responseBody.contains( + "\"error\":\"" + expectedErrorMessage + "\"")); + } + final String getResourceClusterEndpoint() { return "/api/v1/resourceClusters"; }