diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 9d8400e426..ff82940897 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -10,11 +10,11 @@ concurrency:
group: "pages"
cancel-in-progress: false
jobs:
- build-and-deploy:
+ publish-docs:
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.9
@@ -26,11 +26,11 @@ jobs:
run: |
mkdocs build -d docsbuild
- name: Setup Pages
- uses: actions/configure-pages@v3
+ uses: actions/configure-pages@v4
- name: Upload artifact
- uses: actions/upload-pages-artifact@v1
+ uses: actions/upload-pages-artifact@v3
with:
path: 'docsbuild'
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v2
\ No newline at end of file
+ uses: actions/deploy-pages@v4
\ No newline at end of file
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 9f061d0cdb..a065c23210 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -15,23 +15,35 @@ on:
schedule:
- cron: '0 1 * * *' # nightly build
workflow_dispatch:
- inputs:
- redis_version:
- description: "Redis stack version to use for testing"
- required: false
- default: "8.0-M02"
- type: choice
- options:
- - "8.0-M02"
- - "rs-7.4.0-v1"
- - "rs-7.2.0-v13"
jobs:
-
build:
name: Build and Test
runs-on: ubuntu-24.04
+ strategy:
+ fail-fast: false
+ matrix:
+ redis_version:
+ - "unstable"
+ - "8.0"
+ - "7.4"
+ - "7.2"
+
steps:
+ - name: Test Redis Server Version
+ id: map-tags
+ run: |
+ # Map requested version to github or tag
+ case "${{ matrix.redis_version }}" in
+ "unstable") redis_branch="unstable" stack_version="8.0-M04-pre" ;;
+ "8.0") redis_branch="8.0" stack_version="8.0-M04-pre" ;;
+ "7.4") redis_branch="7.4" stack_version="rs-7.4.0-v2" ;;
+ "7.2") redis_branch="7.2" stack_version="rs-7.2.0-v14" ;;
+ *) echo "Unsupported version: ${{ matrix.redis_version }}" && exit 1 ;;
+ esac
+ # Save them as outputs for later use
+ echo "redis_branch=$redis_branch" >> $GITHUB_OUTPUT
+ echo "redis_stack_version=$stack_version" >> $GITHUB_OUTPUT
- name: Checkout project
uses: actions/checkout@v4
- name: Set Java up in the runner
@@ -61,7 +73,8 @@ jobs:
run: |
make test-coverage
env:
- REDIS_STACK_VERSION: ${{ inputs.redis_version || '8.0-M02' }}
+ REDIS: ${{ steps.map-tags.outputs.redis_branch }}
+ REDIS_STACK_VERSION: ${{ steps.map-tags.outputs.redis_stack_version }}
JVM_OPTS: -Xmx3200m
TERM: dumb
- name: Upload coverage reports to Codecov
diff --git a/pom.xml b/pom.xml
index 2a6392958e..64aa9e39cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
and much more.
- http://github.com/lettuce-io/lettuce-core
+ https://github.com/redis/lettuce
lettuce.io
@@ -30,7 +30,7 @@
Github
- https://github.com/lettuce-io/lettuce-core/issues
+ https://github.com/redis/lettuce/issues
@@ -82,10 +82,10 @@
- scm:git:https://github.com/lettuce-io/lettuce-core.git
- scm:git:https://github.com/lettuce-io/lettuce-core.git
+ scm:git:https://github.com/redis/lettuce.git
+ scm:git:https://github.com/redis/lettuce.git
- http://github.com/lettuce-io/lettuce-core
+ https://github.com/redis/lettuce
HEAD
@@ -189,12 +189,6 @@
0.1.1-beta1
test
-
- io.github.cdimascio
- dotenv-java
- 2.2.0
- test
-
@@ -1063,6 +1057,38 @@
ci
+
+ entraid-it
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ maven-failsafe-plugin
+
+ entraid
+ false
+
+ **/*IntegrationTests
+
+
+
+
+ integration-test
+
+ integration-test
+ verify
+
+
+
+
+
+
+
diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
index de095893fa..fd1ad55ebd 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
@@ -48,7 +48,6 @@
import io.lettuce.core.protocol.CommandType;
import io.lettuce.core.protocol.ProtocolKeyword;
import io.lettuce.core.protocol.RedisCommand;
-import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.Instant;
@@ -56,6 +55,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Supplier;
import static io.lettuce.core.ClientOptions.DEFAULT_JSON_PARSER;
import static io.lettuce.core.protocol.CommandType.EXEC;
@@ -87,7 +87,7 @@ public abstract class AbstractRedisAsyncCommands implements RedisAclAsyncC
private final RedisJsonCommandBuilder jsonCommandBuilder;
- private final Mono parser;
+ private final Supplier parser;
/**
* Initialize a new instance.
@@ -96,7 +96,8 @@ public abstract class AbstractRedisAsyncCommands implements RedisAclAsyncC
* @param codec the codec for command encoding
* @param parser the implementation of the {@link JsonParser} to use
*/
- public AbstractRedisAsyncCommands(StatefulConnection connection, RedisCodec codec, Mono parser) {
+ public AbstractRedisAsyncCommands(StatefulConnection connection, RedisCodec codec,
+ Supplier parser) {
this.parser = parser;
this.connection = connection;
this.commandBuilder = new RedisCommandBuilder<>(codec);
@@ -3396,7 +3397,7 @@ public RedisFuture>> clusterLinks() {
@Override
public JsonParser getJsonParser() {
- return this.parser.block();
+ return this.parser.get();
}
private byte[] encodeFunction(String functionCode) {
diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
index 1e9365821f..e1651e5623 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
@@ -97,7 +97,7 @@ public abstract class AbstractRedisReactiveCommands
private final RedisJsonCommandBuilder jsonCommandBuilder;
- private final Mono parser;
+ private final Supplier parser;
private final ClientResources clientResources;
@@ -112,7 +112,8 @@ public abstract class AbstractRedisReactiveCommands
* @param codec the codec for command encoding.
* @param parser the implementation of the {@link JsonParser} to use
*/
- public AbstractRedisReactiveCommands(StatefulConnection connection, RedisCodec codec, Mono parser) {
+ public AbstractRedisReactiveCommands(StatefulConnection connection, RedisCodec codec,
+ Supplier parser) {
this.connection = connection;
this.parser = parser;
this.commandBuilder = new RedisCommandBuilder<>(codec);
@@ -149,7 +150,7 @@ private EventExecutorGroup getScheduler() {
@Override
public JsonParser getJsonParser() {
- return parser.block();
+ return parser.get();
}
@Override
diff --git a/src/main/java/io/lettuce/core/AclCategory.java b/src/main/java/io/lettuce/core/AclCategory.java
index db4066d6b3..bc04372fca 100644
--- a/src/main/java/io/lettuce/core/AclCategory.java
+++ b/src/main/java/io/lettuce/core/AclCategory.java
@@ -111,5 +111,45 @@ public enum AclCategory {
/**
* scripting command
*/
- SCRIPTING
+ SCRIPTING,
+
+ /**
+ * bloom command
+ */
+ BLOOM,
+
+ /**
+ * cuckoo command
+ */
+ CUCKOO,
+
+ /**
+ * count-min-sketch command
+ */
+ CMS,
+
+ /**
+ * top-k command
+ */
+ TOPK,
+
+ /**
+ * t-digest command
+ */
+ TDIGEST,
+
+ /**
+ * search command
+ */
+ SEARCH,
+
+ /**
+ * timeseries command
+ */
+ TIMESERIES,
+
+ /**
+ * json command
+ */
+ JSON
}
diff --git a/src/main/java/io/lettuce/core/ClientOptions.java b/src/main/java/io/lettuce/core/ClientOptions.java
index 3fd635e4ee..5c02c3b18c 100644
--- a/src/main/java/io/lettuce/core/ClientOptions.java
+++ b/src/main/java/io/lettuce/core/ClientOptions.java
@@ -25,6 +25,7 @@
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
+import java.util.function.Supplier;
import io.lettuce.core.api.StatefulConnection;
import io.lettuce.core.internal.LettuceAssert;
@@ -71,7 +72,7 @@ public class ClientOptions implements Serializable {
public static final SocketOptions DEFAULT_SOCKET_OPTIONS = SocketOptions.create();
- public static final Mono DEFAULT_JSON_PARSER = Mono.defer(() -> Mono.fromCallable(() -> {
+ public static final Supplier DEFAULT_JSON_PARSER = () -> {
try {
Iterator services = ServiceLoader.load(JsonParser.class).iterator();
return services.hasNext() ? services.next() : null;
@@ -79,7 +80,7 @@ public class ClientOptions implements Serializable {
throw new RedisJsonException("Could not load JsonParser, please consult the guide"
+ "at https://redis.github.io/lettuce/user-guide/redis-json/", e);
}
- }));
+ };
public static final SslOptions DEFAULT_SSL_OPTIONS = SslOptions.create();
@@ -111,7 +112,7 @@ public class ClientOptions implements Serializable {
private final Charset scriptCharset;
- private final Mono jsonParser;
+ private final Supplier jsonParser;
private final SocketOptions socketOptions;
@@ -216,7 +217,7 @@ public static class Builder {
private Charset scriptCharset = DEFAULT_SCRIPT_CHARSET;
- private Mono jsonParser = DEFAULT_JSON_PARSER;
+ private Supplier jsonParser = DEFAULT_JSON_PARSER;
private SocketOptions socketOptions = DEFAULT_SOCKET_OPTIONS;
@@ -429,7 +430,7 @@ public Builder scriptCharset(Charset scriptCharset) {
* @see JsonParser
* @since 6.5
*/
- public Builder jsonParser(Mono parser) {
+ public Builder jsonParser(Supplier parser) {
LettuceAssert.notNull(parser, "JsonParser must not be null");
this.jsonParser = parser;
@@ -705,7 +706,7 @@ public Charset getScriptCharset() {
* @return the implementation of the {@link JsonParser} to use.
* @since 6.5
*/
- public Mono getJsonParser() {
+ public Supplier getJsonParser() {
return jsonParser;
}
diff --git a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java
index 87bae13e02..4e20755197 100644
--- a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java
@@ -5,7 +5,7 @@
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.json.JsonParser;
-import reactor.core.publisher.Mono;
+import java.util.function.Supplier;
/**
* An asynchronous and thread-safe API for a Redis connection.
@@ -24,7 +24,8 @@ public class RedisAsyncCommandsImpl extends AbstractRedisAsyncCommands connection, RedisCodec codec, Mono parser) {
+ public RedisAsyncCommandsImpl(StatefulRedisConnection connection, RedisCodec codec,
+ Supplier parser) {
super(connection, codec, parser);
}
diff --git a/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java
index ee7e8cf97b..9aff2e5942 100644
--- a/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java
+++ b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java
@@ -20,9 +20,9 @@
import io.lettuce.core.protocol.BaseRedisCommandBuilder;
import io.lettuce.core.protocol.Command;
import io.lettuce.core.protocol.CommandArgs;
-import reactor.core.publisher.Mono;
import java.util.List;
+import java.util.function.Supplier;
import static io.lettuce.core.protocol.CommandType.*;
@@ -34,9 +34,9 @@
*/
class RedisJsonCommandBuilder extends BaseRedisCommandBuilder {
- private final Mono parser;
+ private final Supplier parser;
- RedisJsonCommandBuilder(RedisCodec codec, Mono theParser) {
+ RedisJsonCommandBuilder(RedisCodec codec, Supplier theParser) {
super(codec);
parser = theParser;
}
@@ -118,7 +118,7 @@ Command> jsonArrpop(K key, JsonPath jsonPath, int index) {
}
}
- return createCommand(JSON_ARRPOP, new JsonValueListOutput<>(codec, parser.block()), args);
+ return createCommand(JSON_ARRPOP, new JsonValueListOutput<>(codec, parser.get()), args);
}
Command> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) {
@@ -167,7 +167,7 @@ Command> jsonGet(K key, JsonGetArgs options, JsonPath... j
}
}
- return createCommand(JSON_GET, new JsonValueListOutput<>(codec, parser.block()), args);
+ return createCommand(JSON_GET, new JsonValueListOutput<>(codec, parser.get()), args);
}
Command jsonMerge(K key, JsonPath jsonPath, JsonValue value) {
@@ -194,7 +194,7 @@ Command> jsonMGet(JsonPath jsonPath, K... keys) {
args.add(jsonPath.toString());
}
- return createCommand(JSON_MGET, new JsonValueListOutput<>(codec, parser.block()), args);
+ return createCommand(JSON_MGET, new JsonValueListOutput<>(codec, parser.get()), args);
}
Command jsonMSet(List> arguments) {
diff --git a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java
index de01957391..620b5510ae 100644
--- a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java
@@ -7,6 +7,8 @@
import io.lettuce.core.json.JsonParser;
import reactor.core.publisher.Mono;
+import java.util.function.Supplier;
+
/**
* A reactive and thread-safe API for a Redis Sentinel connection.
*
@@ -25,7 +27,7 @@ public class RedisReactiveCommandsImpl extends AbstractRedisReactiveComman
* @param parser the implementation of the {@link JsonParser} to use
*/
public RedisReactiveCommandsImpl(StatefulRedisConnection connection, RedisCodec codec,
- Mono parser) {
+ Supplier parser) {
super(connection, codec, parser);
}
diff --git a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java
index b51ee8ffae..8052b95617 100644
--- a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java
@@ -27,6 +27,7 @@
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.lettuce.core.api.StatefulRedisConnection;
@@ -67,7 +68,7 @@ public class StatefulRedisConnectionImpl extends RedisChannelHandler
private final PushHandler pushHandler;
- private final Mono parser;
+ private final Supplier parser;
protected MultiOutput multi;
@@ -96,7 +97,7 @@ public StatefulRedisConnectionImpl(RedisChannelWriter writer, PushHandler pushHa
* @param parser the parser to use for JSON commands.
*/
public StatefulRedisConnectionImpl(RedisChannelWriter writer, PushHandler pushHandler, RedisCodec codec,
- Duration timeout, Mono parser) {
+ Duration timeout, Supplier parser) {
super(writer, timeout);
diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java
index 840a6ef1ff..d8f8e40923 100644
--- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java
@@ -29,13 +29,13 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.lettuce.core.*;
@@ -64,7 +64,6 @@
import io.lettuce.core.protocol.Command;
import io.lettuce.core.protocol.CommandType;
import io.lettuce.core.protocol.ConnectionIntent;
-import reactor.core.publisher.Mono;
/**
* An advanced asynchronous and thread-safe API for a Redis Cluster connection.
@@ -89,11 +88,11 @@ public class RedisAdvancedClusterAsyncCommandsImpl extends AbstractRedisAs
* @param codec Codec used to encode/decode keys and values.
* @param parser the implementation of the {@link JsonParser} to use
* @deprecated since 5.1, use
- * {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+ * {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
*/
@Deprecated
public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec,
- Mono parser) {
+ Supplier parser) {
super(connection, codec, parser);
this.codec = codec;
}
@@ -104,7 +103,7 @@ public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl<
* @param connection the stateful connection
* @param codec Codec used to encode/decode keys and values.
* @deprecated since 5.1, use
- * {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+ * {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
*/
@Deprecated
public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec) {
@@ -120,7 +119,7 @@ public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl<
* @param parser the implementation of the {@link JsonParser} to use
*/
public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec,
- Mono parser) {
+ Supplier parser) {
super(connection, codec, parser);
this.codec = codec;
}
diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java
index e12a352c4c..448c4073bb 100644
--- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java
@@ -32,6 +32,7 @@
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.lettuce.core.json.JsonParser;
@@ -78,11 +79,11 @@ public class RedisAdvancedClusterReactiveCommandsImpl extends AbstractRedi
* @param codec Codec used to encode/decode keys and values.
* @param parser the implementation of the {@link JsonParser} to use
* @deprecated since 5.2, use
- * {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+ * {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
*/
@Deprecated
public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec,
- Mono parser) {
+ Supplier parser) {
super(connection, codec, parser);
this.codec = codec;
}
@@ -93,7 +94,7 @@ public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionIm
* @param connection the stateful connection.
* @param codec Codec used to encode/decode keys and values.
* @deprecated since 5.2, use
- * {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+ * {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
*/
@Deprecated
public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl connection,
@@ -110,7 +111,7 @@ public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionIm
* @param parser the implementation of the {@link JsonParser} to use
*/
public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec,
- Mono parser) {
+ Supplier parser) {
super(connection, codec, parser);
this.codec = codec;
}
diff --git a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java
index 31125d93c7..b5fa5cf196 100644
--- a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java
+++ b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java
@@ -587,7 +587,7 @@ ConnectionFuture> connectToNodeAsync(RedisC
* @return new instance of StatefulRedisConnectionImpl
*/
protected StatefulRedisConnectionImpl newStatefulRedisConnection(RedisChannelWriter channelWriter,
- PushHandler pushHandler, RedisCodec codec, Duration timeout, Mono parser) {
+ PushHandler pushHandler, RedisCodec codec, Duration timeout, Supplier parser) {
return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, parser);
}
@@ -734,7 +734,7 @@ private CompletableFuture> connectCl
*/
protected StatefulRedisClusterConnectionImpl newStatefulRedisClusterConnection(
RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout,
- Mono parser) {
+ Supplier parser) {
return new StatefulRedisClusterConnectionImpl(channelWriter, pushHandler, codec, timeout, parser);
}
diff --git a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java
index c84193491b..d967ada1ed 100644
--- a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java
@@ -30,6 +30,7 @@
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.lettuce.core.AbstractRedisClient;
@@ -77,7 +78,7 @@ public class StatefulRedisClusterConnectionImpl extends RedisChannelHandle
protected final RedisCodec codec;
- protected final Mono parser;
+ protected final Supplier parser;
protected final RedisAdvancedClusterCommands sync;
@@ -113,7 +114,7 @@ public StatefulRedisClusterConnectionImpl(RedisChannelWriter writer, ClusterPush
* @param parser the JSON parser
*/
public StatefulRedisClusterConnectionImpl(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec codec,
- Duration timeout, Mono parser) {
+ Duration timeout, Supplier parser) {
super(writer, timeout);
this.pushHandler = pushHandler;
diff --git a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java
index ad1b5a53f6..babafd51e4 100644
--- a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java
@@ -1,12 +1,12 @@
package io.lettuce.core.masterreplica;
import java.time.Duration;
+import java.util.function.Supplier;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.StatefulRedisConnectionImpl;
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.json.JsonParser;
-import reactor.core.publisher.Mono;
import static io.lettuce.core.ClientOptions.DEFAULT_JSON_PARSER;
@@ -36,7 +36,7 @@ class StatefulRedisMasterReplicaConnectionImpl extends StatefulRedisConnec
* @param parser the JSON parser to use
*/
StatefulRedisMasterReplicaConnectionImpl(MasterReplicaChannelWriter writer, RedisCodec codec, Duration timeout,
- Mono parser) {
+ Supplier parser) {
super(writer, NoOpPushHandler.INSTANCE, codec, timeout, parser);
}
diff --git a/src/main/java/io/lettuce/core/models/command/CommandDetailParser.java b/src/main/java/io/lettuce/core/models/command/CommandDetailParser.java
index 0f4c03325c..5dcd3a2b46 100644
--- a/src/main/java/io/lettuce/core/models/command/CommandDetailParser.java
+++ b/src/main/java/io/lettuce/core/models/command/CommandDetailParser.java
@@ -95,6 +95,15 @@ public class CommandDetailParser {
aclCategoriesMap.put("@connection", AclCategory.CONNECTION);
aclCategoriesMap.put("@transaction", AclCategory.TRANSACTION);
aclCategoriesMap.put("@scripting", AclCategory.SCRIPTING);
+ aclCategoriesMap.put("@bloom", AclCategory.BLOOM);
+ aclCategoriesMap.put("@cuckoo", AclCategory.CUCKOO);
+ aclCategoriesMap.put("@cms", AclCategory.CMS);
+ aclCategoriesMap.put("@topk", AclCategory.TOPK);
+ aclCategoriesMap.put("@tdigest", AclCategory.TDIGEST);
+ aclCategoriesMap.put("@search", AclCategory.SEARCH);
+ aclCategoriesMap.put("@timeseries", AclCategory.TIMESERIES);
+ aclCategoriesMap.put("@json", AclCategory.JSON);
+
ACL_CATEGORY_MAPPING = Collections.unmodifiableMap(aclCategoriesMap);
}
diff --git a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java
index e02cf4d960..af5937d4e2 100644
--- a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java
@@ -21,6 +21,7 @@
import java.net.SocketAddress;
import java.util.Map;
+import java.util.function.Supplier;
import io.lettuce.core.AbstractRedisReactiveCommands;
import io.lettuce.core.ClientListArgs;
@@ -52,7 +53,7 @@ public class RedisSentinelReactiveCommandsImpl extends AbstractRedisReacti
private final SentinelCommandBuilder commandBuilder;
public RedisSentinelReactiveCommandsImpl(StatefulConnection connection, RedisCodec codec,
- Mono parser) {
+ Supplier parser) {
super(connection, codec, parser);
commandBuilder = new SentinelCommandBuilder(codec);
}
diff --git a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java
index f5a9a9c625..2eb2090461 100644
--- a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java
@@ -21,6 +21,7 @@
import java.time.Duration;
import java.util.Collection;
+import java.util.function.Supplier;
import io.lettuce.core.ConnectionState;
import io.lettuce.core.RedisChannelHandler;
@@ -34,7 +35,6 @@
import io.lettuce.core.sentinel.api.async.RedisSentinelAsyncCommands;
import io.lettuce.core.sentinel.api.reactive.RedisSentinelReactiveCommands;
import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands;
-import reactor.core.publisher.Mono;
import static io.lettuce.core.ClientOptions.DEFAULT_JSON_PARSER;
@@ -74,7 +74,7 @@ public StatefulRedisSentinelConnectionImpl(RedisChannelWriter writer, RedisCodec
* @param parser the parser used to parse JSON responses
*/
public StatefulRedisSentinelConnectionImpl(RedisChannelWriter writer, RedisCodec codec, Duration timeout,
- Mono parser) {
+ Supplier parser) {
super(writer, timeout);
diff --git a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java
index 1ac2848077..f0d7383363 100644
--- a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java
+++ b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java
@@ -20,6 +20,7 @@
package biz.paluch.redis.extensibility;
import java.time.Duration;
+import java.util.function.Supplier;
import javax.enterprise.inject.Alternative;
@@ -51,7 +52,7 @@ public MyExtendedRedisClusterClient() {
@Override
protected StatefulRedisClusterConnectionImpl newStatefulRedisClusterConnection(
RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout,
- Mono parser) {
+ Supplier parser) {
return new MyRedisClusterConnection<>(channelWriter, pushHandler, codec, timeout, parser);
}
diff --git a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java
index 04632e463c..a1152b9760 100644
--- a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java
+++ b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java
@@ -20,6 +20,7 @@
package biz.paluch.redis.extensibility;
import java.time.Duration;
+import java.util.function.Supplier;
import io.lettuce.core.RedisChannelWriter;
import io.lettuce.core.cluster.ClusterPushHandler;
@@ -37,7 +38,7 @@
class MyRedisClusterConnection extends StatefulRedisClusterConnectionImpl {
public MyRedisClusterConnection(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec codec,
- Duration timeout, Mono parser) {
+ Duration timeout, Supplier parser) {
super(writer, pushHandler, codec, timeout, parser);
}
diff --git a/src/test/java/io/lettuce/TestTags.java b/src/test/java/io/lettuce/TestTags.java
index 68a3434e02..06820f3000 100644
--- a/src/test/java/io/lettuce/TestTags.java
+++ b/src/test/java/io/lettuce/TestTags.java
@@ -29,4 +29,9 @@ public class TestTags {
*/
public static final String API_GENERATOR = "api_generator";
+ /**
+ * Tag for EntraId integration tests (require a running environment with configured microsoft EntraId authentication)
+ */
+ public static final String ENTRA_ID = "entraid";
+
}
diff --git a/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java
new file mode 100644
index 0000000000..e8b3873dea
--- /dev/null
+++ b/src/test/java/io/lettuce/authx/EntraIdClusterIntegrationTests.java
@@ -0,0 +1,140 @@
+package io.lettuce.authx;
+
+import io.lettuce.core.ClientOptions;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.SocketOptions;
+import io.lettuce.core.TimeoutOptions;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.cluster.ClusterClientOptions;
+import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
+import io.lettuce.core.cluster.RedisClusterClient;
+import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
+import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
+import io.lettuce.core.resource.ClientResources;
+import io.lettuce.core.resource.DnsResolver;
+import io.lettuce.test.env.Endpoints;
+import io.lettuce.test.env.Endpoints.Endpoint;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import redis.clients.authentication.core.TokenAuthConfig;
+import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+import static io.lettuce.TestTags.ENTRA_ID;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+@Tag(ENTRA_ID)
+public class EntraIdClusterIntegrationTests {
+
+ private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;
+
+ private static TokenBasedRedisCredentialsProvider credentialsProvider;
+
+ private static RedisClusterClient clusterClient;
+
+ private static ClientResources resources;
+
+ private static Endpoint cluster;
+
+ @BeforeAll
+ public static void setup() {
+ cluster = Endpoints.DEFAULT.getEndpoint("cluster-entraid-acl");
+ if (cluster != null) {
+ Assumptions.assumeTrue(testCtx.getClientId() != null && testCtx.getClientSecret() != null,
+ "Skipping EntraID tests. Azure AD credentials not provided!");
+
+ // Configure timeout options to assure fast test failover
+ ClusterClientOptions clientOptions = ClusterClientOptions.builder()
+ .socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(1)).build())
+ .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1)))
+ // enable re-authentication
+ .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build();
+
+ TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().clientId(testCtx.getClientId())
+ .secret(testCtx.getClientSecret()).authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes())
+ .expirationRefreshRatio(0.0000001F).build();
+
+ credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig);
+
+ resources = ClientResources.builder()
+ // .dnsResolver(DnsResolver.jvmDefault())
+ .build();
+
+ List seedURI = new ArrayList<>();
+ for (String addr : cluster.getRawEndpoints().get(0).getAddr()) {
+ seedURI.add(RedisURI.builder().withAuthentication(credentialsProvider).withHost(addr)
+ .withPort(cluster.getRawEndpoints().get(0).getPort()).build());
+ }
+
+ clusterClient = RedisClusterClient.create(resources, seedURI);
+ clusterClient.setOptions(clientOptions);
+ }
+ }
+
+ @AfterAll
+ public static void cleanup() {
+ if (credentialsProvider != null) {
+ credentialsProvider.close();
+ }
+ if (resources != null) {
+ resources.shutdown();
+ }
+ }
+
+ // T.1.1
+ // Verify authentication using Azure AD with service principals using Redis Cluster Client
+ @Test
+ public void clusterWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException {
+ assumeTrue(cluster != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
+
+ try (StatefulRedisClusterConnection defaultConnection = clusterClient.connect()) {
+ RedisAdvancedClusterCommands sync = defaultConnection.sync();
+ String keyPrefix = UUID.randomUUID().toString();
+ Map mset = prepareMset(keyPrefix);
+
+ assertThat(sync.mset(mset)).isEqualTo("OK");
+
+ for (String mykey : mset.keySet()) {
+ assertThat(defaultConnection.sync().get(mykey)).isEqualTo("value-" + mykey);
+ assertThat(defaultConnection.async().get(mykey).get()).isEqualTo("value-" + mykey);
+ assertThat(defaultConnection.reactive().get(mykey).block()).isEqualTo("value-" + mykey);
+ }
+ assertThat(sync.del(mset.keySet().toArray(new String[0]))).isEqualTo(mset.keySet().size());
+
+ // Test connections to each node
+ defaultConnection.getPartitions().forEach((partition) -> {
+ StatefulRedisConnection, ?> nodeConnection = defaultConnection.getConnection(partition.getNodeId());
+ assertThat(nodeConnection.sync().ping()).isEqualTo("PONG");
+ });
+
+ defaultConnection.getPartitions().forEach((partition) -> {
+ StatefulRedisConnection, ?> nodeConnection = defaultConnection.getConnection(partition.getUri().getHost(),
+ partition.getUri().getPort());
+ assertThat(nodeConnection.sync().ping()).isEqualTo("PONG");
+ });
+ }
+ }
+
+ Map prepareMset(String keyPrefix) {
+ Map mset = new HashMap<>();
+ for (char c = 'a'; c <= 'z'; c++) {
+ String keySuffix = new String(new char[] { c, c, c }); // Generates "aaa", "bbb", etc.
+ String key = String.format("%s-{%s}", keyPrefix, keySuffix);
+ mset.put(key, "value-" + key);
+ }
+ return mset;
+ }
+
+}
diff --git a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java
index a4eba6704f..ba2f08d766 100644
--- a/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java
+++ b/src/test/java/io/lettuce/authx/EntraIdIntegrationTests.java
@@ -9,67 +9,69 @@
import io.lettuce.core.TransactionResult;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.async.RedisAsyncCommands;
+import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.cluster.ClusterClientOptions;
-import io.lettuce.core.cluster.RedisClusterClient;
-import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.support.PubSubTestListener;
import io.lettuce.test.Wait;
+import io.lettuce.test.env.Endpoints;
+import io.lettuce.test.env.Endpoints.Endpoint;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import redis.clients.authentication.core.TokenAuthConfig;
import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder;
import java.time.Duration;
+import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import static io.lettuce.TestTags.ENTRA_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+@Tag(ENTRA_ID)
public class EntraIdIntegrationTests {
- private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;;
-
- private static ClusterClientOptions clientOptions;
+ private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;
private static TokenBasedRedisCredentialsProvider credentialsProvider;
private static RedisClient client;
- private static RedisClusterClient clusterClient;
+ private static Endpoint standalone;
@BeforeAll
public static void setup() {
- Assumptions.assumeTrue(testCtx.host() != null && !testCtx.host().isEmpty(),
- "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
-
- // Configure timeout options to assure fast test failover
- clientOptions = ClusterClientOptions.builder()
- .socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(1)).build())
- .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1)))
- .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build();
-
- TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().clientId(testCtx.getClientId())
- .secret(testCtx.getClientSecret()).authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes())
- .expirationRefreshRatio(0.0000001F).build();
-
- credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig);
-
- RedisURI uri = RedisURI.builder().withHost(testCtx.host()).withPort(testCtx.port())
- .withAuthentication(credentialsProvider).build();
+ standalone = Endpoints.DEFAULT.getEndpoint("standalone-entraid-acl");
+ if (standalone != null) {
+ Assumptions.assumeTrue(testCtx.getClientId() != null && testCtx.getClientSecret() != null,
+ "Skipping EntraID tests. Azure AD credentials not provided!");
+ // Configure timeout options to assure fast test failover
+ ClusterClientOptions clientOptions = ClusterClientOptions.builder()
+ .socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(1)).build())
+ .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1)))
+ // enable re-authentication
+ .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build();
+
+ TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().clientId(testCtx.getClientId())
+ .secret(testCtx.getClientSecret()).authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes())
+ .expirationRefreshRatio(0.0000001F).build();
+
+ credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig);
+
+ RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0)));
+ uri.setCredentialsProvider(credentialsProvider);
+ client = RedisClient.create(uri);
+ client.setOptions(clientOptions);
- client = RedisClient.create(uri);
- client.setOptions(clientOptions);
-
- RedisURI clusterUri = RedisURI.builder().withHost(testCtx.clusterHost().get(0)).withPort(testCtx.clusterPort())
- .withAuthentication(credentialsProvider).build();
- clusterClient = RedisClusterClient.create(clusterUri);
- clusterClient.setOptions(clientOptions);
+ }
}
@AfterAll
@@ -83,28 +85,16 @@ public static void cleanup() {
// Verify authentication using Azure AD with service principals using Redis Standalone client
@Test
public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException {
- try (StatefulRedisConnection connection = client.connect()) {
- assertThat(connection.sync().aclWhoami()).isEqualTo(testCtx.getSpOID());
- assertThat(connection.async().aclWhoami().get()).isEqualTo(testCtx.getSpOID());
- assertThat(connection.reactive().aclWhoami().block()).isEqualTo(testCtx.getSpOID());
- }
- }
-
- // T.1.1
- // Verify authentication using Azure AD with service principals using Redis Cluster Client
- @Test
- public void clusterWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException {
-
- try (StatefulRedisClusterConnection connection = clusterClient.connect()) {
- assertThat(connection.sync().aclWhoami()).isEqualTo(testCtx.getSpOID());
- assertThat(connection.async().aclWhoami().get()).isEqualTo(testCtx.getSpOID());
- assertThat(connection.reactive().aclWhoami().block()).isEqualTo(testCtx.getSpOID());
+ assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
- connection.getPartitions().forEach((partition) -> {
- try (StatefulRedisConnection, ?> nodeConnection = connection.getConnection(partition.getNodeId())) {
- assertThat(nodeConnection.sync().aclWhoami()).isEqualTo(testCtx.getSpOID());
- }
- });
+ try (StatefulRedisConnection connection = client.connect()) {
+ RedisCommands sync = connection.sync();
+ String key = UUID.randomUUID().toString();
+ sync.set(key, "value");
+ assertThat(connection.sync().get(key)).isEqualTo("value");
+ assertThat(connection.async().get(key).get()).isEqualTo("value");
+ assertThat(connection.reactive().get(key).block()).isEqualTo("value");
+ sync.del(key);
}
}
@@ -112,6 +102,7 @@ public void clusterWithSecret_azureServicePrincipalIntegrationTest() throws Exec
// Test that the Redis client is not blocked/interrupted during token renewal.
@Test
public void renewalDuringOperationsTest() throws InterruptedException {
+ assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
// Counter to track the number of command cycles
AtomicInteger commandCycleCount = new AtomicInteger(0);
@@ -162,6 +153,8 @@ public void renewalDuringOperationsTest() throws InterruptedException {
// Test basic Pub/Sub functionality is not blocked/interrupted during token renewal.
@Test
public void renewalDuringPubSubOperationsTest() throws InterruptedException {
+ assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
+
try (StatefulRedisPubSubConnection connectionPubSub = client.connectPubSub();
StatefulRedisPubSubConnection connectionPubSub1 = client.connectPubSub()) {
@@ -183,7 +176,7 @@ public void renewalDuringPubSubOperationsTest() throws InterruptedException {
latch.countDown();
});
- assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue(); // Wait for at least 10 token renewals
+ assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue(); // Wait for at least 10 token renewals
pubsubThread.join(); // Wait for the pub/sub thread to finish
// Verify that all messages were received
diff --git a/src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java b/src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java
new file mode 100644
index 0000000000..16cfea4a6d
--- /dev/null
+++ b/src/test/java/io/lettuce/authx/EntraIdManagedIdentityIntegrationTests.java
@@ -0,0 +1,105 @@
+package io.lettuce.authx;
+
+import io.lettuce.core.ClientOptions;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.api.sync.RedisCommands;
+import io.lettuce.core.cluster.ClusterClientOptions;
+import io.lettuce.test.env.Endpoints;
+import io.lettuce.test.env.Endpoints.Endpoint;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import redis.clients.authentication.core.TokenAuthConfig;
+import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder;
+import redis.clients.authentication.entraid.ManagedIdentityInfo;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+import static io.lettuce.TestTags.ENTRA_ID;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+@Tag(ENTRA_ID)
+public class EntraIdManagedIdentityIntegrationTests {
+
+ private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;
+
+ private static RedisClient client;
+
+ private static Endpoint standalone;
+
+ private static Set managedIdentityAudience = Collections.singleton("https://redis.azure.com");
+
+ @BeforeAll
+ public static void setup() {
+ standalone = Endpoints.DEFAULT.getEndpoint("standalone-entraid-acl");
+ assumeTrue(standalone != null, "Skipping test because no Redis endpoint is configured!");
+ }
+
+ @Test
+ public void withUserAssignedId_azureManagedIdentityIntegrationTest() throws ExecutionException, InterruptedException {
+
+ // enable re-authentication
+ ClusterClientOptions clientOptions = ClusterClientOptions.builder()
+ .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build();
+
+ TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder()
+ .userAssignedManagedIdentity(ManagedIdentityInfo.UserManagedIdentityType.OBJECT_ID,
+ testCtx.getUserAssignedManagedIdentity())
+ .scopes(managedIdentityAudience).build();
+
+ try (TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider
+ .create(tokenAuthConfig)) {
+
+ RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0)));
+ uri.setCredentialsProvider(credentialsProvider);
+ client = RedisClient.create(uri);
+ client.setOptions(clientOptions);
+
+ try (StatefulRedisConnection connection = client.connect()) {
+ RedisCommands sync = connection.sync();
+ String key = UUID.randomUUID().toString();
+ sync.set(key, "value");
+ assertThat(connection.sync().get(key)).isEqualTo("value");
+ assertThat(connection.async().get(key).get()).isEqualTo("value");
+ assertThat(connection.reactive().get(key).block()).isEqualTo("value");
+ sync.del(key);
+ }
+ }
+ }
+
+ @Test
+ public void withSystemAssignedId_azureManagedIdentityIntegrationTest() throws ExecutionException, InterruptedException {
+ // enable re-authentication
+ ClusterClientOptions clientOptions = ClusterClientOptions.builder()
+ .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build();
+
+ TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().systemAssignedManagedIdentity()
+ .scopes(managedIdentityAudience).build();
+
+ try (TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider
+ .create(tokenAuthConfig)) {
+
+ RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0)));
+ uri.setCredentialsProvider(credentialsProvider);
+ client = RedisClient.create(uri);
+ client.setOptions(clientOptions);
+
+ try (StatefulRedisConnection connection = client.connect()) {
+ RedisCommands sync = connection.sync();
+ String key = UUID.randomUUID().toString();
+ sync.set(key, "value");
+ assertThat(connection.sync().get(key)).isEqualTo("value");
+ assertThat(connection.async().get(key).get()).isEqualTo("value");
+ assertThat(connection.reactive().get(key).block()).isEqualTo("value");
+ sync.del(key);
+ }
+ }
+ }
+
+}
diff --git a/src/test/java/io/lettuce/authx/EntraIdTestContext.java b/src/test/java/io/lettuce/authx/EntraIdTestContext.java
index 7abfac0fe8..551dc03582 100644
--- a/src/test/java/io/lettuce/authx/EntraIdTestContext.java
+++ b/src/test/java/io/lettuce/authx/EntraIdTestContext.java
@@ -1,10 +1,7 @@
package io.lettuce.authx;
-import io.github.cdimascio.dotenv.Dotenv;
-
import java.util.Arrays;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
public class EntraIdTestContext {
@@ -13,21 +10,11 @@ public class EntraIdTestContext {
private static final String AZURE_CLIENT_SECRET = "AZURE_CLIENT_SECRET";
- private static final String AZURE_SP_OID = "AZURE_SP_OID";
-
private static final String AZURE_AUTHORITY = "AZURE_AUTHORITY";
private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES";
- private static final String REDIS_AZURE_HOST = "REDIS_AZURE_HOST";
-
- private static final String REDIS_AZURE_PORT = "REDIS_AZURE_PORT";
-
- private static final String REDIS_AZURE_CLUSTER_HOST = "REDIS_AZURE_CLUSTER_HOST";
-
- private static final String REDIS_AZURE_CLUSTER_PORT = "REDIS_AZURE_CLUSTER_PORT";
-
- private static final String REDIS_AZURE_DB = "REDIS_AZURE_DB";
+ private static final String AZURE_USER_ASSIGNED_MANAGED_ID = "AZURE_USER_ASSIGNED_MANAGED_ID";
private final String clientId;
@@ -35,67 +22,32 @@ public class EntraIdTestContext {
private final String clientSecret;
- private final String spOID;
-
- private final Set redisScopes;
+ private Set redisScopes;
- private final String redisHost;
-
- private final int redisPort;
-
- private final List redisClusterHost;
-
- private final int redisClusterPort;
-
- private static Dotenv dotenv;
- static {
- dotenv = Dotenv.configure().directory("src/test/resources").filename(".env.entraid").load();
- }
+ private String userAssignedManagedIdentity;
public static final EntraIdTestContext DEFAULT = new EntraIdTestContext();
private EntraIdTestContext() {
- // Using Dotenv directly here
- clientId = dotenv.get(AZURE_CLIENT_ID, "");
- clientSecret = dotenv.get(AZURE_CLIENT_SECRET, "");
- spOID = dotenv.get(AZURE_SP_OID, "");
- authority = dotenv.get(AZURE_AUTHORITY, "https://login.microsoftonline.com/your-tenant-id");
- redisHost = dotenv.get(REDIS_AZURE_HOST);
- redisPort = Integer.parseInt(dotenv.get(REDIS_AZURE_PORT, "6379"));
- redisClusterHost = Arrays.asList(dotenv.get(REDIS_AZURE_CLUSTER_HOST, "").split(","));
- redisClusterPort = Integer.parseInt(dotenv.get(REDIS_AZURE_CLUSTER_PORT, "6379"));
- String redisScopesEnv = dotenv.get(AZURE_REDIS_SCOPES, "https://redis.azure.com/.default");
- if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) {
- this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";")));
- } else {
- this.redisScopes = new HashSet<>();
- }
- }
-
- public String host() {
- return redisHost;
- }
-
- public int port() {
- return redisPort;
- }
-
- public List clusterHost() {
- return redisClusterHost;
+ clientId = System.getenv(AZURE_CLIENT_ID);
+ authority = System.getenv(AZURE_AUTHORITY);
+ clientSecret = System.getenv(AZURE_CLIENT_SECRET);
+ this.userAssignedManagedIdentity = System.getenv(AZURE_USER_ASSIGNED_MANAGED_ID);
}
- public int clusterPort() {
- return redisClusterPort;
+ public EntraIdTestContext(String clientId, String authority, String clientSecret, Set redisScopes,
+ String userAssignedManagedIdentity) {
+ this.clientId = clientId;
+ this.authority = authority;
+ this.clientSecret = clientSecret;
+ this.redisScopes = redisScopes;
+ this.userAssignedManagedIdentity = userAssignedManagedIdentity;
}
public String getClientId() {
return clientId;
}
- public String getSpOID() {
- return spOID;
- }
-
public String getAuthority() {
return authority;
}
@@ -105,7 +57,15 @@ public String getClientSecret() {
}
public Set getRedisScopes() {
+ if (redisScopes == null) {
+ String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES);
+ this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";")));
+ }
return redisScopes;
}
+ public String getUserAssignedManagedIdentity() {
+ return userAssignedManagedIdentity;
+ }
+
}
diff --git a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java
index bb2d0f91a0..49811ea5c8 100644
--- a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java
+++ b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java
@@ -40,7 +40,7 @@ void testDefault() {
assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.SET, null))).isFalse();
assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.PUBLISH, null))).isFalse();
assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.GET, null))).isTrue();
- assertThat(options.getJsonParser().block()).isInstanceOf(DefaultJsonParser.class);
+ assertThat(options.getJsonParser().get()).isInstanceOf(DefaultJsonParser.class);
}
@Test
@@ -66,8 +66,8 @@ void testCopy() {
@Test
void jsonParser() {
JsonParser parser = new CustomJsonParser();
- ClientOptions options = ClientOptions.builder().jsonParser(Mono.justOrEmpty(parser)).build();
- assertThat(options.getJsonParser().block()).isInstanceOf(CustomJsonParser.class);
+ ClientOptions options = ClientOptions.builder().jsonParser(() -> parser).build();
+ assertThat(options.getJsonParser().get()).isInstanceOf(CustomJsonParser.class);
}
static class CustomJsonParser implements JsonParser {
diff --git a/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java b/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java
index 5eda44ffd9..caa258f744 100644
--- a/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java
@@ -26,22 +26,23 @@ public class RedisContainerIntegrationTests {
private static final String REDIS_STACK_CLUSTER = "clustered-stack";
- private static final String REDIS_STACK_VERSION = System.getProperty("REDIS_STACK_VERSION", "8.0-M02");;
+ private static final String REDIS_STACK_VERSION = System.getProperty("REDIS_STACK_VERSION");
private static Exception initializationException;
public static ComposeContainer CLUSTERED_STACK = new ComposeContainer(
new File("src/test/resources/docker/docker-compose.yml")).withExposedService(REDIS_STACK_CLUSTER, 36379)
.withExposedService(REDIS_STACK_CLUSTER, 36380).withExposedService(REDIS_STACK_CLUSTER, 36381)
- .withExposedService(REDIS_STACK_STANDALONE, 6379)
- .withEnv("CLIENT_LIBS_TEST_IMAGE", "redislabs/client-libs-test")
- .withEnv("REDIS_STACK_VERSION", REDIS_STACK_VERSION).withPull(false).withLocalCompose(true);
+ .withExposedService(REDIS_STACK_STANDALONE, 6379).withPull(false).withLocalCompose(true);
// Singleton container pattern - start the containers only once
// See https://java.testcontainers.org/test_framework_integration/manual_lifecycle_control/#singleton-containers
static {
int attempts = 0;
+ if (REDIS_STACK_VERSION != null && !REDIS_STACK_VERSION.isEmpty()) {
+ CLUSTERED_STACK.withEnv("REDIS_VERSION", REDIS_STACK_VERSION);
+ }
// In case you need to debug the container uncomment these lines to redirect the output
CLUSTERED_STACK.withLogConsumer(REDIS_STACK_CLUSTER, (OutputFrame frame) -> LOGGER.debug(frame.getUtf8String()));
CLUSTERED_STACK.withLogConsumer(REDIS_STACK_STANDALONE, (OutputFrame frame) -> LOGGER.debug(frame.getUtf8String()));
diff --git a/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java
index 4106a711c2..22f3aed345 100644
--- a/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java
+++ b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java
@@ -49,7 +49,7 @@ class RedisJsonCommandBuilderUnitTests {
public static final JsonPath MY_PATH = JsonPath.of("$..commuter_bikes");
- RedisJsonCommandBuilder builder = new RedisJsonCommandBuilder<>(StringCodec.UTF8, Mono.just(PARSER));
+ RedisJsonCommandBuilder builder = new RedisJsonCommandBuilder<>(StringCodec.UTF8, () -> PARSER);
@Test
void shouldCorrectlyConstructJsonArrappend() {
diff --git a/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java b/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java
index da3e121450..2b12d74437 100644
--- a/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java
@@ -22,6 +22,7 @@
import static io.lettuce.TestTags.INTEGRATION_TEST;
import static org.assertj.core.api.AssertionsForClassTypes.*;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.util.ArrayList;
import java.util.List;
@@ -30,6 +31,7 @@
import javax.inject.Inject;
+import io.lettuce.test.condition.RedisConditions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -161,6 +163,8 @@ void hashSinglePass() {
@Test
void hashNoValuesSinglePass() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
redis.hmset(key, KeysAndValues.MAP);
@@ -194,6 +198,8 @@ void hashMultiPass() {
@Test
void hashNoValuesMultiPass() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
redis.hmset(key, KeysAndValues.MAP);
diff --git a/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java b/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java
index dfa4078db9..54172130b2 100644
--- a/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java
@@ -21,12 +21,14 @@
import static io.lettuce.TestTags.INTEGRATION_TEST;
import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.util.List;
import java.util.stream.IntStream;
import javax.inject.Inject;
+import io.lettuce.test.condition.RedisConditions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -95,6 +97,8 @@ void shouldHscanIteratively() {
@Test
void shouldHscanNovaluesIteratively() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
for (int i = 0; i < 1000; i++) {
redis.hset(key, "field-" + i, "value-" + i);
@@ -160,6 +164,8 @@ void shouldCorrectlyEmitItemsWithConcurrentPoll() {
@Test
void shouldCorrectlyEmitKeysWithConcurrentPoll() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
RedisReactiveCommands commands = connection.reactive();
diff --git a/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java
index 0683535170..8a5ca15d95 100644
--- a/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java
@@ -22,6 +22,7 @@
import static io.lettuce.TestTags.INTEGRATION_TEST;
import static org.assertj.core.api.AssertionsForClassTypes.*;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.util.ArrayList;
import java.util.List;
@@ -30,6 +31,7 @@
import javax.inject.Inject;
+import io.lettuce.test.condition.RedisConditions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -185,6 +187,8 @@ void hashSinglePass() {
@Test
void hashNovaluesSinglePass() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
redis.hmset(key, KeysAndValues.MAP);
@@ -215,6 +219,8 @@ void hashMultiPass() {
@Test
void hashNovaluesMultiPass() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
redis.hmset(key, KeysAndValues.MAP);
diff --git a/src/test/java/io/lettuce/core/commands/ConsolidatedAclCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ConsolidatedAclCommandIntegrationTests.java
new file mode 100644
index 0000000000..f0519af54e
--- /dev/null
+++ b/src/test/java/io/lettuce/core/commands/ConsolidatedAclCommandIntegrationTests.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2011-Present, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ *
+ * This file contains contributions from third-party contributors
+ * licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.lettuce.core.commands;
+
+import io.lettuce.core.*;
+import io.lettuce.core.api.sync.RedisCommands;
+
+import io.lettuce.test.condition.RedisConditions;
+import org.junit.jupiter.api.*;
+
+import java.util.Arrays;
+
+import static io.lettuce.TestTags.INTEGRATION_TEST;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * Integration tests for ACL commands with Redis modules since Redis 8.0.
+ *
+ * @author M Sazzadul Hoque
+ */
+@Tag(INTEGRATION_TEST)
+public class ConsolidatedAclCommandIntegrationTests extends RedisContainerIntegrationTests {
+
+ private static RedisClient client;
+
+ private static RedisCommands redis;
+
+ @BeforeAll
+ public static void setup() {
+ RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build();
+
+ client = RedisClient.create(redisURI);
+ redis = client.connect().sync();
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.9"));
+ }
+
+ @AfterAll
+ static void teardown() {
+ if (client != null) {
+ client.shutdown();
+ }
+ }
+
+ @BeforeEach
+ void setUp() {
+ redis.flushall();
+ redis.aclUsers().stream().filter(o -> !"default".equals(o)).forEach(redis::aclDeluser);
+ redis.aclLogReset();
+ }
+
+ @Test
+ public void listACLCategoriesTest() {
+ assertThat(redis.aclCat()).containsAll(Arrays.asList(AclCategory.BLOOM, AclCategory.CUCKOO, AclCategory.CMS,
+ AclCategory.TOPK, AclCategory.TDIGEST, AclCategory.SEARCH, AclCategory.TIMESERIES, AclCategory.JSON));
+ }
+
+ @Test
+ void grantBloomCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.BLOOM, "bloom");
+ }
+
+ @Test
+ void grantCuckooCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.CUCKOO, "cuckoo");
+ }
+
+ @Test
+ void grantCmsCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.CMS, "cms");
+ }
+
+ @Test
+ void grantTopkCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.TOPK, "topk");
+ }
+
+ @Test
+ void grantTdigestCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.TDIGEST, "tdigest");
+ }
+
+ @Test
+ void grantSearchCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.SEARCH, "search");
+ }
+
+ @Test
+ void grantTimeseriesCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.TIMESERIES, "timeseries");
+ }
+
+ @Test
+ void grantJsonCommandCatTest() {
+ grantModuleCommandCatTest(AclCategory.JSON, "json");
+ }
+
+ private void grantModuleCommandCatTest(AclCategory category, String categoryStr) {
+ assertThat(redis.aclDeluser("foo")).isNotNull();
+ AclSetuserArgs args = AclSetuserArgs.Builder.on().addCategory(category);
+ assertThat(redis.aclSetuser("foo", args)).isEqualTo("OK");
+ assertThat(redis.aclGetuser("foo")).contains("-@all +@" + categoryStr);
+ assertThat(redis.aclDeluser("foo")).isNotNull();
+ }
+
+}
diff --git a/src/test/java/io/lettuce/core/commands/ConsolidatedConfigurationCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ConsolidatedConfigurationCommandIntegrationTests.java
new file mode 100644
index 0000000000..3a009fc47e
--- /dev/null
+++ b/src/test/java/io/lettuce/core/commands/ConsolidatedConfigurationCommandIntegrationTests.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2011-Present, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ *
+ * This file contains contributions from third-party contributors
+ * licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.lettuce.core.commands;
+
+import static io.lettuce.TestTags.INTEGRATION_TEST;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import io.lettuce.core.*;
+import io.lettuce.core.api.sync.RedisCommands;
+import io.lettuce.test.condition.RedisConditions;
+
+import org.junit.jupiter.api.*;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Integration tests for {@link io.lettuce.core.api.sync.RedisServerCommands} with Redis modules since Redis 8.0.
+ *
+ * @author M Sazzadul Hoque
+ */
+@Tag(INTEGRATION_TEST)
+public class ConsolidatedConfigurationCommandIntegrationTests extends RedisContainerIntegrationTests {
+
+ private static RedisClient client;
+
+ private static RedisCommands redis;
+
+ @BeforeAll
+ public static void setup() {
+ RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build();
+
+ client = RedisClient.create(redisURI);
+ redis = client.connect().sync();
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.9"));
+ }
+
+ @AfterAll
+ static void teardown() {
+ if (client != null) {
+ client.shutdown();
+ }
+ }
+
+ @BeforeEach
+ void setUp() {
+ redis.flushall();
+ }
+
+ @Test
+ public void setSearchConfigGloballyTest() {
+ final String configParam = "search-default-dialect";
+ // confirm default
+ assertThat(redis.configGet(configParam)).isEqualTo(Collections.singletonMap(configParam, "1"));
+
+ try {
+ assertThat(redis.configSet(configParam, "2")).isEqualTo("OK");
+ assertThat(redis.configGet(configParam)).isEqualTo(Collections.singletonMap(configParam, "2"));
+ } finally {
+ // restore to default
+ assertThat(redis.configSet(configParam, "1")).isEqualTo("OK");
+ }
+ }
+
+ @Test
+ public void setReadOnlySearchConfigTest() {
+ assertThatThrownBy(() -> redis.configSet("search-max-doctablesize", "10"))
+ .isInstanceOf(RedisCommandExecutionException.class);
+ }
+
+ @Test
+ public void getSearchConfigSettingTest() {
+ assertThat(redis.configGet("search-timeout")).hasSize(1);
+ }
+
+ @Test
+ public void getTSConfigSettingTest() {
+ assertThat(redis.configGet("ts-retention-policy")).hasSize(1);
+ }
+
+ @Test
+ public void getBFConfigSettingTest() {
+ assertThat(redis.configGet("bf-error-rate")).hasSize(1);
+ }
+
+ @Test
+ public void getCFConfigSettingTest() {
+ assertThat(redis.configGet("cf-initial-size")).hasSize(1);
+ }
+
+ @Test
+ public void getAllConfigSettings() {
+ assertThat(redis.configGet("*").keySet()).contains("search-default-dialect", "search-timeout", "ts-retention-policy",
+ "bf-error-rate", "cf-initial-size");
+ }
+
+}
diff --git a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java
index 45ee95eb81..149f61e6a9 100644
--- a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java
@@ -32,6 +32,7 @@
import io.lettuce.test.LettuceExtension;
import io.lettuce.test.ListStreamingAdapter;
import io.lettuce.test.condition.EnabledOnCommand;
+import io.lettuce.test.condition.RedisConditions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -54,6 +55,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset;
import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* Integration tests for {@link io.lettuce.core.api.sync.RedisHashCommands}.
@@ -340,6 +342,9 @@ void hscan() {
@Test
void hscanNovalues() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.hset(key, key, value);
KeyScanCursor cursor = redis.hscanNovalues(key);
@@ -361,6 +366,9 @@ void hscanWithCursor() {
@Test
void hscanNoValuesWithCursor() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.hset(key, key, value);
KeyScanCursor cursor = redis.hscanNovalues(key, ScanCursor.INITIAL);
@@ -383,6 +391,9 @@ void hscanWithCursorAndArgs() {
@Test
void hscanNoValuesWithCursorAndArgs() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.hset(key, key, value);
KeyScanCursor cursor = redis.hscanNovalues(key, ScanCursor.INITIAL, ScanArgs.Builder.limit(2));
@@ -407,6 +418,9 @@ void hscanStreaming() {
@Test
void hscanNoValuesStreaming() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.hset(key, key, value);
ListStreamingAdapter adapter = new ListStreamingAdapter<>();
@@ -432,6 +446,9 @@ void hscanStreamingWithCursor() {
@Test
void hscanNoValuesStreamingWithCursor() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.hset(key, key, value);
ListStreamingAdapter adapter = new ListStreamingAdapter<>();
@@ -457,6 +474,9 @@ void hscanStreamingWithCursorAndArgs() {
@Test
void hscanNoValuesStreamingWithCursorAndArgs() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.hset(key, key, value);
ListStreamingAdapter adapter = new ListStreamingAdapter<>();
@@ -482,6 +502,9 @@ void hscanStreamingWithArgs() {
@Test
void hscanNoValuesStreamingWithArgs() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.hset(key, key, value);
ListStreamingAdapter adapter = new ListStreamingAdapter<>();
@@ -520,6 +543,8 @@ void hscanMultiple() {
@Test
void hscanNoValuesMultiple() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
Map expect = new LinkedHashMap<>();
setup100KeyValues(expect);
@@ -558,6 +583,8 @@ void hscanMatch() {
@Test
void hscanNoValuesMatch() {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
Map expect = new LinkedHashMap<>();
setup100KeyValues(expect);
diff --git a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
index d523a88555..09bb67a504 100644
--- a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
@@ -398,6 +398,11 @@ void configGetMultipleParameters() {
.containsEntry("hash-max-listpack-entries", "512");
}
+ @Test
+ public void getAllConfigSettings() {
+ assertThat(redis.configGet("*")).isNotEmpty();
+ }
+
@Test
void configResetstat() {
redis.get(key);
diff --git a/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java
index 1816d3f480..57d613f823 100644
--- a/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java
@@ -30,6 +30,7 @@
import io.lettuce.core.protocol.CommandArgs;
import io.lettuce.test.LettuceExtension;
import io.lettuce.test.condition.EnabledOnCommand;
+import io.lettuce.test.condition.RedisConditions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -49,6 +50,7 @@
import static io.lettuce.TestTags.INTEGRATION_TEST;
import static io.lettuce.core.protocol.CommandType.XINFO;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* Integration tests for {@link io.lettuce.core.api.sync.RedisStreamCommands}.
@@ -340,6 +342,9 @@ public void xreadTransactional() {
@Test
public void xreadLastVsLatest() {
+ // Redis 7.4 - you can use the + sign as a special ID to read the last message in the stream.
+ assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4"));
+
redis.xadd("stream-1", Collections.singletonMap("key1", "value1"));
redis.xadd("stream-1", Collections.singletonMap("key2", "value2"));
diff --git a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java
index 76fdd0661f..b8d4ac924c 100644
--- a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java
@@ -601,7 +601,7 @@ void withCustomParser() {
RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build();
try (RedisClient client = RedisClient.create(redisURI)) {
- client.setOptions(ClientOptions.builder().jsonParser(Mono.just(new CustomParser())).build());
+ client.setOptions(ClientOptions.builder().jsonParser(CustomParser::new).build());
StatefulRedisConnection connection = client.connect(StringCodec.UTF8);
RedisCommands redis = connection.sync();
assertThat(redis.getJsonParser()).isInstanceOf(CustomParser.class);
diff --git a/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java b/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java
index 82772e35c1..7e6fea08f7 100644
--- a/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java
+++ b/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java
@@ -19,22 +19,27 @@
@Tag(UNIT_TEST)
class CommonsPool2ConfigConverterUnitTests {
+ private static final int MIN_IDLE_EXPECTED = 2;
+
+ private static final int MAX_IDLE_EXPECTED = 12;
+
+ private static final int MAX_TOTAL_EXPECTED = 13;
+
@Test
void shouldAdaptConfiguration() {
-
GenericObjectPoolConfig config = new GenericObjectPoolConfig<>();
- config.setMinIdle(2);
- config.setMaxIdle(12);
- config.setMaxTotal(13);
+ config.setMinIdle(MIN_IDLE_EXPECTED);
+ config.setMaxIdle(MAX_IDLE_EXPECTED);
+ config.setMaxTotal(MAX_TOTAL_EXPECTED);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
config.setTestOnCreate(true);
BoundedPoolConfig result = CommonsPool2ConfigConverter.bounded(config);
- assertThat(result.getMinIdle()).isEqualTo(2);
- assertThat(result.getMaxIdle()).isEqualTo(12);
- assertThat(result.getMaxTotal()).isEqualTo(13);
+ assertThat(result.getMinIdle()).isEqualTo(MIN_IDLE_EXPECTED);
+ assertThat(result.getMaxIdle()).isEqualTo(MAX_IDLE_EXPECTED);
+ assertThat(result.getMaxTotal()).isEqualTo(MAX_TOTAL_EXPECTED);
assertThat(result.isTestOnAcquire()).isTrue();
assertThat(result.isTestOnCreate()).isTrue();
assertThat(result.isTestOnRelease()).isTrue();
diff --git a/src/test/java/io/lettuce/test/env/Endpoints.java b/src/test/java/io/lettuce/test/env/Endpoints.java
new file mode 100644
index 0000000000..0631713cd9
--- /dev/null
+++ b/src/test/java/io/lettuce/test/env/Endpoints.java
@@ -0,0 +1,235 @@
+package io.lettuce.test.env;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+
+public class Endpoints {
+
+ private static final Logger log = LoggerFactory.getLogger(Endpoints.class);
+
+ private Map endpoints;
+
+ public static final Endpoints DEFAULT;
+
+ static {
+ String filePath = System.getenv("REDIS_ENDPOINTS_CONFIG_PATH");
+ if (filePath == null || filePath.isEmpty()) {
+ log.info("REDIS_ENDPOINTS_CONFIG_PATH environment variable is not set. No Endpoints configuration will be loaded.");
+ DEFAULT = new Endpoints(Collections.emptyMap());
+ } else {
+ DEFAULT = fromFile(filePath);
+ }
+ }
+
+ private Endpoints(Map endpoints) {
+ this.endpoints = endpoints;
+ }
+
+ /**
+ * Factory method to create an Endpoints instance from a file.
+ *
+ * @param filePath Path to the JSON file.
+ * @return Populated Endpoints instance.
+ */
+ public static Endpoints fromFile(String filePath) {
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ File file = new File(filePath);
+
+ HashMap endpoints = objectMapper.readValue(file,
+ objectMapper.getTypeFactory().constructMapType(HashMap.class, String.class, Endpoint.class));
+ return new Endpoints(endpoints);
+
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load Endpoints from file: " + filePath, e);
+ }
+ }
+
+ /**
+ * Get an endpoint by name.
+ *
+ * @param name the name of the endpoint.
+ * @return the corresponding Endpoint or {@code null} if not found.
+ */
+ public Endpoint getEndpoint(String name) {
+ return endpoints != null ? endpoints.get(name) : null;
+ }
+
+ public Map getEndpoints() {
+ return endpoints;
+ }
+
+ public void setEndpoints(Map endpoints) {
+ this.endpoints = endpoints;
+ }
+
+ // Inner classes for Endpoint and RawEndpoint
+ public static class Endpoint {
+
+ @JsonProperty("bdb_id")
+ private int bdbId;
+
+ private String username;
+
+ private String password;
+
+ private boolean tls;
+
+ @JsonProperty("raw_endpoints")
+ private List rawEndpoints;
+
+ private List endpoints;
+
+ // Getters and Setters
+ public int getBdbId() {
+ return bdbId;
+ }
+
+ public void setBdbId(int bdbId) {
+ this.bdbId = bdbId;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public boolean isTls() {
+ return tls;
+ }
+
+ public void setTls(boolean tls) {
+ this.tls = tls;
+ }
+
+ public List getRawEndpoints() {
+ return rawEndpoints;
+ }
+
+ public void setRawEndpoints(List rawEndpoints) {
+ this.rawEndpoints = rawEndpoints;
+ }
+
+ public List getEndpoints() {
+ return endpoints;
+ }
+
+ public void setEndpoints(List endpoints) {
+ this.endpoints = endpoints;
+ }
+
+ }
+
+ public static class RawEndpoint {
+
+ private List addr;
+
+ @JsonProperty("addr_type")
+ private String addrType;
+
+ @JsonProperty("dns_name")
+ private String dnsName;
+
+ @JsonProperty("oss_cluster_api_preferred_endpoint_type")
+ private String preferredEndpointType;
+
+ @JsonProperty("oss_cluster_api_preferred_ip_type")
+ private String preferredIpType;
+
+ private int port;
+
+ @JsonProperty("proxy_policy")
+ private String proxyPolicy;
+
+ private String uid;
+
+ // Getters and Setters
+ public List getAddr() {
+ return addr;
+ }
+
+ public void setAddr(List addr) {
+ this.addr = addr;
+ }
+
+ public String getAddrType() {
+ return addrType;
+ }
+
+ public void setAddrType(String addrType) {
+ this.addrType = addrType;
+ }
+
+ public String getDnsName() {
+ return dnsName;
+ }
+
+ public void setDnsName(String dnsName) {
+ this.dnsName = dnsName;
+ }
+
+ public String getPreferredEndpointType() {
+ return preferredEndpointType;
+ }
+
+ public void setPreferredEndpointType(String preferredEndpointType) {
+ this.preferredEndpointType = preferredEndpointType;
+ }
+
+ public String getPreferredIpType() {
+ return preferredIpType;
+ }
+
+ public void setPreferredIpType(String preferredIpType) {
+ this.preferredIpType = preferredIpType;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public String getProxyPolicy() {
+ return proxyPolicy;
+ }
+
+ public void setProxyPolicy(String proxyPolicy) {
+ this.proxyPolicy = proxyPolicy;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ public void setUid(String uid) {
+ this.uid = uid;
+ }
+
+ }
+
+}
diff --git a/src/test/java/io/redis/examples/async/StringExample.java b/src/test/java/io/redis/examples/async/StringExample.java
index 9be29395b2..af856743e7 100644
--- a/src/test/java/io/redis/examples/async/StringExample.java
+++ b/src/test/java/io/redis/examples/async/StringExample.java
@@ -7,15 +7,12 @@
// REMOVE_START
import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
// REMOVE_END
import java.util.*;
import java.util.concurrent.CompletableFuture;
-// REMOVE_START
-import static org.assertj.core.api.Assertions.assertThat;
-// REMOVE_END
-
public class StringExample {
// REMOVE_START
@@ -29,7 +26,7 @@ public void run() {
// STEP_START set_get
CompletableFuture setAndGet = asyncCommands.set("bike:1", "Deimos").thenCompose(v -> {
- System.out.println(v); // OK
+ System.out.println(v); // >>> OK
// REMOVE_START
assertThat(v).isEqualTo("OK");
// REMOVE_END
@@ -41,13 +38,16 @@ public void run() {
return res;
})
// REMOVE_END
- .thenAccept(System.out::println) // Deimos
+ .thenAccept(System.out::println) // >>> Deimos
.toCompletableFuture();
// STEP_END
+ // HIDE_START
+ setAndGet.join();
+ // HIDE_END
// STEP_START setnx_xx
CompletableFuture setnx = asyncCommands.setnx("bike:1", "bike").thenCompose(v -> {
- System.out.println(v); // false (because key already exists)
+ System.out.println(v); // >>> false (because key already exists)
// REMOVE_START
assertThat(v).isFalse();
// REMOVE_END
@@ -59,8 +59,11 @@ public void run() {
return res;
})
// REMOVE_END
- .thenAccept(System.out::println) // Deimos (value is unchanged)
+ .thenAccept(System.out::println) // >>> Deimos (value is unchanged)
.toCompletableFuture();
+ // HIDE_START
+ setnx.join();
+ // HIDE_END
// set the value to "bike" if it already exists
CompletableFuture setxx = asyncCommands.set("bike:1", "bike", SetArgs.Builder.xx())
@@ -70,8 +73,11 @@ public void run() {
return res;
})
// REMOVE_END
- .thenAccept(System.out::println) // OK
+ .thenAccept(System.out::println) // >>> OK
.toCompletableFuture();
+ // HIDE_START
+ setxx.join();
+ // HIDE_END
// STEP_END
// STEP_START mset
@@ -81,7 +87,7 @@ public void run() {
bikeMap.put("bike:3", "Vanth");
CompletableFuture mset = asyncCommands.mset(bikeMap).thenCompose(v -> {
- System.out.println(v); // OK
+ System.out.println(v); // >>> OK
return asyncCommands.mget("bike:1", "bike:2", "bike:3");
})
// REMOVE_START
@@ -93,15 +99,19 @@ public void run() {
return res;
})
// REMOVE_END
- .thenAccept(System.out::println) // [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3,
- // Vanth]]
+ .thenAccept(System.out::println)
+ // >>> [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3,
+ // Vanth]]
.toCompletableFuture();
// STEP_END
+ // HIDE_START
+ mset.join();
+ // HIDE_END
// STEP_START incr
CompletableFuture incrby = asyncCommands.set("total_crashes", "0")
.thenCompose(v -> asyncCommands.incr("total_crashes")).thenCompose(v -> {
- System.out.println(v); // 1
+ System.out.println(v); // >>> 1
// REMOVE_START
assertThat(v).isEqualTo(1L);
// REMOVE_END
@@ -113,12 +123,12 @@ public void run() {
return res;
})
// REMOVE_END
- .thenAccept(System.out::println) // 11
+ .thenAccept(System.out::println) // >>> 11
.toCompletableFuture();
// STEP_END
-
- CompletableFuture.allOf(setAndGet, setnx, setxx, mset, incrby).join();
-
+ // HIDE_START
+ incrby.join();
+ // HIDE_END
} finally {
redisClient.shutdown();
}
diff --git a/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt b/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt
index ad83fd5fc1..464b8253cf 100644
--- a/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt
+++ b/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt
@@ -23,11 +23,13 @@ import io.lettuce.TestTags
import io.lettuce.core.api.StatefulRedisConnection
import io.lettuce.core.api.coroutines
import io.lettuce.test.LettuceExtension
+import io.lettuce.test.condition.RedisConditions
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
@@ -78,6 +80,9 @@ internal class ScanFlowIntegrationTests @Inject constructor(private val connecti
@Test
fun `should hscanNovalues iteratively`() = runBlocking {
+ // NOVALUES flag (since Redis 7.4)
+ assumeTrue(RedisConditions.of(connection).hasVersionGreaterOrEqualsTo("7.4"));
+
with(connection.coroutines()) {
repeat(iterations) {
hset(key, "field-$it", "value-$it")
diff --git a/src/test/resources/docker/docker-compose.yml b/src/test/resources/docker/docker-compose.yml
index 97b1777373..2cc05fa3dd 100644
--- a/src/test/resources/docker/docker-compose.yml
+++ b/src/test/resources/docker/docker-compose.yml
@@ -1,8 +1,11 @@
---
+x-client-libs-stack-image: &client-libs-stack-image
+ image: "redislabs/client-libs-test:${REDIS_STACK_VERSION:-8.0-M02}"
+
services:
standalone-stack:
- image: "${CLIENT_LIBS_TEST_IMAGE}:${REDIS_STACK_VERSION}"
+ <<: *client-libs-stack-image
environment:
- REDIS_CLUSTER=no
- PORT=6379
@@ -10,7 +13,7 @@ services:
- "16379:6379"
clustered-stack:
- image: "${CLIENT_LIBS_TEST_IMAGE}:${REDIS_STACK_VERSION}"
+ <<: *client-libs-stack-image
environment:
- REDIS_CLUSTER=yes
- PORT=36379