From 392b3b058a0d246b24136efc44651ea28d090222 Mon Sep 17 00:00:00 2001 From: atakavci Date: Tue, 12 Nov 2024 15:40:02 +0300 Subject: [PATCH] drop use of authxmanager and authenticatedconnection from core --- .../java/redis/clients/jedis/Connection.java | 39 ++- .../clients/jedis/ConnectionFactory.java | 18 +- .../redis/clients/jedis/ConnectionPool.java | 31 +- .../authentication/JedisAuthXManager.java | 95 ++++++- .../JedisAuthenticationException.java | 12 + .../TokenBasedAuthenticationUnitTests.java | 266 +++++++++++++++++- 6 files changed, 392 insertions(+), 69 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/authentication/JedisAuthenticationException.java diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java index 6e2de1377a..655158f1eb 100644 --- a/src/main/java/redis/clients/jedis/Connection.java +++ b/src/main/java/redis/clients/jedis/Connection.java @@ -16,14 +16,11 @@ import java.util.function.Supplier; import java.util.concurrent.atomic.AtomicReference; -import redis.clients.authentication.core.AuthenticatedConnection; -import redis.clients.authentication.core.Token; import redis.clients.jedis.Protocol.Command; import redis.clients.jedis.Protocol.Keyword; import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.args.ClientAttributeOption; import redis.clients.jedis.args.Rawable; -import redis.clients.jedis.authentication.TokenCredentials; import redis.clients.jedis.commands.ProtocolCommand; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisDataException; @@ -33,7 +30,7 @@ import redis.clients.jedis.util.RedisInputStream; import redis.clients.jedis.util.RedisOutputStream; -public class Connection implements Closeable, AuthenticatedConnection { +public class Connection implements Closeable { private ConnectionPool memberOf; protected RedisProtocol protocol; @@ -48,7 +45,8 @@ public class Connection implements Closeable, AuthenticatedConnection { private String strVal; protected String server; protected String version; - protected AtomicReference currentToken = new AtomicReference(null); + protected AtomicReference currentCredentials = new AtomicReference( + null); public Connection() { this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT); @@ -98,8 +96,8 @@ public String toIdentityString() { SocketAddress remoteAddr = socket.getRemoteSocketAddress(); SocketAddress localAddr = socket.getLocalSocketAddress(); if (remoteAddr != null) { - strVal = String.format("%s{id: 0x%X, L:%s %c R:%s}", className, id, - localAddr, (broken ? '!' : '-'), remoteAddr); + strVal = String.format("%s{id: 0x%X, L:%s %c R:%s}", className, id, localAddr, + (broken ? '!' : '-'), remoteAddr); } else if (localAddr != null) { strVal = String.format("%s{id: 0x%X, L:%s}", className, id, localAddr); } else { @@ -443,8 +441,8 @@ private static boolean validateClientInfo(String info) { for (int i = 0; i < info.length(); i++) { char c = info.charAt(i); if (c < '!' || c > '~') { - throw new JedisValidationException("client info cannot contain spaces, " - + "newlines or special characters."); + throw new JedisValidationException( + "client info cannot contain spaces, " + "newlines or special characters."); } } return true; @@ -474,7 +472,8 @@ protected void initializeFromClientConfig(final JedisClientConfig config) { String clientName = config.getClientName(); if (clientName != null && validateClientInfo(clientName)) { - fireAndForgetMsg.add(new CommandArguments(Command.CLIENT).add(Keyword.SETNAME).add(clientName)); + fireAndForgetMsg + .add(new CommandArguments(Command.CLIENT).add(Keyword.SETNAME).add(clientName)); } ClientSetInfoConfig setInfoConfig = config.getClientSetInfoConfig(); @@ -530,12 +529,13 @@ private void helloAndAuth(final RedisProtocol protocol, final RedisCredentials c if (protocol != null && credentials != null && credentials.getUser() != null) { byte[] rawPass = encodeToBytes(credentials.getPassword()); try { - helloResult = hello(encode(protocol.version()), Keyword.AUTH.getRaw(), encode(credentials.getUser()), rawPass); + helloResult = hello(encode(protocol.version()), Keyword.AUTH.getRaw(), + encode(credentials.getUser()), rawPass); } finally { Arrays.fill(rawPass, (byte) 0); // clear sensitive data } } else { - auth(credentials); + authenticate(credentials); helloResult = protocol == null ? null : hello(encode(protocol.version())); } if (helloResult != null) { @@ -547,11 +547,11 @@ private void helloAndAuth(final RedisProtocol protocol, final RedisCredentials c // handled in RedisCredentialsProvider.cleanUp() } - public void setToken(Token token) { - currentToken.set(token); + public void setCredentials(RedisCredentials credentials) { + currentCredentials.set(credentials); } - private void auth(RedisCredentials credentials) { + public void authenticate(RedisCredentials credentials) { if (credentials == null || credentials.getPassword() == null) { return; } @@ -569,9 +569,9 @@ private void auth(RedisCredentials credentials) { } public void reAuth() { - Token temp = currentToken.getAndSet(null); + RedisCredentials temp = currentCredentials.getAndSet(null); if (temp != null) { - auth(new TokenCredentials(temp)); + authenticate(temp); } } @@ -601,9 +601,4 @@ public boolean ping() { } return true; } - - @Override - public void authenticate(Token token) { - this.setToken(token); - } } diff --git a/src/main/java/redis/clients/jedis/ConnectionFactory.java b/src/main/java/redis/clients/jedis/ConnectionFactory.java index 9ccdb6b918..ce4a10cb7b 100644 --- a/src/main/java/redis/clients/jedis/ConnectionFactory.java +++ b/src/main/java/redis/clients/jedis/ConnectionFactory.java @@ -11,8 +11,7 @@ import java.util.function.Supplier; import redis.clients.jedis.annots.Experimental; -import redis.clients.jedis.authentication.TokenCredentials; -import redis.clients.authentication.core.AuthXManager; +import redis.clients.jedis.authentication.JedisAuthXManager; import redis.clients.jedis.csc.Cache; import redis.clients.jedis.csc.CacheConnection; import redis.clients.jedis.exceptions.JedisException; @@ -39,7 +38,7 @@ public ConnectionFactory(final HostAndPort hostAndPort, final JedisClientConfig @Experimental public ConnectionFactory(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, - Cache csCache, AuthXManager authXManager) { + Cache csCache, JedisAuthXManager authXManager) { this(new DefaultJedisSocketFactory(hostAndPort, clientConfig), clientConfig, csCache, authXManager); } @@ -50,7 +49,7 @@ public ConnectionFactory(final JedisSocketFactory jedisSocketFactory, } private ConnectionFactory(final JedisSocketFactory jedisSocketFactory, - final JedisClientConfig clientConfig, Cache csCache, AuthXManager authXManager) { + final JedisClientConfig clientConfig, Cache csCache, JedisAuthXManager authXManager) { this.jedisSocketFactory = jedisSocketFactory; this.clientSideCache = csCache; @@ -60,7 +59,7 @@ private ConnectionFactory(final JedisSocketFactory jedisSocketFactory, this.objectMaker = connectionSupplier(); } else { this.clientConfig = replaceCredentialsProvider(clientConfig, - buildCredentialsProvider(authXManager)); + authXManager); Supplier supplier = connectionSupplier(); this.objectMaker = () -> (Connection) authXManager.addConnection(supplier.get()); @@ -78,15 +77,6 @@ private JedisClientConfig replaceCredentialsProvider(JedisClientConfig origin, .credentialsProvider(newCredentialsProvider).build(); } - private Supplier buildCredentialsProvider(AuthXManager connManager) { - return new Supplier() { - @Override - public RedisCredentials get() { - return new TokenCredentials(connManager.getCurrentToken()); - } - }; - } - private Supplier connectionSupplier() { return clientSideCache == null ? () -> new Connection(jedisSocketFactory, clientConfig) : () -> new CacheConnection(jedisSocketFactory, clientConfig, clientSideCache); diff --git a/src/main/java/redis/clients/jedis/ConnectionPool.java b/src/main/java/redis/clients/jedis/ConnectionPool.java index ac9cf63679..d7dc0d85f7 100644 --- a/src/main/java/redis/clients/jedis/ConnectionPool.java +++ b/src/main/java/redis/clients/jedis/ConnectionPool.java @@ -3,9 +3,6 @@ import org.apache.commons.pool2.PooledObjectFactory; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; -import redis.clients.authentication.core.AuthXManagerFactory; -import redis.clients.authentication.core.Token; -import redis.clients.authentication.core.TokenListener; import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.authentication.JedisAuthXManager; import redis.clients.jedis.csc.Cache; @@ -23,7 +20,7 @@ public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig) { public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig, JedisAuthXManager authXManager) { this(new ConnectionFactory(hostAndPort, clientConfig, null, authXManager)); - attachAuthXManager(authXManager); + attachAuthenticationListener(authXManager); } @Experimental @@ -36,7 +33,7 @@ public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig, public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig, Cache clientSideCache, JedisAuthXManager authXManager) { this(new ConnectionFactory(hostAndPort, clientConfig, clientSideCache, authXManager)); - attachAuthXManager(authXManager); + attachAuthenticationListener(authXManager); } public ConnectionPool(PooledObjectFactory factory) { @@ -60,7 +57,7 @@ public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig, GenericObjectPoolConfig poolConfig) { this(new ConnectionFactory(hostAndPort, clientConfig, clientSideCache, authXManager), poolConfig); - attachAuthXManager(authXManager); + attachAuthenticationListener(authXManager); } public ConnectionPool(PooledObjectFactory factory, @@ -85,26 +82,20 @@ public void close() { private static JedisAuthXManager createAuthXManager(JedisClientConfig config) { if (config.getTokenAuthConfig() != null) { - return AuthXManagerFactory.create(JedisAuthXManager.class, config.getTokenAuthConfig()); + return new JedisAuthXManager(config.getTokenAuthConfig()); } return null; } - private void attachAuthXManager(JedisAuthXManager authXManager) { + private void attachAuthenticationListener(JedisAuthXManager authXManager) { this.authXManager = authXManager; if (authXManager != null) { - authXManager.setListener(new TokenListener() { - @Override - public void onTokenRenewed(Token token) { - try { - evict(); - } catch (Exception e) { - throw new JedisException("Failed to evict connections from pool", e); - } - } - - @Override - public void onError(Exception reason) { + authXManager.setListener(token -> { + try { + // this is to trigger validations on each connection via ConnectionFactory + evict(); + } catch (Exception e) { + throw new JedisException("Failed to evict connections from pool", e); } }); } diff --git a/src/main/java/redis/clients/jedis/authentication/JedisAuthXManager.java b/src/main/java/redis/clients/jedis/authentication/JedisAuthXManager.java index dfda02b8ad..57a3bab894 100644 --- a/src/main/java/redis/clients/jedis/authentication/JedisAuthXManager.java +++ b/src/main/java/redis/clients/jedis/authentication/JedisAuthXManager.java @@ -1,26 +1,103 @@ package redis.clients.jedis.authentication; -import redis.clients.authentication.core.AuthXManager; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import redis.clients.authentication.core.Token; +import redis.clients.authentication.core.TokenAuthConfig; import redis.clients.authentication.core.TokenListener; import redis.clients.authentication.core.TokenManager; +import redis.clients.jedis.Connection; +import redis.clients.jedis.RedisCredentials; + +public class JedisAuthXManager implements Supplier { + + private static final Logger log = LoggerFactory.getLogger(JedisAuthXManager.class); -public class JedisAuthXManager extends AuthXManager { - private TokenListener listener; + private TokenManager tokenManager; + private List> connections = Collections + .synchronizedList(new ArrayList<>()); + private Token currentToken; + private AuthenticationListener listener; + + public interface AuthenticationListener { + public void onAuthenticate(Token token); + } public JedisAuthXManager(TokenManager tokenManager) { - super(tokenManager); + this.tokenManager = tokenManager; } - public void setListener(TokenListener listener) { - this.listener = listener; + public JedisAuthXManager(TokenAuthConfig tokenAuthConfig) { + this(new TokenManager(tokenAuthConfig.getIdentityProviderConfig().getProvider(), + tokenAuthConfig.getTokenManagerConfig())); + } + + public void start(boolean blockForInitialToken) + throws InterruptedException, ExecutionException, TimeoutException { + + tokenManager.start(new TokenListener() { + @Override + public void onTokenRenewed(Token token) { + currentToken = token; + authenticateConnections(token); + } + + @Override + public void onError(Exception reason) { + JedisAuthXManager.this.onError(reason); + } + }, blockForInitialToken); } - @Override public void authenticateConnections(Token token) { - super.authenticateConnections(token); + RedisCredentials credentialsFromToken = new TokenCredentials(token); + for (WeakReference connectionRef : connections) { + Connection connection = connectionRef.get(); + if (connection != null) { + try { + connection.setCredentials(credentialsFromToken); + } catch (Exception e) { + log.error("Failed to authenticate connection!", e); + } + } else { + connections.remove(connectionRef); + } + } if (listener != null) { - listener.onTokenRenewed(token); + listener.onAuthenticate(token); } } + + public void onError(Exception reason) { + throw new JedisAuthenticationException( + "Token request/renewal failed with message:" + reason.getMessage(), reason); + } + + public Connection addConnection(Connection connection) { + connections.add(new WeakReference<>(connection)); + return connection; + } + + public void stop() { + tokenManager.stop(); + } + + public void setListener(AuthenticationListener listener) { + this.listener = listener; + } + + @Override + public RedisCredentials get() { + return new TokenCredentials(this.currentToken); + } + } \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/authentication/JedisAuthenticationException.java b/src/main/java/redis/clients/jedis/authentication/JedisAuthenticationException.java new file mode 100644 index 0000000000..adc421e790 --- /dev/null +++ b/src/main/java/redis/clients/jedis/authentication/JedisAuthenticationException.java @@ -0,0 +1,12 @@ +package redis.clients.jedis.authentication; + +public class JedisAuthenticationException extends RuntimeException { + + public JedisAuthenticationException(String message) { + super(message); + } + + public JedisAuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/test/java/redis/clients/jedis/authentication/TokenBasedAuthenticationUnitTests.java b/src/test/java/redis/clients/jedis/authentication/TokenBasedAuthenticationUnitTests.java index a59e81a824..8a0720906e 100644 --- a/src/test/java/redis/clients/jedis/authentication/TokenBasedAuthenticationUnitTests.java +++ b/src/test/java/redis/clients/jedis/authentication/TokenBasedAuthenticationUnitTests.java @@ -1,14 +1,39 @@ package redis.clients.jedis.authentication; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.either; +import static org.hamcrest.CoreMatchers.is; import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; + +import org.hamcrest.Matchers; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; + import redis.clients.authentication.core.IdentityProvider; +import redis.clients.authentication.core.IdentityProviderConfig; import redis.clients.authentication.core.SimpleToken; +import redis.clients.authentication.core.Token; +import redis.clients.authentication.core.TokenAuthConfig; +import redis.clients.authentication.core.TokenListener; import redis.clients.authentication.core.TokenManager; import redis.clients.authentication.core.TokenManagerConfig; import redis.clients.jedis.ConnectionPool; @@ -19,7 +44,25 @@ public class TokenBasedAuthenticationUnitTests { protected static final EndpointConfig endpoint = HostAndPorts.getRedisEndpoint("standalone0"); @Test - public void testJedisAuthXManager() throws Exception { + public void testJedisAuthXManagerInstance() { + TokenManagerConfig tokenManagerConfig = mock(TokenManagerConfig.class); + IdentityProviderConfig identityProviderConfig = mock(IdentityProviderConfig.class); + IdentityProvider identityProvider = mock(IdentityProvider.class); + + when(identityProviderConfig.getProvider()).thenReturn(identityProvider); + + try (MockedConstruction mockedConstructor = mockConstruction(TokenManager.class, + (mock, context) -> { + assertEquals(identityProvider, context.arguments().get(0)); + assertEquals(tokenManagerConfig, context.arguments().get(1)); + })) { + + new JedisAuthXManager(new TokenAuthConfig(tokenManagerConfig, identityProviderConfig)); + } + } + + @Test + public void testJedisAuthXManagerTriggersEvict() throws Exception { IdentityProvider idProvider = mock(IdentityProvider.class); when(idProvider.requestToken()) @@ -31,17 +74,232 @@ public void testJedisAuthXManager() throws Exception { JedisAuthXManager jedisAuthXManager = new JedisAuthXManager(tokenManager); AtomicInteger numberOfEvictions = new AtomicInteger(0); - ConnectionPool pool = spy(new ConnectionPool(endpoint.getHostAndPort(), + ConnectionPool pool = new ConnectionPool(endpoint.getHostAndPort(), endpoint.getClientConfigBuilder().build(), jedisAuthXManager) { @Override public void evict() throws Exception { numberOfEvictions.incrementAndGet(); super.evict(); } - }); + }; jedisAuthXManager.start(true); - assertEquals(1, numberOfEvictions.get()); } + + public static class TokenManagerConfigWrapper extends TokenManagerConfig { + int lower; + float ratio; + + public TokenManagerConfigWrapper() { + super(0, 0, 0, null); + } + + @Override + public int getLowerRefreshBoundMillis() { + return lower; + } + + @Override + public float getExpirationRefreshRatio() { + return ratio; + } + } + + @Test + public void testCalculateRenewalDelay() { + long delay = 0; + long duration = 0; + long issueDate; + long expireDate; + + TokenManagerConfigWrapper config = new TokenManagerConfigWrapper(); + TokenManager manager = new TokenManager(() -> null, config); + + duration = 5000; + config.lower = 2000; + config.ratio = 0.5F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertThat(delay, Matchers + .greaterThanOrEqualTo(Math.min(duration - config.lower, (long) (duration * config.ratio)))); + + duration = 10000; + config.lower = 8000; + config.ratio = 0.2F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertThat(delay, Matchers + .greaterThanOrEqualTo(Math.min(duration - config.lower, (long) (duration * config.ratio)))); + + duration = 10000; + config.lower = 10000; + config.ratio = 0.2F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertEquals(0, delay); + + duration = 0; + config.lower = 5000; + config.ratio = 0.2F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertEquals(0, delay); + + duration = 10000; + config.lower = 1000; + config.ratio = 0.00001F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertEquals(0, delay); + + duration = 10000; + config.lower = 1000; + config.ratio = 0.0001F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertThat(delay, either(is(0L)).or(is(1L))); + } + + @Test + public void testAuthXManagerReceivesNewToken() throws InterruptedException, ExecutionException, TimeoutException { + + IdentityProvider identityProvider = () -> new SimpleToken("tokenVal", + System.currentTimeMillis() + 5 * 1000, System.currentTimeMillis(), + Collections.singletonMap("oid", "user1")); + + TokenManager tokenManager = new TokenManager(identityProvider, + new TokenManagerConfig(0.7F, 200, 2000, null)); + + JedisAuthXManager manager = spy(new JedisAuthXManager(tokenManager)); + + final Token[] tokenHolder = new Token[1]; + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + tokenHolder[0] = (Token) args[0]; + return null; + }).when(manager).authenticateConnections(any()); + + manager.start(true); + assertEquals(tokenHolder[0].getValue(), "tokenVal"); + } + + @Test + public void testBlockForInitialToken() { + IdentityProvider identityProvider = () -> { + throw new RuntimeException("Test exception from identity provider!"); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, + new TokenManagerConfig(0.7F, 200, 2000, new TokenManagerConfig.RetryPolicy(5, 100))); + + JedisAuthXManager manager = new JedisAuthXManager(tokenManager); + ExecutionException e = assertThrows(ExecutionException.class, () -> manager.start(true)); + + assertEquals("java.lang.RuntimeException: Test exception from identity provider!", + e.getCause().getCause().getMessage()); + } + + @Test + public void testNoBlockForInitialToken() + throws InterruptedException, ExecutionException, TimeoutException { + int numberOfRetries = 5; + CountDownLatch requesLatch = new CountDownLatch(numberOfRetries); + IdentityProvider identityProvider = () -> { + requesLatch.countDown(); + throw new RuntimeException("Test exception from identity provider!"); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, + 2000, new TokenManagerConfig.RetryPolicy(numberOfRetries - 1, 100))); + + JedisAuthXManager manager = spy(new JedisAuthXManager(tokenManager)); + manager.start(false); + + requesLatch.await(); + verify(manager, Mockito.atLeastOnce()).onError(Mockito.any()); + verify(manager, Mockito.never()).authenticateConnections(Mockito.any()); + } + + @Test + public void testTokenManagerWithFailingTokenRequest() + throws InterruptedException, ExecutionException, TimeoutException { + int numberOfRetries = 5; + CountDownLatch requesLatch = new CountDownLatch(numberOfRetries); + + IdentityProvider identityProvider = mock(IdentityProvider.class); + when(identityProvider.requestToken()).thenAnswer(invocation -> { + requesLatch.countDown(); + if (requesLatch.getCount() > 0) { + throw new RuntimeException("Test exception from identity provider!"); + } + return new SimpleToken("tokenValX", System.currentTimeMillis() + 50 * 1000, + System.currentTimeMillis(), Collections.singletonMap("oid", "user1")); + }); + + ArgumentCaptor argument = ArgumentCaptor.forClass(Token.class); + + TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, + 2000, new TokenManagerConfig.RetryPolicy(numberOfRetries - 1, 100))); + + TokenListener listener = mock(TokenListener.class); + tokenManager.start(listener, false); + requesLatch.await(); + verify(identityProvider, times(numberOfRetries)).requestToken(); + verify(listener, never()).onError(any()); + verify(listener).onTokenRenewed(argument.capture()); + assertEquals("tokenValX", argument.getValue().getValue()); + } + + @Test + public void testTokenManagerWithHangingTokenRequest() + throws InterruptedException, ExecutionException, TimeoutException { + int sleepDuration = 200; + int executionTimeout = 100; + int tokenLifetime = 50 * 1000; + int numberOfRetries = 5; + CountDownLatch requesLatch = new CountDownLatch(numberOfRetries); + + IdentityProvider identityProvider = () -> { + requesLatch.countDown(); + if (requesLatch.getCount() > 0) { + try { + Thread.sleep(sleepDuration); + } catch (InterruptedException e) { + } + return null; + } + return new SimpleToken("tokenValX", System.currentTimeMillis() + tokenLifetime, + System.currentTimeMillis(), Collections.singletonMap("oid", "user1")); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, + executionTimeout, new TokenManagerConfig.RetryPolicy(numberOfRetries, 100))); + + JedisAuthXManager manager = spy(new JedisAuthXManager(tokenManager)); + manager.start(false); + requesLatch.await(); + verify(manager, never()).onError(any()); + await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { + verify(manager, times(1)).authenticateConnections(any()); + }); + } }