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 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 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 getFirstEntryFromMultiValue(ClusterValue 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 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); + } +}