From 3352d2774f63779c7d8f9ec990fd729b0c76cbb3 Mon Sep 17 00:00:00 2001 From: Atif Ali <56743004+aali309@users.noreply.github.com> Date: Tue, 27 Jun 2023 13:12:40 -0400 Subject: [PATCH] feat(GraphQL): Add GraphQL filter ability for node name from set (#1518) --- .../api/v2/graph/ActiveRecordingsFetcher.java | 8 + .../v2/graph/ArchivedRecordingsFetcher.java | 8 + .../api/v2/graph/EnvironmentNodesFetcher.java | 5 + .../web/http/api/v2/graph/FilterInput.java | 1 + .../http/api/v2/graph/TargetNodesFetcher.java | 7 + src/main/resources/types.graphqls | 5 + .../v2/graph/ActiveRecordingsFetcherTest.java | 32 ++ .../graph/ArchivedRecordingsFetcherTest.java | 33 ++ .../v2/graph/EnvironmentNodesFetcherTest.java | 34 ++ .../api/v2/graph/TargetNodesFetcherTest.java | 12 +- src/test/java/itest/GraphQLIT.java | 431 ++++++++++++++++++ 11 files changed, 572 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcher.java b/src/main/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcher.java index 1fcefce100..b29673a689 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcher.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcher.java @@ -100,6 +100,14 @@ public Active getAuthenticated(DataFetchingEnvironment environment) throws Excep .filter(r -> Objects.equals(r.getName(), recordingName)) .collect(Collectors.toList()); } + if (filter.contains(FilterInput.Key.NAMES)) { + List recordingNames = filter.get(FilterInput.Key.NAMES); + recordings = + recordings.stream() + .filter(r -> recordingNames.contains(r.getName())) + .collect(Collectors.toList()); + } + if (filter.contains(FilterInput.Key.LABELS)) { List labels = filter.get(FilterInput.Key.LABELS); for (String label : labels) { diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcher.java b/src/main/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcher.java index f8ef1a42ec..d0b1d1cd85 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcher.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcher.java @@ -99,6 +99,14 @@ public Archived getAuthenticated(DataFetchingEnvironment environment) throws Exc .filter(r -> Objects.equals(r.getName(), recordingName)) .collect(Collectors.toList()); } + if (filter.contains(FilterInput.Key.NAMES)) { + List names = filter.get(FilterInput.Key.NAMES); + recordings = + recordings.stream() + .filter(r -> names.contains(r.getName())) + .collect(Collectors.toList()); + } + if (filter.contains(FilterInput.Key.LABELS)) { List labels = filter.get(FilterInput.Key.LABELS); for (String label : labels) { diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcher.java b/src/main/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcher.java index 1cbca95301..04ae3cc46e 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcher.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcher.java @@ -103,6 +103,11 @@ public List getAuthenticated(DataFetchingEnvironment environmen nodes = filter(nodes, n -> Objects.equals(n.getName(), nodeName)); } + if (filter.contains(FilterInput.Key.NAMES)) { + List names = filter.get(FilterInput.Key.NAMES); + nodes = filter(nodes, n -> names.contains(n.getName())); + } + if (filter.contains(FilterInput.Key.LABELS)) { List labels = filter.get(FilterInput.Key.LABELS); for (String label : labels) { diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/graph/FilterInput.java b/src/main/java/io/cryostat/net/web/http/api/v2/graph/FilterInput.java index ee783a7cbd..a845453df0 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/graph/FilterInput.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/graph/FilterInput.java @@ -67,6 +67,7 @@ T get(Key key) { enum Key { ID("id"), NAME("name"), + NAMES("names"), LABELS("labels"), ANNOTATIONS("annotations"), SOURCE_TARGET("sourceTarget"), diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcher.java b/src/main/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcher.java index 2045688600..7c6973d739 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcher.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcher.java @@ -110,6 +110,13 @@ public List getAuthenticated(DataFetchingEnvironment environment) th .filter(n -> Objects.equals(n.getName(), nodeName)) .collect(Collectors.toList()); } + if (filter.contains(FilterInput.Key.NAMES)) { + List names = filter.get(FilterInput.Key.NAMES); + result = + result.stream() + .filter(n -> names.contains(n.getName())) + .collect(Collectors.toList()); + } if (filter.contains(FilterInput.Key.LABELS)) { List labels = filter.get(FilterInput.Key.LABELS); for (String label : labels) { diff --git a/src/main/resources/types.graphqls b/src/main/resources/types.graphqls index e45417208a..de8677eabf 100644 --- a/src/main/resources/types.graphqls +++ b/src/main/resources/types.graphqls @@ -10,6 +10,7 @@ scalar Float input EnvironmentNodeFilterInput { id: Int name: String + names: [String] nodeType: String labels: [String] } @@ -17,6 +18,7 @@ input EnvironmentNodeFilterInput { input TargetNodesFilterInput { id: Int name: String + names: [String] labels: [String] annotations: [String] } @@ -24,12 +26,14 @@ input TargetNodesFilterInput { input DescendantTargetsFilterInput { id: Int name: String + names: [String] labels: [String] annotations: [String] } input ActiveRecordingFilterInput { name: String + names: [String] state: String continuous: Boolean toDisk: Boolean @@ -42,6 +46,7 @@ input ActiveRecordingFilterInput { input ArchivedRecordingFilterInput { name: String + names: [String] labels: [String] sourceTarget: String sizeBytesGreaterThanEqual: Long diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcherTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcherTest.java index e731b4ea89..f9c062a514 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcherTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/graph/ActiveRecordingsFetcherTest.java @@ -284,4 +284,36 @@ void shouldReturnRecordingsMultipleFilters() throws Exception { MatcherAssert.assertThat(active.aggregate.count, Matchers.equalTo(1L)); } } + + @Test + void shouldReturnRecordingsFilteredByNames() throws Exception { + try (MockedStatic staticFilter = Mockito.mockStatic(FilterInput.class)) { + staticFilter.when(() -> FilterInput.from(env)).thenReturn(filter); + when(env.getGraphQlContext()).thenReturn(graphCtx); + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) + .thenReturn(CompletableFuture.completedFuture(true)); + + GraphRecordingDescriptor recording1 = Mockito.mock(GraphRecordingDescriptor.class); + GraphRecordingDescriptor recording2 = Mockito.mock(GraphRecordingDescriptor.class); + GraphRecordingDescriptor recording3 = Mockito.mock(GraphRecordingDescriptor.class); + when(recording1.getName()).thenReturn("Recording1"); + when(recording2.getName()).thenReturn("Recording2"); + when(recording3.getName()).thenReturn("Recording3"); + + when(filter.contains(Mockito.any())).thenReturn(false); + when(filter.contains(FilterInput.Key.NAMES)).thenReturn(true); + when(filter.get(FilterInput.Key.NAMES)).thenReturn(List.of("Recording1", "Recording3")); + + Recordings source = Mockito.mock(Recordings.class); + source.active = List.of(recording1, recording2, recording3); + + when(env.getSource()).thenReturn(source); + + Active active = fetcher.get(env); + + MatcherAssert.assertThat(active, Matchers.notNullValue()); + MatcherAssert.assertThat(active.data, Matchers.contains(recording1, recording3)); + MatcherAssert.assertThat(active.aggregate.count, Matchers.equalTo(2L)); + } + } } diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcherTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcherTest.java index 518c8ca120..f084a86751 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcherTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/graph/ArchivedRecordingsFetcherTest.java @@ -245,4 +245,37 @@ void shouldReturnRecordingsMultipleFilters() throws Exception { Matchers.equalTo(recording3.getSize() + recording5.getSize())); } } + + @Test + void shouldReturnArchivedRecordingsFilteredByNames() throws Exception { + try (MockedStatic staticFilter = Mockito.mockStatic(FilterInput.class)) { + staticFilter.when(() -> FilterInput.from(env)).thenReturn(filter); + when(env.getGraphQlContext()).thenReturn(graphCtx); + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) + .thenReturn(CompletableFuture.completedFuture(true)); + + ArchivedRecordingInfo recording1 = Mockito.mock(ArchivedRecordingInfo.class); + ArchivedRecordingInfo recording2 = Mockito.mock(ArchivedRecordingInfo.class); + ArchivedRecordingInfo recording3 = Mockito.mock(ArchivedRecordingInfo.class); + when(recording1.getName()).thenReturn("foo"); + when(recording2.getName()).thenReturn("bar"); + when(recording3.getName()).thenReturn("baz"); + + when(filter.contains(Mockito.any())).thenReturn(false); + when(filter.contains(FilterInput.Key.NAMES)).thenReturn(true); + when(filter.get(FilterInput.Key.NAMES)).thenReturn(List.of("foo", "baz")); + + Recordings source = Mockito.mock(Recordings.class); + source.archived = List.of(recording1, recording2, recording3); + + when(env.getSource()).thenReturn(source); + + Archived archived = fetcher.get(env); + + MatcherAssert.assertThat(archived, Matchers.notNullValue()); + MatcherAssert.assertThat( + archived.data, Matchers.containsInAnyOrder(recording1, recording3)); + MatcherAssert.assertThat(archived.aggregate.count, Matchers.equalTo(2L)); + } + } } diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcherTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcherTest.java index 81e36f4da0..66ed324358 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcherTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/graph/EnvironmentNodesFetcherTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; @@ -175,4 +176,37 @@ void shouldReturnFilteredEnvironmentNodes() throws Exception { Matchers.containsInAnyOrder(leftChildNode, rightChildNode, leftGrandchildNode)); } } + + @Test + void shouldReturnFilteredEnvironmentNodesByNames() throws Exception { + try (MockedStatic staticFilter = Mockito.mockStatic(FilterInput.class)) { + staticFilter.when(() -> FilterInput.from(env)).thenReturn(filter); + when(env.getGraphQlContext()).thenReturn(graphCtx); + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) + .thenReturn(CompletableFuture.completedFuture(true)); + + EnvironmentNode northAmericaNode = + new EnvironmentNode("North America", BaseNodeType.REALM); + EnvironmentNode earthNode = new EnvironmentNode("Earth", BaseNodeType.REALM); + EnvironmentNode marsNode = new EnvironmentNode("Mars", BaseNodeType.REALM); + + EnvironmentNode universe = + new EnvironmentNode( + "Universe", + BaseNodeType.UNIVERSE, + Collections.emptyMap(), + Set.of(northAmericaNode, earthNode, marsNode)); + + when(filter.contains(Mockito.any())).thenReturn(false); + when(filter.contains(FilterInput.Key.NAMES)).thenReturn(true); + when(filter.get(FilterInput.Key.NAMES)).thenReturn(Arrays.asList("Earth", "Mars")); + + when(rootNodeFetcher.get(env)).thenReturn(universe); + + List nodes = fetcher.get(env); + + MatcherAssert.assertThat(nodes, Matchers.notNullValue()); + MatcherAssert.assertThat(nodes, Matchers.containsInAnyOrder(earthNode, marsNode)); + } + } } diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcherTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcherTest.java index c4dfa36875..2920fe1f29 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcherTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/graph/TargetNodesFetcherTest.java @@ -151,24 +151,28 @@ void shouldReturnTargetsFiltered() throws Exception { .thenReturn(CompletableFuture.completedFuture(true)); when(filter.contains(Mockito.any())).thenReturn(false); - when(filter.contains(FilterInput.Key.NAME)).thenReturn(true); - when(filter.get(FilterInput.Key.NAME)).thenReturn("foo"); + when(filter.contains(FilterInput.Key.NAMES)).thenReturn(true); + when(filter.get(FilterInput.Key.NAMES)).thenReturn(List.of("foo", "bar", "baz")); TargetNode target1 = Mockito.mock(TargetNode.class); TargetNode target2 = Mockito.mock(TargetNode.class); TargetNode target3 = Mockito.mock(TargetNode.class); + TargetNode target4 = Mockito.mock(TargetNode.class); + TargetNode target5 = Mockito.mock(TargetNode.class); when(target1.getName()).thenReturn("foo"); when(target2.getName()).thenReturn("foobar"); when(target3.getName()).thenReturn("foo"); + when(target4.getName()).thenReturn("bar"); + when(target5.getName()).thenReturn("baz"); when(recurseFetcher.get(Mockito.any())) - .thenReturn(List.of(target1, target2, target3)); + .thenReturn(List.of(target1, target2, target3, target4, target5)); List nodes = fetcher.get(env); MatcherAssert.assertThat(nodes, Matchers.notNullValue()); - MatcherAssert.assertThat(nodes, Matchers.containsInAnyOrder(target1, target3)); + MatcherAssert.assertThat(nodes, Matchers.hasItems(target1, target4, target5)); } } } diff --git a/src/test/java/itest/GraphQLIT.java b/src/test/java/itest/GraphQLIT.java index c4d074cdb6..c76e2c9311 100644 --- a/src/test/java/itest/GraphQLIT.java +++ b/src/test/java/itest/GraphQLIT.java @@ -37,7 +37,12 @@ */ package itest; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; + import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -53,8 +58,13 @@ import io.cryostat.MainModule; import io.cryostat.core.log.Logger; +import io.cryostat.net.web.http.HttpMimeType; import com.google.gson.Gson; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import itest.bases.ExternalTargetsTest; import itest.util.ITestCleanupFailedException; @@ -62,6 +72,7 @@ import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; @@ -78,6 +89,8 @@ class GraphQLIT extends ExternalTargetsTest { static final int NUM_EXT_CONTAINERS = 8; static final List CONTAINERS = new ArrayList<>(); + static final String TEST_RECORDING_NAME = "archivedRecording"; + @BeforeAll static void setup() throws Exception { Set specs = new HashSet<>(); @@ -588,6 +601,423 @@ void testNodesHaveIds() throws Exception { } } + @Test + @Order(9) + void testQueryForSpecificTargetsByNames() throws Exception { + CompletableFuture resp = new CompletableFuture<>(); + + String query = + String.format( + "query { targetNodes(filter: { names:" + + " [\"service:jmx:rmi:///jndi/rmi://cryostat-itests:9091/jmxrmi\"," + + " \"service:jmx:rmi:///jndi/rmi://cryostat-itests:9093/jmxrmi\"] }) {" + + " name nodeType } }"); + webClient + .post("/api/v2.2/graphql") + .sendJson( + new JsonObject().put("query", query), + ar -> { + if (assertRequestStatus(ar, resp)) { + resp.complete( + gson.fromJson( + ar.result().bodyAsString(), + TargetNodesQueryResponse.class)); + } + }); + TargetNodesQueryResponse actual = resp.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + List targetNodes = actual.data.targetNodes; + + int expectedSize = 2; + + assertThat(targetNodes.size(), is(expectedSize)); + + TargetNode target1 = new TargetNode(); + target1.name = "service:jmx:rmi:///jndi/rmi://cryostat-itests:9091/jmxrmi"; + target1.nodeType = "JVM"; + TargetNode target2 = new TargetNode(); + target2.name = "service:jmx:rmi:///jndi/rmi://cryostat-itests:9093/jmxrmi"; + target2.nodeType = "JVM"; + + assertThat(targetNodes, hasItem(target1)); + assertThat(targetNodes, hasItem(target2)); + } + + @Test + @Order(10) + public void testQueryForFilteredActiveRecordingsByNames() throws Exception { + // Check preconditions + CompletableFuture listRespFuture1 = new CompletableFuture<>(); + webClient + .get(String.format("/api/v1/targets/%s/recordings", SELF_REFERENCE_TARGET_ID)) + .send( + ar -> { + if (assertRequestStatus(ar, listRespFuture1)) { + listRespFuture1.complete(ar.result().bodyAsJsonArray()); + } + }); + JsonArray listResp = listRespFuture1.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + Assertions.assertTrue(listResp.isEmpty()); + + // Create two new recordings + CompletableFuture createRecordingFuture1 = new CompletableFuture<>(); + MultiMap form1 = MultiMap.caseInsensitiveMultiMap(); + form1.add("recordingName", "Recording1"); + form1.add("duration", "5"); + form1.add("events", "template=ALL"); + webClient + .post(String.format("/api/v1/targets/%s/recordings", SELF_REFERENCE_TARGET_ID)) + .sendForm( + form1, + ar -> { + if (assertRequestStatus(ar, createRecordingFuture1)) { + createRecordingFuture1.complete(null); + } + }); + createRecordingFuture1.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + CompletableFuture createRecordingFuture2 = new CompletableFuture<>(); + MultiMap form2 = MultiMap.caseInsensitiveMultiMap(); + form2.add("recordingName", "Recording2"); + form2.add("duration", "5"); + form2.add("events", "template=ALL"); + webClient + .post(String.format("/api/v1/targets/%s/recordings", SELF_REFERENCE_TARGET_ID)) + .sendForm( + form2, + ar -> { + if (assertRequestStatus(ar, createRecordingFuture2)) { + createRecordingFuture2.complete(null); + } + }); + createRecordingFuture2.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // GraphQL Query to filter Active recordings by names + CompletableFuture resp2 = new CompletableFuture<>(); + String query = + "query { targetNodes (filter: {name:" + + " \"service:jmx:rmi:///jndi/rmi://cryostat-itests:9091/jmxrmi\"}){ recordings" + + " {active(filter: { names: [\"Recording1\", \"Recording2\",\"Recording3\"] })" + + " {data {name}}}}}"; + webClient + .post("/api/v2.2/graphql") + .sendJson( + new JsonObject().put("query", query), + ar -> { + if (assertRequestStatus(ar, resp2)) { + resp2.complete( + gson.fromJson( + ar.result().bodyAsString(), + TargetNodesQueryResponse.class)); + } + }); + + TargetNodesQueryResponse graphqlResp = resp2.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + List filterNames = Arrays.asList("Recording1", "Recording2"); + + List filteredRecordings = + graphqlResp.data.targetNodes.stream() + .flatMap(targetNode -> targetNode.recordings.active.data.stream()) + .filter(recording -> filterNames.contains(recording.name)) + .collect(Collectors.toList()); + + MatcherAssert.assertThat(filteredRecordings.size(), Matchers.equalTo(2)); + ActiveRecording r1 = new ActiveRecording(); + r1.name = "Recording1"; + ActiveRecording r2 = new ActiveRecording(); + r2.name = "Recording2"; + + assertThat(filteredRecordings, hasItem(r1)); + assertThat(filteredRecordings, hasItem(r2)); + + // Delete recordings + for (ActiveRecording recording : filteredRecordings) { + String recordingName = recording.name; + CompletableFuture deleteRecordingFuture = new CompletableFuture<>(); + webClient + .delete( + String.format( + "/api/v1/targets/%s/recordings/%s", + SELF_REFERENCE_TARGET_ID, recordingName)) + .send( + ar -> { + if (assertRequestStatus(ar, deleteRecordingFuture)) { + deleteRecordingFuture.complete(null); + } + }); + deleteRecordingFuture.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + // Verify no recordings available + CompletableFuture listRespFuture4 = new CompletableFuture<>(); + webClient + .get(String.format("/api/v1/targets/%s/recordings", SELF_REFERENCE_TARGET_ID)) + .send( + ar -> { + if (assertRequestStatus(ar, listRespFuture4)) { + listRespFuture4.complete(ar.result().bodyAsJsonArray()); + } + }); + listResp = listRespFuture4.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + MatcherAssert.assertThat( + "list should have size 0 after deleting recordings", + listResp.size(), + Matchers.equalTo(0)); + } + + @Test + @Order(11) + public void shouldReturnArchivedRecordingsFilteredByNames() throws Exception { + // Check preconditions + CompletableFuture listRespFuture1 = new CompletableFuture<>(); + webClient + .get(String.format("/api/v1/targets/%s/recordings", SELF_REFERENCE_TARGET_ID)) + .send( + ar -> { + if (assertRequestStatus(ar, listRespFuture1)) { + listRespFuture1.complete(ar.result().bodyAsJsonArray()); + } + }); + JsonArray listResp = listRespFuture1.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + Assertions.assertTrue(listResp.isEmpty()); + + // Create a new recording + CompletableFuture createRecordingFuture = new CompletableFuture<>(); + MultiMap form = MultiMap.caseInsensitiveMultiMap(); + form.add("recordingName", TEST_RECORDING_NAME); + form.add("duration", "5"); + form.add("events", "template=ALL"); + webClient + .post(String.format("/api/v1/targets/%s/recordings", SELF_REFERENCE_TARGET_ID)) + .sendForm( + form, + ar -> { + if (assertRequestStatus(ar, createRecordingFuture)) { + createRecordingFuture.complete(null); + } + }); + createRecordingFuture.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // Archive the recording + CompletableFuture archiveRecordingFuture = new CompletableFuture<>(); + webClient + .patch( + String.format( + "/api/v1/targets/%s/recordings/%s", + SELF_REFERENCE_TARGET_ID, TEST_RECORDING_NAME)) + .putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpMimeType.PLAINTEXT.mime()) + .sendBuffer( + Buffer.buffer("SAVE"), + ar -> { + if (assertRequestStatus(ar, archiveRecordingFuture)) { + archiveRecordingFuture.complete(null); + } else { + + archiveRecordingFuture.completeExceptionally( + new RuntimeException("Archive request failed")); + } + }); + + archiveRecordingFuture.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // retrieve to match the exact name + CompletableFuture archivedRecordingsFuture2 = new CompletableFuture<>(); + webClient + .get(String.format("/api/v1/recordings")) + .send( + ar -> { + if (assertRequestStatus(ar, archivedRecordingsFuture2)) { + archivedRecordingsFuture2.complete(ar.result().bodyAsJsonArray()); + } + }); + JsonArray retrivedArchivedRecordings = + archivedRecordingsFuture2.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + JsonObject retrievedArchivedrecordings = retrivedArchivedRecordings.getJsonObject(0); + String retrievedArchivedRecordingsName = retrievedArchivedrecordings.getString("name"); + + // GraphQL Query to filter Archived recordings by names + CompletableFuture resp2 = new CompletableFuture<>(); + + String query = + "query { targetNodes {" + + "recordings {" + + "archived(filter: { names: [\"" + + retrievedArchivedRecordingsName + + "\",\"someOtherName\"] }) {" + + "data {" + + "name" + + "}" + + "}" + + "}" + + "}" + + "}"; + webClient + .post("/api/v2.2/graphql") + .sendJson( + new JsonObject().put("query", query), + ar -> { + if (assertRequestStatus(ar, resp2)) { + resp2.complete( + gson.fromJson( + ar.result().bodyAsString(), + TargetNodesQueryResponse.class)); + } + }); + + TargetNodesQueryResponse graphqlResp = resp2.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + List archivedRecordings2 = + graphqlResp.data.targetNodes.stream() + .flatMap(targetNode -> targetNode.recordings.archived.data.stream()) + .collect(Collectors.toList()); + + int filteredRecordingsCount = archivedRecordings2.size(); + Assertions.assertEquals( + 1, filteredRecordingsCount, "Number of filtered recordings should be 1"); + + ArchivedRecording archivedRecording = archivedRecordings2.get(0); + String filteredName = archivedRecording.name; + Assertions.assertEquals( + filteredName, + retrievedArchivedRecordingsName, + "Filtered name should match the archived recording name"); + + // Delete archived recording by name + for (ArchivedRecording archrecording : archivedRecordings2) { + String nameMatch = archrecording.name; + + CompletableFuture deleteFuture = new CompletableFuture<>(); + webClient + .delete( + String.format( + "/api/beta/recordings/%s/%s", + SELF_REFERENCE_TARGET_ID, nameMatch)) + .send( + ar -> { + if (assertRequestStatus(ar, deleteFuture)) { + deleteFuture.complete(null); + } else { + deleteFuture.completeExceptionally( + new RuntimeException("Delete request failed")); + } + }); + + deleteFuture.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + // Retrieve the list of updated archived recordings to verify that the targeted recordings + // have been deleted + CompletableFuture updatedArchivedRecordingsFuture = new CompletableFuture<>(); + webClient + .get("/api/v1/recordings") + .send( + ar -> { + if (assertRequestStatus(ar, updatedArchivedRecordingsFuture)) { + updatedArchivedRecordingsFuture.complete( + ar.result().bodyAsJsonArray()); + } + }); + + JsonArray updatedArchivedRecordings = + updatedArchivedRecordingsFuture.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // Assert that the targeted recordings have been deleted + boolean recordingsDeleted = + updatedArchivedRecordings.stream() + .noneMatch( + json -> { + JsonObject recording = (JsonObject) json; + return recording.getString("name").equals(TEST_RECORDING_NAME); + }); + + Assertions.assertTrue( + recordingsDeleted, "The targeted archived recordings should be deleted"); + + // Clean up what we created + CompletableFuture deleteRespFuture1 = new CompletableFuture<>(); + webClient + .delete( + String.format( + "/api/v1/targets/%s/recordings/%s", + SELF_REFERENCE_TARGET_ID, TEST_RECORDING_NAME)) + .send( + ar -> { + if (assertRequestStatus(ar, deleteRespFuture1)) { + deleteRespFuture1.complete(null); + } + }); + + deleteRespFuture1.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + CompletableFuture savedRecordingsFuture = new CompletableFuture<>(); + webClient + .get("/api/v1/recordings") + .send( + ar -> { + if (assertRequestStatus(ar, savedRecordingsFuture)) { + savedRecordingsFuture.complete(ar.result().bodyAsJsonArray()); + } + }); + + JsonArray savedRecordings = + savedRecordingsFuture.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + for (Object savedRecording : savedRecordings) { + String recordingName = ((JsonObject) savedRecording).getString("name"); + if (recordingName.matches("archivedRecordings")) { + CompletableFuture deleteRespFuture2 = new CompletableFuture<>(); + webClient + .delete( + String.format( + "/api/beta/recordings/%s/%s", + SELF_REFERENCE_TARGET_ID, recordingName)) + .send( + ar -> { + if (assertRequestStatus(ar, deleteRespFuture2)) { + deleteRespFuture2.complete(null); + } + }); + + deleteRespFuture2.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } + } + + @Test + @Order(12) + public void testQueryforFilteredEnvironmentNodesByNames() throws Exception { + CompletableFuture resp = new CompletableFuture<>(); + + String query = + "query { environmentNodes(filter: { names: [\"anotherName1\"," + + " \"JDP\",\"anotherName2\"] }) { name nodeType } }"; + webClient + .post("/api/v2.2/graphql") + .sendJson( + new JsonObject().put("query", query), + ar -> { + if (assertRequestStatus(ar, resp)) { + resp.complete( + gson.fromJson( + ar.result().bodyAsString(), + EnvironmentNodesResponse.class)); + } + }); + + EnvironmentNodesResponse actual = resp.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + List environmentNodes = actual.data.environmentNodes; + + Assertions.assertEquals(1, environmentNodes.size(), "The list filtered should be 1"); + + boolean nameExists = false; + for (EnvironmentNode environmentNode : environmentNodes) { + if (environmentNode.name.matches("JDP")) { + nameExists = true; + break; + } + } + Assertions.assertTrue(nameExists, "Name not found"); + } + static class Target { String alias; String serviceUri; @@ -835,6 +1265,7 @@ public boolean equals(Object obj) { } static class TargetNodes { + List targetNodes; @Override