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