From 8e21d2e8048f912e258931e01f8f3bf9e09306f7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Mar 2019 10:04:43 +0100 Subject: [PATCH] Add support for Redis Sentinel authentication #1002 Redis now supports authentication against Redis Sentinel that was introduced with Redis 5.0.1. The password can be only set programmatically as URI-based user-info applies to the actual Redis server. RedisURI sentinelUrl = RedisURI.Builder.sentinel("host", 26379, "my-master", "some-password").build(); --- .../java/io/lettuce/core/RedisClient.java | 120 +++++++++++------- src/main/java/io/lettuce/core/RedisURI.java | 91 +++++++++++-- .../RedisSentinelAsyncCommandsImpl.java | 24 +++- .../RedisSentinelReactiveCommandsImpl.java | 24 ++++ .../api/async/RedisSentinelAsyncCommands.java | 26 ++++ .../RedisSentinelReactiveCommands.java | 26 ++++ .../api/sync/RedisSentinelCommands.java | 26 ++++ .../core/RedisURIBuilderUnitTests.java | 55 ++++++-- 8 files changed, 323 insertions(+), 69 deletions(-) diff --git a/src/main/java/io/lettuce/core/RedisClient.java b/src/main/java/io/lettuce/core/RedisClient.java index 21b3d71b4b..3443b71413 100644 --- a/src/main/java/io/lettuce/core/RedisClient.java +++ b/src/main/java/io/lettuce/core/RedisClient.java @@ -21,9 +21,11 @@ import java.net.SocketAddress; import java.time.Duration; import java.util.List; +import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; +import java.util.concurrent.LinkedBlockingQueue; import java.util.function.Supplier; import reactor.core.publisher.Mono; @@ -283,8 +285,7 @@ private ConnectionFuture> connectStandalone @SuppressWarnings("unchecked") private ConnectionFuture connectStatefulAsync(StatefulRedisConnectionImpl connection, - RedisCodec codec, Endpoint endpoint, - RedisURI redisURI, Supplier commandHandlerSupplier) { + RedisCodec codec, Endpoint endpoint, RedisURI redisURI, Supplier commandHandlerSupplier) { ConnectionBuilder connectionBuilder; if (redisURI.isSsl()) { @@ -432,8 +433,7 @@ private ConnectionFuture> connectPubS StatefulRedisPubSubConnectionImpl connection = newStatefulRedisPubSubConnection(endpoint, writer, codec, timeout); ConnectionFuture> future = connectStatefulAsync(connection, codec, endpoint, - redisURI, - () -> new PubSubCommandHandler<>(clientOptions, clientResources, codec, endpoint)); + redisURI, () -> new PubSubCommandHandler<>(clientOptions, clientResources, codec, endpoint)); return future.whenComplete((conn, throwable) -> { @@ -523,6 +523,59 @@ private CompletableFuture> connectS assertNotNull(codec); checkValidRedisURI(redisURI); + logger.debug("Trying to get a Redis Sentinel connection for one of: " + redisURI.getSentinels()); + + if (redisURI.getSentinels().isEmpty() && (isNotEmpty(redisURI.getHost()) || !isEmpty(redisURI.getSocket()))) { + return doConnectSentinelAsync(codec, redisURI.getClientName(), redisURI, timeout).toCompletableFuture(); + } + + List sentinels = redisURI.getSentinels(); + Queue exceptionCollector = new LinkedBlockingQueue<>(); + validateUrisAreOfSameConnectionType(sentinels); + + Mono> connectionLoop = null; + + for (RedisURI uri : sentinels) { + + String clientName = LettuceStrings.isNotEmpty(uri.getClientName()) ? uri.getClientName() : redisURI.getClientName(); + + Mono> connectionMono = Mono + .defer(() -> Mono.fromCompletionStage(doConnectSentinelAsync(codec, clientName, uri, timeout))) + .onErrorMap(CompletionException.class, Throwable::getCause) + .onErrorMap(e -> new RedisConnectionException("Cannot connect Redis Sentinel at " + uri, e)) + .doOnError(exceptionCollector::add); + + if (connectionLoop == null) { + connectionLoop = connectionMono; + } else { + connectionLoop = connectionLoop.onErrorResume(t -> connectionMono); + } + } + + if (connectionLoop == null) { + return Mono.> error( + new RedisConnectionException("Cannot connect to a Redis Sentinel: " + redisURI.getSentinels())).toFuture(); + } + + return connectionLoop.onErrorMap( + e -> { + + RedisConnectionException ex = new RedisConnectionException("Cannot connect to a Redis Sentinel: " + + redisURI.getSentinels(), e); + + for (Throwable throwable : exceptionCollector) { + if (e != throwable) { + ex.addSuppressed(throwable); + } + } + + return ex; + }).toFuture(); + } + + private ConnectionFuture> doConnectSentinelAsync(RedisCodec codec, + String clientName, RedisURI redisURI, Duration timeout) { + ConnectionBuilder connectionBuilder = ConnectionBuilder.connectionBuilder(); connectionBuilder.clientOptions(ClientOptions.copyOf(getOptions())); connectionBuilder.clientResources(clientResources); @@ -536,7 +589,7 @@ private CompletableFuture> connectS StatefulRedisSentinelConnectionImpl connection = newStatefulRedisSentinelConnection(writer, codec, timeout); - logger.debug("Trying to get a Redis Sentinel connection for one of: " + redisURI.getSentinels()); + logger.debug("Connecting to Redis Sentinel, address: " + redisURI); connectionBuilder.endpoint(endpoint).commandHandler(() -> new CommandHandler(clientOptions, clientResources, endpoint)) .connection(connection); @@ -546,55 +599,32 @@ private CompletableFuture> connectS connectionBuilder.enablePingBeforeConnect(); } - Mono> connect; - if (redisURI.getSentinels().isEmpty() && (isNotEmpty(redisURI.getHost()) || !isEmpty(redisURI.getSocket()))) { - - channelType(connectionBuilder, redisURI); - connect = Mono.fromCompletionStage(initializeChannelAsync(connectionBuilder)); - } else { + channelType(connectionBuilder, redisURI); + ConnectionFuture sync = initializeChannelAsync(connectionBuilder); - List sentinels = redisURI.getSentinels(); - validateUrisAreOfSameConnectionType(sentinels); + if (!clientOptions.isPingBeforeActivateConnection() && hasPassword(redisURI)) { - Mono> connectionLoop = Mono.defer(() -> { + sync = sync.thenCompose(channelHandler -> { - RedisURI uri = sentinels.get(0); - channelType(connectionBuilder, uri); - return connectSentinel(connectionBuilder, uri); + CommandArgs args = new CommandArgs<>(codec).add(redisURI.getPassword()); + return connection.async().dispatch(CommandType.AUTH, new StatusOutput<>(codec), args).toCompletableFuture(); }); - - for (int i = 1; i < sentinels.size(); i++) { - - RedisURI uri = sentinels.get(i); - connectionLoop = connectionLoop.onErrorResume(t -> connectSentinel(connectionBuilder, uri)); - } - - connect = connectionLoop; } - if (LettuceStrings.isNotEmpty(redisURI.getClientName())) { - connect = connect.doOnNext(c -> connection.setClientName(redisURI.getClientName())); + if (LettuceStrings.isNotEmpty(clientName)) { + sync = sync.thenApply(channelHandler -> { + connection.setClientName(clientName); + return channelHandler; + }); } - return connect.doOnError(e -> { - - connection.close(); - throw new RedisConnectionException("Cannot connect to a Redis Sentinel: " + redisURI.getSentinels(), e); - }).toFuture(); - } - - private Mono> connectSentinel(ConnectionBuilder connectionBuilder, RedisURI uri) { - - connectionBuilder.socketAddressSupplier(getSocketAddressSupplier(uri)); - SocketAddress socketAddress = clientResources.socketAddressResolver().resolve(uri); - logger.debug("Connecting to Redis Sentinel, address: " + socketAddress); + return sync.thenApply(ignore -> (StatefulRedisSentinelConnection) connection).whenComplete((ignore, e) -> { - Mono> connectionMono = Mono - .fromCompletionStage(initializeChannelAsync(connectionBuilder)); - - return connectionMono.onErrorMap(CompletionException.class, Throwable::getCause) // - .doOnError(t -> logger.warn("Cannot connect Redis Sentinel at " + uri + ": " + t.toString())) // - .onErrorMap(e -> new RedisConnectionException("Cannot connect Redis Sentinel at " + uri, e)); + if (e != null) { + logger.warn("Cannot connect Redis Sentinel at " + redisURI + ": " + e.toString()); + connection.close(); + } + }); } /** diff --git a/src/main/java/io/lettuce/core/RedisURI.java b/src/main/java/io/lettuce/core/RedisURI.java index e5d34013e8..b053b66597 100644 --- a/src/main/java/io/lettuce/core/RedisURI.java +++ b/src/main/java/io/lettuce/core/RedisURI.java @@ -349,9 +349,19 @@ public char[] getPassword() { * @param password the password, must not be {@literal null}. */ public void setPassword(String password) { + setPassword((CharSequence) password); + } + + /** + * Sets the password. Use empty string to skip authentication. + * + * @param password the password, must not be {@literal null}. + * @since 5.2 + */ + public void setPassword(CharSequence password) { LettuceAssert.notNull(password, "Password must not be null"); - this.password = password.toCharArray(); + this.password = password.toString().toCharArray(); } /** @@ -958,11 +968,12 @@ public static class Builder { private int database; private String clientName; private char[] password; + private char[] sentinelPassword; private boolean ssl = false; private boolean verifyPeer = true; private boolean startTls = false; private Duration timeout = DEFAULT_TIMEOUT_DURATION; - private final List sentinels = new ArrayList<>(); + private final List sentinels = new ArrayList<>(); private Builder() { } @@ -971,7 +982,7 @@ private Builder() { * Set Redis socket. Creates a new builder. * * @param socket the host name - * @return New builder with Redis socket. + * @return new builder with Redis socket. */ public static Builder socket(String socket) { @@ -986,7 +997,7 @@ public static Builder socket(String socket) { * Set Redis host. Creates a new builder. * * @param host the host name - * @return New builder with Redis host/port. + * @return new builder with Redis host/port. */ public static Builder redis(String host) { return redis(host, DEFAULT_REDIS_PORT); @@ -997,7 +1008,7 @@ public static Builder redis(String host) { * * @param host the host name * @param port the port - * @return New builder with Redis host/port. + * @return new builder with Redis host/port. */ public static Builder redis(String host, int port) { @@ -1012,7 +1023,7 @@ public static Builder redis(String host, int port) { * Set Sentinel host. Creates a new builder. * * @param host the host name - * @return New builder with Sentinel host/port. + * @return new builder with Sentinel host/port. */ public static Builder sentinel(String host) { @@ -1027,7 +1038,7 @@ public static Builder sentinel(String host) { * * @param host the host name * @param port the port - * @return New builder with Sentinel host/port. + * @return new builder with Sentinel host/port. */ public static Builder sentinel(String host, int port) { @@ -1043,7 +1054,7 @@ public static Builder sentinel(String host, int port) { * * @param host the host name * @param masterId sentinel master id - * @return New builder with Sentinel host/port. + * @return new builder with Sentinel host/port. */ public static Builder sentinel(String host, String masterId) { return sentinel(host, DEFAULT_SENTINEL_PORT, masterId); @@ -1055,14 +1066,30 @@ public static Builder sentinel(String host, String masterId) { * @param host the host name * @param port the port * @param masterId sentinel master id - * @return New builder with Sentinel host/port. + * @return new builder with Sentinel host/port. */ public static Builder sentinel(String host, int port, String masterId) { + return sentinel(host, port, masterId, null); + } + + /** + * Set Sentinel host, port, master id and Sentinel authentication. Creates a new builder. + * + * @param host the host name + * @param port the port + * @param masterId sentinel master id + * @param password the Sentinel password (supported since Redis 5.0.1) + * @return new builder with Sentinel host/port. + */ + public static Builder sentinel(String host, int port, String masterId, CharSequence password) { LettuceAssert.notEmpty(host, "Host must not be empty"); LettuceAssert.isTrue(isValidPort(port), String.format("Port out of range: %s", port)); Builder builder = RedisURI.builder(); + if (password != null) { + builder.sentinelPassword = password.toString().toCharArray(); + } return builder.withSentinelMasterId(masterId).withSentinel(host, port); } @@ -1085,11 +1112,49 @@ public Builder withSentinel(String host) { */ public Builder withSentinel(String host, int port) { + if (this.sentinelPassword != null) { + return withSentinel(host, port, new String(this.sentinelPassword)); + } + + return withSentinel(host, port, null); + } + + /** + * Add a withSentinel host/port and Sentinel authentication to the existing builder. + * + * @param host the host name + * @param port the port + * @param password the Sentinel password (supported since Redis 5.0.1) + * @return the builder + * @since 5.2 + */ + public Builder withSentinel(String host, int port, CharSequence password) { + LettuceAssert.assertState(this.host == null, "Cannot use with Redis mode."); LettuceAssert.notEmpty(host, "Host must not be empty"); LettuceAssert.isTrue(isValidPort(port), String.format("Port out of range: %s", port)); - sentinels.add(HostAndPort.of(host, port)); + RedisURI redisURI = RedisURI.create(host, port); + + if (password != null) { + redisURI.setPassword(password.toString()); + } + + return withSentinel(redisURI); + } + + /** + * Add a withSentinel RedisURI to the existing builder. + * + * @param redisURI the sentinel URI + * @return the builder + * @since 5.2 + */ + public Builder withSentinel(RedisURI redisURI) { + + LettuceAssert.notNull(redisURI, "Redis URI must not be null"); + + sentinels.add(redisURI); return this; } @@ -1290,8 +1355,10 @@ public RedisURI build() { redisURI.setSentinelMasterId(sentinelMasterId); - for (HostAndPort sentinel : sentinels) { - redisURI.getSentinels().add(new RedisURI(sentinel.getHostText(), sentinel.getPort(), timeout)); + for (RedisURI sentinel : sentinels) { + + sentinel.setTimeout(timeout); + redisURI.getSentinels().add(sentinel); } redisURI.setSocket(socket); diff --git a/src/main/java/io/lettuce/core/sentinel/RedisSentinelAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/sentinel/RedisSentinelAsyncCommandsImpl.java index 954ce71a9e..d16c1e92f7 100644 --- a/src/main/java/io/lettuce/core/sentinel/RedisSentinelAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/sentinel/RedisSentinelAsyncCommandsImpl.java @@ -23,8 +23,9 @@ import io.lettuce.core.RedisFuture; import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.codec.RedisCodec; -import io.lettuce.core.protocol.AsyncCommand; -import io.lettuce.core.protocol.RedisCommand; +import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.output.CommandOutput; +import io.lettuce.core.protocol.*; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; import io.lettuce.core.sentinel.api.async.RedisSentinelAsyncCommands; @@ -136,6 +137,25 @@ public RedisFuture info(String section) { return dispatch(commandBuilder.info(section)); } + @Override + public RedisFuture dispatch(ProtocolKeyword type, CommandOutput output) { + + LettuceAssert.notNull(type, "Command type must not be null"); + LettuceAssert.notNull(output, "CommandOutput type must not be null"); + + return dispatch(new AsyncCommand<>(new Command<>(type, output))); + } + + @Override + public RedisFuture dispatch(ProtocolKeyword type, CommandOutput output, CommandArgs args) { + + LettuceAssert.notNull(type, "Command type must not be null"); + LettuceAssert.notNull(output, "CommandOutput type must not be null"); + LettuceAssert.notNull(args, "CommandArgs type must not be null"); + + return dispatch(new AsyncCommand<>(new Command<>(type, output, args))); + } + public AsyncCommand dispatch(RedisCommand cmd) { return (AsyncCommand) connection.dispatch(new AsyncCommand<>(cmd)); } diff --git a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java index 2bbe0c1870..580fa5a027 100644 --- a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java @@ -24,6 +24,11 @@ import io.lettuce.core.KillArgs; import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.output.CommandOutput; +import io.lettuce.core.protocol.Command; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.ProtocolKeyword; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; import io.lettuce.core.sentinel.api.reactive.RedisSentinelReactiveCommands; @@ -135,6 +140,25 @@ public Mono info(String section) { return createMono(() -> commandBuilder.info(section)); } + @SuppressWarnings("unchecked") + public Flux dispatch(ProtocolKeyword type, CommandOutput output) { + + LettuceAssert.notNull(type, "Command type must not be null"); + LettuceAssert.notNull(output, "CommandOutput type must not be null"); + + return (Flux) createFlux(() -> new Command<>(type, output)); + } + + @SuppressWarnings("unchecked") + public Flux dispatch(ProtocolKeyword type, CommandOutput output, CommandArgs args) { + + LettuceAssert.notNull(type, "Command type must not be null"); + LettuceAssert.notNull(output, "CommandOutput type must not be null"); + LettuceAssert.notNull(args, "CommandArgs type must not be null"); + + return (Flux) createFlux(() -> new Command<>(type, output, args)); + } + @Override public void close() { getStatefulConnection().close(); diff --git a/src/main/java/io/lettuce/core/sentinel/api/async/RedisSentinelAsyncCommands.java b/src/main/java/io/lettuce/core/sentinel/api/async/RedisSentinelAsyncCommands.java index 534290b789..9eef575530 100644 --- a/src/main/java/io/lettuce/core/sentinel/api/async/RedisSentinelAsyncCommands.java +++ b/src/main/java/io/lettuce/core/sentinel/api/async/RedisSentinelAsyncCommands.java @@ -21,6 +21,9 @@ import io.lettuce.core.KillArgs; import io.lettuce.core.RedisFuture; +import io.lettuce.core.output.CommandOutput; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.ProtocolKeyword; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; /** @@ -180,6 +183,29 @@ public interface RedisSentinelAsyncCommands { */ RedisFuture ping(); + /** + * Dispatch a command to the Redis Server. Please note the command output type must fit to the command response. + * + * @param type the command, must not be {@literal null}. + * @param output the command output, must not be {@literal null}. + * @param response type + * @return the command response + * @since 5.2 + */ + RedisFuture dispatch(ProtocolKeyword type, CommandOutput output); + + /** + * Dispatch a command to the Redis Server. Please note the command output type must fit to the command response. + * + * @param type the command, must not be {@literal null}. + * @param output the command output, must not be {@literal null}. + * @param args the command arguments, must not be {@literal null}. + * @param response type + * @return the command response + * @since 5.2 + */ + RedisFuture dispatch(ProtocolKeyword type, CommandOutput output, CommandArgs args); + /** * @return true if the connection is open (connected and not closed). */ diff --git a/src/main/java/io/lettuce/core/sentinel/api/reactive/RedisSentinelReactiveCommands.java b/src/main/java/io/lettuce/core/sentinel/api/reactive/RedisSentinelReactiveCommands.java index b57be0d3a6..011278e74a 100644 --- a/src/main/java/io/lettuce/core/sentinel/api/reactive/RedisSentinelReactiveCommands.java +++ b/src/main/java/io/lettuce/core/sentinel/api/reactive/RedisSentinelReactiveCommands.java @@ -21,6 +21,9 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import io.lettuce.core.KillArgs; +import io.lettuce.core.output.CommandOutput; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.ProtocolKeyword; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; /** @@ -180,6 +183,29 @@ public interface RedisSentinelReactiveCommands { */ Mono ping(); + /** + * Dispatch a command to the Redis Server. Please note the command output type must fit to the command response. + * + * @param type the command, must not be {@literal null}. + * @param output the command output, must not be {@literal null}. + * @param response type + * @return the command response + * @since 5.2 + */ + Flux dispatch(ProtocolKeyword type, CommandOutput output); + + /** + * Dispatch a command to the Redis Server. Please note the command output type must fit to the command response. + * + * @param type the command, must not be {@literal null}. + * @param output the command output, must not be {@literal null}. + * @param args the command arguments, must not be {@literal null}. + * @param response type + * @return the command response + * @since 5.2 + */ + Flux dispatch(ProtocolKeyword type, CommandOutput output, CommandArgs args); + /** * @return true if the connection is open (connected and not closed). */ diff --git a/src/main/java/io/lettuce/core/sentinel/api/sync/RedisSentinelCommands.java b/src/main/java/io/lettuce/core/sentinel/api/sync/RedisSentinelCommands.java index 4165c49cb2..14c047b5e0 100644 --- a/src/main/java/io/lettuce/core/sentinel/api/sync/RedisSentinelCommands.java +++ b/src/main/java/io/lettuce/core/sentinel/api/sync/RedisSentinelCommands.java @@ -20,6 +20,9 @@ import java.util.Map; import io.lettuce.core.KillArgs; +import io.lettuce.core.output.CommandOutput; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.ProtocolKeyword; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; /** @@ -179,6 +182,29 @@ public interface RedisSentinelCommands { */ String ping(); + /** + * Dispatch a command to the Redis Server. Please note the command output type must fit to the command response. + * + * @param type the command, must not be {@literal null}. + * @param output the command output, must not be {@literal null}. + * @param response type + * @return the command response + * @since 5.2 + */ + T dispatch(ProtocolKeyword type, CommandOutput output); + + /** + * Dispatch a command to the Redis Server. Please note the command output type must fit to the command response. + * + * @param type the command, must not be {@literal null}. + * @param output the command output, must not be {@literal null}. + * @param args the command arguments, must not be {@literal null}. + * @param response type + * @return the command response + * @since 5.2 + */ + T dispatch(ProtocolKeyword type, CommandOutput output, CommandArgs args); + /** * @return true if the connection is open (connected and not closed). */ diff --git a/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java index 1b6c8975e0..991e8b01bb 100644 --- a/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java +++ b/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java @@ -51,7 +51,7 @@ void sentinelWithPort() { @Test void shouldFailIfBuilderIsEmpty() { - assertThatThrownBy(() -> RedisURI.builder().build()).isInstanceOf(IllegalStateException. class); + assertThatThrownBy(() -> RedisURI.builder().build()).isInstanceOf(IllegalStateException.class); } @Test @@ -82,12 +82,13 @@ void redisWithClientName() { @Test void redisHostAndPortWithInvalidPort() { - assertThatThrownBy(() -> RedisURI.Builder.redis("localhost", -1)).isInstanceOf(IllegalArgumentException. class); + assertThatThrownBy(() -> RedisURI.Builder.redis("localhost", -1)).isInstanceOf(IllegalArgumentException.class); } @Test void redisWithInvalidPort() { - assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").withPort(65536)).isInstanceOf(IllegalArgumentException. class); + assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").withPort(65536)).isInstanceOf( + IllegalArgumentException.class); } @Test @@ -168,37 +169,72 @@ void redisSentinelFromUrl() { RedisURI sentinel3 = result.getSentinels().get(2); assertThat(sentinel3.getPort()).isEqualTo(RedisURI.DEFAULT_SENTINEL_PORT); assertThat(sentinel3.getHost()).isEqualTo("host3"); + } + + @Test + void withAuthenticatedSentinel() { + + RedisURI result = RedisURI.Builder.sentinel("host", 1234, "master", "foo").build(); + + RedisURI sentinel = result.getSentinels().get(0); + assertThat(new String(sentinel.getPassword())).isEqualTo("foo"); + } + + @Test + void withAuthenticatedSentinelUri() { + RedisURI sentinel = new RedisURI("host", 1234, Duration.ZERO); + sentinel.setPassword("bar"); + RedisURI result = RedisURI.Builder.sentinel("host", 1234, "master").withSentinel(sentinel).build(); + + assertThat(result.getSentinels().get(0).getPassword()).isNull(); + assertThat(new String(result.getSentinels().get(1).getPassword())).isEqualTo("bar"); + } + + @Test + void withAuthenticatedSentinelWithSentinel() { + + RedisURI result = RedisURI.Builder.sentinel("host", 1234, "master", "foo").withSentinel("bar").build(); + + assertThat(new String(result.getSentinels().get(0).getPassword())).isEqualTo("foo"); + assertThat(new String(result.getSentinels().get(1).getPassword())).isEqualTo("foo"); + + result = RedisURI.Builder.sentinel("host", 1234, "master", "foo").withSentinel("bar", 1234, "baz").build(); + + assertThat(new String(result.getSentinels().get(0).getPassword())).isEqualTo("foo"); + assertThat(new String(result.getSentinels().get(1).getPassword())).isEqualTo("baz"); } @Test void redisSentinelWithInvalidPort() { - assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 65536)).isInstanceOf(IllegalArgumentException. class); + assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 65536)).isInstanceOf(IllegalArgumentException.class); } @Test void redisSentinelWithMasterIdAndInvalidPort() { - assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 65536, "")).isInstanceOf(IllegalArgumentException. class); + assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 65536, "")).isInstanceOf(IllegalArgumentException.class); } @Test void redisSentinelWithNullMasterId() { - assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 1, null)).isInstanceOf(IllegalArgumentException. class); + assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 1, null)).isInstanceOf(IllegalArgumentException.class); } @Test void redisSentinelWithSSLNotPossible() { - assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 1, "master").withSsl(true)).isInstanceOf(IllegalStateException. class); + assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 1, "master").withSsl(true)).isInstanceOf( + IllegalStateException.class); } @Test void redisSentinelWithTLSNotPossible() { - assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 1, "master").withStartTls(true)).isInstanceOf(IllegalStateException. class); + assertThatThrownBy(() -> RedisURI.Builder.sentinel("a", 1, "master").withStartTls(true)).isInstanceOf( + IllegalStateException.class); } @Test void invalidScheme() { - assertThatThrownBy(() -> RedisURI.create("http://www.web.de")).isInstanceOf(IllegalArgumentException. class); + assertThatThrownBy(() -> RedisURI.create("http://www.web.de")).isInstanceOf(IllegalArgumentException.class); } @Test @@ -226,5 +262,4 @@ void redisSocketWithPassword() throws IOException { assertThat(result.getPort()).isEqualTo(0); assertThat(result.isSsl()).isFalse(); } - }