From 8334d289445f852869ed56eef29fe5f5890bd229 Mon Sep 17 00:00:00 2001
From: SanHalacogluImproving
 <144171266+SanHalacogluImproving@users.noreply.github.com>
Date: Wed, 13 Mar 2024 13:53:53 -0700
Subject: [PATCH] Java: Added Missing Client Tests. (#127)

---
 .../test/java/glide/SharedClientTests.java    | 113 ++++++++++++
 .../src/test/java/glide/TestUtilities.java    |  53 ++++++
 .../test/java/glide/cluster/ClientTests.java  |  48 ------
 .../glide/cluster/ClusterClientTests.java     | 161 +++++++++++++++++
 .../java/glide/standalone/ClientTests.java    |  47 -----
 .../standalone/StandaloneClientTests.java     | 162 ++++++++++++++++++
 6 files changed, 489 insertions(+), 95 deletions(-)
 create mode 100644 java/integTest/src/test/java/glide/SharedClientTests.java
 delete mode 100644 java/integTest/src/test/java/glide/cluster/ClientTests.java
 create mode 100644 java/integTest/src/test/java/glide/cluster/ClusterClientTests.java
 delete mode 100644 java/integTest/src/test/java/glide/standalone/ClientTests.java
 create mode 100644 java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java

diff --git a/java/integTest/src/test/java/glide/SharedClientTests.java b/java/integTest/src/test/java/glide/SharedClientTests.java
new file mode 100644
index 0000000000..79d5f29f17
--- /dev/null
+++ b/java/integTest/src/test/java/glide/SharedClientTests.java
@@ -0,0 +1,113 @@
+/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
+package glide;
+
+import static glide.TestUtilities.commonClientConfig;
+import static glide.TestUtilities.commonClusterClientConfig;
+import static glide.TestUtilities.getRandomString;
+import static glide.api.BaseClient.OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import glide.api.BaseClient;
+import glide.api.RedisClient;
+import glide.api.RedisClusterClient;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Stream;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@Timeout(25)
+public class SharedClientTests {
+
+    private static RedisClient standaloneClient = null;
+    private static RedisClusterClient clusterClient = null;
+
+    @Getter private static List<Arguments> clients;
+
+    @BeforeAll
+    @SneakyThrows
+    public static void init() {
+        standaloneClient = RedisClient.CreateClient(commonClientConfig().build()).get();
+        clusterClient =
+                RedisClusterClient.CreateClient(commonClusterClientConfig().requestTimeout(5000).build())
+                        .get();
+
+        clients = List.of(Arguments.of(standaloneClient), Arguments.of(clusterClient));
+    }
+
+    @AfterAll
+    @SneakyThrows
+    public static void teardown() {
+        standaloneClient.close();
+        clusterClient.close();
+    }
+
+    @SneakyThrows
+    @ParameterizedTest
+    @MethodSource("getClients")
+    public void send_and_receive_large_values(BaseClient client) {
+        int length = 1 << 16;
+        String key = getRandomString(length);
+        String value = getRandomString(length);
+
+        assertEquals(length, key.length());
+        assertEquals(length, value.length());
+        assertEquals(OK, client.set(key, value).get());
+        assertEquals(value, client.get(key).get());
+    }
+
+    @SneakyThrows
+    @ParameterizedTest
+    @MethodSource("getClients")
+    public void send_and_receive_non_ascii_unicode(BaseClient client) {
+        String key = "foo";
+        String value = "שלום hello 汉字";
+
+        assertEquals(OK, client.set(key, value).get());
+        assertEquals(value, client.get(key).get());
+    }
+
+    private static Stream<Arguments> clientAndDataSize() {
+        return Stream.of(
+                Arguments.of(standaloneClient, 100),
+                Arguments.of(standaloneClient, 1 << 16),
+                Arguments.of(clusterClient, 100),
+                Arguments.of(clusterClient, 1 << 16));
+    }
+
+    @ParameterizedTest
+    @MethodSource("clientAndDataSize")
+    public void client_can_handle_concurrent_workload(BaseClient client, int valueSize) {
+        ExecutorService executorService = Executors.newCachedThreadPool();
+        CompletableFuture[] futures = new CompletableFuture[100];
+
+        for (int i = 0; i < 100; i++) {
+            futures[i] =
+                    CompletableFuture.runAsync(
+                            () -> {
+                                String key = getRandomString(valueSize);
+                                String value = getRandomString(valueSize);
+                                try {
+                                    assertEquals(OK, client.set(key, value).get());
+                                    assertEquals(value, client.get(key).get());
+                                } catch (InterruptedException | ExecutionException e) {
+                                    throw new RuntimeException(e);
+                                }
+                            },
+                            executorService);
+        }
+
+        CompletableFuture.allOf(futures).join();
+
+        executorService.shutdown();
+    }
+}
diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java
index 8c18e8b98f..a50d4542f5 100644
--- a/java/integTest/src/test/java/glide/TestUtilities.java
+++ b/java/integTest/src/test/java/glide/TestUtilities.java
@@ -1,9 +1,18 @@
 /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
 package glide;
 
+import static glide.TestConfiguration.CLUSTER_PORTS;
+import static glide.TestConfiguration.STANDALONE_PORTS;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import glide.api.models.ClusterValue;
+import glide.api.models.configuration.NodeAddress;
+import glide.api.models.configuration.RedisClientConfiguration;
+import glide.api.models.configuration.RedisClusterClientConfiguration;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
@@ -23,4 +32,48 @@ public static int getValueFromInfo(String data, String value) {
     public static <T> T getFirstEntryFromMultiValue(ClusterValue<T> data) {
         return data.getMultiValue().get(data.getMultiValue().keySet().toArray(String[]::new)[0]);
     }
+
+    /** Generates a random string of a specified length using ASCII letters. */
+    public static String getRandomString(int length) {
+        String asciiLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+        SecureRandom random = new SecureRandom();
+        StringBuilder sb = new StringBuilder(length);
+
+        for (int i = 0; i < length; i++) {
+            int index = random.nextInt(asciiLetters.length());
+            char randomChar = asciiLetters.charAt(index);
+            sb.append(randomChar);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Transforms server info string into a Map, using lines with ":" to create key-value pairs,
+     * replacing duplicates with the last encountered value.
+     */
+    public static Map<String, String> parseInfoResponseToMap(String serverInfo) {
+        return serverInfo
+                .lines()
+                .filter(line -> line.contains(":"))
+                .map(line -> line.split(":", 2))
+                .collect(
+                        Collectors.toMap(
+                                parts -> parts[0],
+                                parts -> parts[1],
+                                (existingValue, newValue) -> newValue,
+                                HashMap::new));
+    }
+
+    public static RedisClientConfiguration.RedisClientConfigurationBuilder<?, ?>
+            commonClientConfig() {
+        return RedisClientConfiguration.builder()
+                .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build());
+    }
+
+    public static RedisClusterClientConfiguration.RedisClusterClientConfigurationBuilder<?, ?>
+            commonClusterClientConfig() {
+        return RedisClusterClientConfiguration.builder()
+                .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build());
+    }
 }
diff --git a/java/integTest/src/test/java/glide/cluster/ClientTests.java b/java/integTest/src/test/java/glide/cluster/ClientTests.java
deleted file mode 100644
index 3924747391..0000000000
--- a/java/integTest/src/test/java/glide/cluster/ClientTests.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
-package glide.cluster;
-
-import static glide.TestConfiguration.CLUSTER_PORTS;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import glide.api.RedisClusterClient;
-import glide.api.models.configuration.NodeAddress;
-import glide.api.models.configuration.RedisClusterClientConfiguration;
-import glide.api.models.exceptions.ClosingException;
-import java.util.concurrent.ExecutionException;
-import lombok.SneakyThrows;
-import org.junit.jupiter.api.Test;
-
-public class ClientTests {
-    @Test
-    @SneakyThrows
-    public void custom_command_info() {
-        RedisClusterClient client =
-                RedisClusterClient.CreateClient(
-                                RedisClusterClientConfiguration.builder()
-                                        .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build())
-                                        .clientName("TEST_CLIENT_NAME")
-                                        .build())
-                        .get();
-
-        String clientInfo =
-                (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get().getSingleValue();
-        assertTrue(clientInfo.contains("name=TEST_CLIENT_NAME"));
-    }
-
-    @Test
-    @SneakyThrows
-    public void close_client_throws_ExecutionException_with_ClosingException_cause() {
-        RedisClusterClient client =
-                RedisClusterClient.CreateClient(
-                                RedisClusterClientConfiguration.builder()
-                                        .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build())
-                                        .build())
-                        .get();
-
-        client.close();
-        ExecutionException executionException =
-                assertThrows(ExecutionException.class, () -> client.set("foo", "bar").get());
-        assertTrue(executionException.getCause() instanceof ClosingException);
-    }
-}
diff --git a/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java b/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java
new file mode 100644
index 0000000000..aefeb36ad3
--- /dev/null
+++ b/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java
@@ -0,0 +1,161 @@
+/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
+package glide.cluster;
+
+import static glide.TestConfiguration.REDIS_VERSION;
+import static glide.TestUtilities.commonClusterClientConfig;
+import static glide.TestUtilities.getRandomString;
+import static glide.api.BaseClient.OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import glide.api.RedisClusterClient;
+import glide.api.models.configuration.RedisCredentials;
+import glide.api.models.exceptions.ClosingException;
+import glide.api.models.exceptions.RequestException;
+import java.util.concurrent.ExecutionException;
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@Timeout(10)
+public class ClusterClientTests {
+
+    @SneakyThrows
+    @Test
+    public void register_client_name_and_version() {
+        String minVersion = "7.2.0";
+        assumeTrue(
+                REDIS_VERSION.isGreaterThanOrEqualTo(minVersion),
+                "Redis version required >= " + minVersion);
+
+        RedisClusterClient client =
+                RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get();
+
+        String info =
+                (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get().getSingleValue();
+        assertTrue(info.contains("lib-name=GlideJava"));
+        assertTrue(info.contains("lib-ver=unknown"));
+
+        client.close();
+    }
+
+    @SneakyThrows
+    @Test
+    public void can_connect_with_auth_requirepass() {
+        RedisClusterClient client =
+                RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get();
+
+        String password = "TEST_AUTH";
+        client.customCommand(new String[] {"CONFIG", "SET", "requirepass", password}).get();
+
+        // Creation of a new client without a password should fail
+        ExecutionException exception =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get());
+        assertTrue(exception.getCause() instanceof ClosingException);
+
+        // Creation of a new client with credentials
+        RedisClusterClient auth_client =
+                RedisClusterClient.CreateClient(
+                                commonClusterClientConfig()
+                                        .credentials(RedisCredentials.builder().password(password).build())
+                                        .build())
+                        .get();
+
+        String key = getRandomString(10);
+        String value = getRandomString(10);
+
+        assertEquals(OK, auth_client.set(key, value).get());
+        assertEquals(value, auth_client.get(key).get());
+
+        // Reset password
+        client.customCommand(new String[] {"CONFIG", "SET", "requirepass", ""}).get();
+
+        auth_client.close();
+        client.close();
+    }
+
+    @SneakyThrows
+    @Test
+    public void can_connect_with_auth_acl() {
+        RedisClusterClient client =
+                RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get();
+
+        String username = "testuser";
+        String password = "TEST_AUTH";
+        assertEquals(
+                OK,
+                client
+                        .customCommand(
+                                new String[] {
+                                    "ACL",
+                                    "SETUSER",
+                                    username,
+                                    "on",
+                                    "allkeys",
+                                    "+get",
+                                    "+cluster",
+                                    "+ping",
+                                    "+info",
+                                    "+client",
+                                    ">" + password,
+                                })
+                        .get()
+                        .getSingleValue());
+
+        String key = getRandomString(10);
+        String value = getRandomString(10);
+
+        assertEquals(OK, client.set(key, value).get());
+
+        // Creation of a new cluster client with credentials
+        RedisClusterClient testUserClient =
+                RedisClusterClient.CreateClient(
+                                commonClusterClientConfig()
+                                        .credentials(
+                                                RedisCredentials.builder().username(username).password(password).build())
+                                        .build())
+                        .get();
+
+        assertEquals(value, testUserClient.get(key).get());
+
+        ExecutionException executionException =
+                assertThrows(ExecutionException.class, () -> testUserClient.set("foo", "bar").get());
+        assertTrue(executionException.getCause() instanceof RequestException);
+
+        client.customCommand(new String[] {"ACL", "DELUSER", username}).get();
+
+        testUserClient.close();
+        client.close();
+    }
+
+    @SneakyThrows
+    @Test
+    public void client_name() {
+        RedisClusterClient client =
+                RedisClusterClient.CreateClient(
+                                commonClusterClientConfig().clientName("TEST_CLIENT_NAME").build())
+                        .get();
+
+        String clientInfo =
+                (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get().getSingleValue();
+        assertTrue(clientInfo.contains("name=TEST_CLIENT_NAME"));
+
+        client.close();
+    }
+
+    @Test
+    @SneakyThrows
+    public void closed_client_throws_ExecutionException_with_ClosingException_as_cause() {
+        RedisClusterClient client =
+                RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get();
+
+        client.close();
+        ExecutionException executionException =
+                assertThrows(ExecutionException.class, () -> client.set("foo", "bar").get());
+        assertTrue(executionException.getCause() instanceof ClosingException);
+    }
+}
diff --git a/java/integTest/src/test/java/glide/standalone/ClientTests.java b/java/integTest/src/test/java/glide/standalone/ClientTests.java
deleted file mode 100644
index 960696b06e..0000000000
--- a/java/integTest/src/test/java/glide/standalone/ClientTests.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
-package glide.standalone;
-
-import static glide.TestConfiguration.STANDALONE_PORTS;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import glide.api.RedisClient;
-import glide.api.models.configuration.NodeAddress;
-import glide.api.models.configuration.RedisClientConfiguration;
-import glide.api.models.exceptions.ClosingException;
-import java.util.concurrent.ExecutionException;
-import lombok.SneakyThrows;
-import org.junit.jupiter.api.Test;
-
-public class ClientTests {
-    @Test
-    @SneakyThrows
-    public void custom_command_info() {
-        RedisClient client =
-                RedisClient.CreateClient(
-                                RedisClientConfiguration.builder()
-                                        .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build())
-                                        .clientName("TEST_CLIENT_NAME")
-                                        .build())
-                        .get();
-
-        String clientInfo = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get();
-        assertTrue(clientInfo.contains("name=TEST_CLIENT_NAME"));
-    }
-
-    @Test
-    @SneakyThrows
-    public void close_client_throws_ExecutionException_with_ClosingException_cause() {
-        RedisClient client =
-                RedisClient.CreateClient(
-                                RedisClientConfiguration.builder()
-                                        .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build())
-                                        .build())
-                        .get();
-
-        client.close();
-        ExecutionException executionException =
-                assertThrows(ExecutionException.class, () -> client.set("key", "value").get());
-        assertTrue(executionException.getCause() instanceof ClosingException);
-    }
-}
diff --git a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java
new file mode 100644
index 0000000000..4356f1e333
--- /dev/null
+++ b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java
@@ -0,0 +1,162 @@
+/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
+package glide.standalone;
+
+import static glide.TestConfiguration.REDIS_VERSION;
+import static glide.TestUtilities.commonClientConfig;
+import static glide.TestUtilities.getRandomString;
+import static glide.api.BaseClient.OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import glide.api.RedisClient;
+import glide.api.models.configuration.RedisCredentials;
+import glide.api.models.exceptions.ClosingException;
+import glide.api.models.exceptions.RequestException;
+import java.util.concurrent.ExecutionException;
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@Timeout(10)
+public class StandaloneClientTests {
+
+    @SneakyThrows
+    @Test
+    public void register_client_name_and_version() {
+        String minVersion = "7.2.0";
+        assumeTrue(
+                REDIS_VERSION.isGreaterThanOrEqualTo(minVersion),
+                "Redis version required >= " + minVersion);
+
+        RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get();
+
+        String info = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get();
+        assertTrue(info.contains("lib-name=GlideJava"));
+        assertTrue(info.contains("lib-ver=unknown"));
+
+        client.close();
+    }
+
+    @SneakyThrows
+    @Test
+    public void can_connect_with_auth_require_pass() {
+        RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get();
+
+        String password = "TEST_AUTH";
+        client.customCommand(new String[] {"CONFIG", "SET", "requirepass", password}).get();
+
+        // Creation of a new client without a password should fail
+        ExecutionException exception =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> RedisClient.CreateClient(commonClientConfig().build()).get());
+        assertTrue(exception.getCause() instanceof ClosingException);
+
+        // Creation of a new client with credentials
+        RedisClient auth_client =
+                RedisClient.CreateClient(
+                                commonClientConfig()
+                                        .credentials(RedisCredentials.builder().password(password).build())
+                                        .build())
+                        .get();
+
+        String key = getRandomString(10);
+        String value = getRandomString(10);
+
+        assertEquals(OK, auth_client.set(key, value).get());
+        assertEquals(value, auth_client.get(key).get());
+
+        // Reset password
+        client.customCommand(new String[] {"CONFIG", "SET", "requirepass", ""}).get();
+
+        auth_client.close();
+        client.close();
+    }
+
+    @SneakyThrows
+    @Test
+    public void can_connect_with_auth_acl() {
+        RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get();
+
+        String username = "testuser";
+        String password = "TEST_AUTH";
+        assertEquals(
+                OK,
+                client
+                        .customCommand(
+                                new String[] {
+                                    "ACL",
+                                    "SETUSER",
+                                    username,
+                                    "on",
+                                    "allkeys",
+                                    "+get",
+                                    "+cluster",
+                                    "+ping",
+                                    "+info",
+                                    "+client",
+                                    ">" + password,
+                                })
+                        .get());
+
+        String key = getRandomString(10);
+        String value = getRandomString(10);
+
+        assertEquals(OK, client.set(key, value).get());
+
+        // Creation of a new client with credentials
+        RedisClient testUserClient =
+                RedisClient.CreateClient(
+                                commonClientConfig()
+                                        .credentials(
+                                                RedisCredentials.builder().username(username).password(password).build())
+                                        .build())
+                        .get();
+
+        assertEquals(value, testUserClient.get(key).get());
+        ExecutionException executionException =
+                assertThrows(ExecutionException.class, () -> testUserClient.set("foo", "bar").get());
+        assertTrue(executionException.getCause() instanceof RequestException);
+
+        client.customCommand(new String[] {"ACL", "DELUSER", username}).get();
+
+        testUserClient.close();
+        client.close();
+    }
+
+    @SneakyThrows
+    @Test
+    public void select_standalone_database_id() {
+        RedisClient client = RedisClient.CreateClient(commonClientConfig().databaseId(4).build()).get();
+
+        String clientInfo = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get();
+        assertTrue(clientInfo.contains("db=4"));
+
+        client.close();
+    }
+
+    @SneakyThrows
+    @Test
+    public void client_name() {
+        RedisClient client =
+                RedisClient.CreateClient(commonClientConfig().clientName("TEST_CLIENT_NAME").build()).get();
+
+        String clientInfo = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get();
+        assertTrue(clientInfo.contains("name=TEST_CLIENT_NAME"));
+
+        client.close();
+    }
+
+    @Test
+    @SneakyThrows
+    public void closed_client_throws_ExecutionException_with_ClosingException_as_cause() {
+        RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get();
+
+        client.close();
+        ExecutionException executionException =
+                assertThrows(ExecutionException.class, () -> client.set("key", "value").get());
+        assertTrue(executionException.getCause() instanceof ClosingException);
+    }
+}