From f4bf72aa89cfa7bf1db4461f9a72231cec8d60b0 Mon Sep 17 00:00:00 2001 From: Thiago Cardoso Date: Wed, 8 Apr 2026 20:25:50 -0300 Subject: [PATCH] Add configurable token providers --- README.md | 48 +++++- build.gradle | 2 +- .../java/com/incognia/api/IncogniaAPI.java | 31 ++-- .../api/clients/AutoRefreshTokenProvider.java | 46 ++++++ .../clients/ManualRefreshTokenProvider.java | 72 +++++++++ .../clients/TokenAwareNetworkingClient.java | 58 +++++-- .../incognia/api/clients/TokenProvider.java | 76 +-------- .../incognia/api/clients/TokenRequester.java | 41 +++++ .../incognia/api/clients/TokenResponse.java | 6 - src/main/java/com/incognia/common/Token.java | 15 ++ .../exceptions/TokenExpiredException.java | 7 + .../exceptions/TokenNotFoundException.java | 7 + .../incognia/common/utils/CustomOptions.java | 4 +- .../com/incognia/api/IncogniaAPITest.java | 61 +++++++- .../clients/AutoRefreshTokenProviderTest.java | 137 ++++++++++++++++ .../ManualRefreshTokenProviderTest.java | 122 +++++++++++++++ .../api/clients/TokenAwareDispatcher.java | 2 +- .../TokenAwareNetworkingClientTest.java | 147 +++++++++++++++++- .../api/clients/TokenProviderTest.java | 120 -------------- 19 files changed, 752 insertions(+), 250 deletions(-) create mode 100644 src/main/java/com/incognia/api/clients/AutoRefreshTokenProvider.java create mode 100644 src/main/java/com/incognia/api/clients/ManualRefreshTokenProvider.java create mode 100644 src/main/java/com/incognia/api/clients/TokenRequester.java create mode 100644 src/main/java/com/incognia/common/Token.java create mode 100644 src/main/java/com/incognia/common/exceptions/TokenExpiredException.java create mode 100644 src/main/java/com/incognia/common/exceptions/TokenNotFoundException.java create mode 100644 src/test/java/com/incognia/api/clients/AutoRefreshTokenProviderTest.java create mode 100644 src/test/java/com/incognia/api/clients/ManualRefreshTokenProviderTest.java delete mode 100644 src/test/java/com/incognia/api/clients/TokenProviderTest.java diff --git a/README.md b/README.md index b87f68cb..05638e76 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ And then add the artifact `incognia-api-client` **or** `incognia-api-client-shad com.incognia incognia-api-client - 3.16.0 + 3.17.0 ``` ```xml com.incognia incognia-api-client-shaded - 3.16.0 + 3.17.0 ``` @@ -47,13 +47,13 @@ repositories { And then add the dependency ```gradle dependencies { - implementation 'com.incognia:incognia-api-client:3.16.0' + implementation 'com.incognia:incognia-api-client:3.17.0' } ``` OR ```gradle dependencies { - implementation 'com.incognia:incognia-api-client-shaded:3.16.0' + implementation 'com.incognia:incognia-api-client-shaded:3.17.0' } ``` @@ -137,9 +137,45 @@ The implementation is based on the [Incognia API Reference](https://dash.incogni #### Authentication -Authentication is done transparently, so you don't need to worry about it. +Authentication is handled automatically by default, including refreshing expired tokens during API calls. -If you are curious about how we handle it, you can check the `TokenAwareNetworkingClient` class +For latency-sensitive services, you can take control of when refresh happens by creating a `ManualRefreshTokenProvider` and passing it through `CustomOptions`. The library does not create background threads for token refresh, so your application stays in full control of scheduling. + +```java +CustomOptions sharedOptions = + CustomOptions.builder() + .timeoutMillis(5_000L) + .maxConnections(10) + .keepAliveSeconds(60L) + .build(); + +ManualRefreshTokenProvider tokenProvider = + new ManualRefreshTokenProvider("client-id", "client-secret", sharedOptions); + +IncogniaAPI api = + IncogniaAPI.init( + "client-id", + "client-secret", + sharedOptions.toBuilder() + .tokenProvider(tokenProvider) + .build()); + +ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); +executor.scheduleAtFixedRate( + () -> { + try { + Token token = tokenProvider.refresh(); + System.out.println("token refreshed until " + token.getExpiresAt()); + } catch (IncogniaException e) { + System.out.println("could not refresh token"); + } + }, + 0, + 1, + TimeUnit.MINUTES); +``` + +If you are curious about how we handle it, you can check the `TokenAwareNetworkingClient` class. #### Registering Signup diff --git a/build.gradle b/build.gradle index bced639a..de327cbc 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = "com.incognia" -version = "3.16.0" +version = "3.17.0" task createProjectVersionFile { def projectVersionDir = "$projectDir/src/main/java/com/incognia/api" diff --git a/src/main/java/com/incognia/api/IncogniaAPI.java b/src/main/java/com/incognia/api/IncogniaAPI.java index df950a8a..183faa22 100644 --- a/src/main/java/com/incognia/api/IncogniaAPI.java +++ b/src/main/java/com/incognia/api/IncogniaAPI.java @@ -1,6 +1,9 @@ package com.incognia.api; +import com.incognia.api.clients.AutoRefreshTokenProvider; +import com.incognia.api.clients.NetworkingClient; import com.incognia.api.clients.TokenAwareNetworkingClient; +import com.incognia.api.clients.TokenProvider; import com.incognia.common.Address; import com.incognia.common.exceptions.IncogniaAPIException; import com.incognia.common.exceptions.IncogniaException; @@ -62,20 +65,22 @@ public class IncogniaAPI { IncogniaAPI(String clientId, String clientSecret, CustomOptions options, String apiUrl) { Asserts.assertNotEmpty(clientId, "client id"); Asserts.assertNotEmpty(clientSecret, "client secret"); + Asserts.assertNotNull(options, "custom options"); Asserts.assertNotEmpty(apiUrl, "api url"); - tokenAwareNetworkingClient = - new TokenAwareNetworkingClient( - new OkHttpClient.Builder() - .callTimeout(options.getTimeoutMillis(), TimeUnit.MILLISECONDS) - .connectionPool( - new ConnectionPool( - options.getMaxConnections(), - options.getKeepAliveSeconds(), - TimeUnit.SECONDS)) - .build(), - apiUrl, - clientId, - clientSecret); + OkHttpClient httpClient = + new OkHttpClient.Builder() + .callTimeout(options.getTimeoutMillis(), TimeUnit.MILLISECONDS) + .connectionPool( + new ConnectionPool( + options.getMaxConnections(), options.getKeepAliveSeconds(), TimeUnit.SECONDS)) + .build(); + TokenProvider tokenProvider = options.getTokenProvider(); + if (tokenProvider == null) { + tokenProvider = + new AutoRefreshTokenProvider( + clientId, clientSecret, new NetworkingClient(httpClient, apiUrl)); + } + tokenAwareNetworkingClient = new TokenAwareNetworkingClient(httpClient, apiUrl, tokenProvider); } /** diff --git a/src/main/java/com/incognia/api/clients/AutoRefreshTokenProvider.java b/src/main/java/com/incognia/api/clients/AutoRefreshTokenProvider.java new file mode 100644 index 00000000..7bcd94e7 --- /dev/null +++ b/src/main/java/com/incognia/api/clients/AutoRefreshTokenProvider.java @@ -0,0 +1,46 @@ +package com.incognia.api.clients; + +import com.incognia.common.Token; +import com.incognia.common.exceptions.IncogniaException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.locks.ReentrantLock; + +public class AutoRefreshTokenProvider implements TokenProvider { + private static final int TOKEN_REFRESH_BEFORE_SECONDS = 10; + + private final ReentrantLock lock = new ReentrantLock(); + private final TokenRequester tokenRequester; + private volatile Token token; + + public AutoRefreshTokenProvider( + String clientId, String clientSecret, NetworkingClient networkingClient) { + this.tokenRequester = new TokenRequester(clientId, clientSecret, networkingClient); + } + + @Override + public Token getToken() throws IncogniaException { + refreshTokenIfNeeded(); + return token; + } + + private boolean needsRefresh(Token token) { + return token == null + || Instant.now().until(token.getExpiresAt(), ChronoUnit.SECONDS) + <= TOKEN_REFRESH_BEFORE_SECONDS; + } + + private void refreshTokenIfNeeded() throws IncogniaException { + Token currentToken = token; + if (needsRefresh(currentToken)) { + lock.lock(); + try { + if (needsRefresh(token)) { + token = tokenRequester.requestToken(); + } + } finally { + lock.unlock(); + } + } + } +} diff --git a/src/main/java/com/incognia/api/clients/ManualRefreshTokenProvider.java b/src/main/java/com/incognia/api/clients/ManualRefreshTokenProvider.java new file mode 100644 index 00000000..428d2eba --- /dev/null +++ b/src/main/java/com/incognia/api/clients/ManualRefreshTokenProvider.java @@ -0,0 +1,72 @@ +package com.incognia.api.clients; + +import com.incognia.common.Token; +import com.incognia.common.exceptions.IncogniaException; +import com.incognia.common.exceptions.TokenExpiredException; +import com.incognia.common.exceptions.TokenNotFoundException; +import com.incognia.common.utils.Asserts; +import com.incognia.common.utils.CustomOptions; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; + +public class ManualRefreshTokenProvider implements TokenProvider { + private static final String API_URL = "https://api.incognia.com"; + + private final ReentrantLock lock = new ReentrantLock(); + private final TokenRequester tokenRequester; + private volatile Token token; + + public ManualRefreshTokenProvider(String clientId, String clientSecret) { + this(clientId, clientSecret, CustomOptions.builder().build()); + } + + public ManualRefreshTokenProvider(String clientId, String clientSecret, CustomOptions options) { + this(clientId, clientSecret, createNetworkingClient(options)); + } + + ManualRefreshTokenProvider( + String clientId, String clientSecret, NetworkingClient networkingClient) { + Asserts.assertNotEmpty(clientId, "client id"); + Asserts.assertNotEmpty(clientSecret, "client secret"); + Asserts.assertNotNull(networkingClient, "networking client"); + this.tokenRequester = new TokenRequester(clientId, clientSecret, networkingClient); + } + + @Override + public Token getToken() throws IncogniaException { + Token currentToken = token; + if (currentToken == null) { + throw new TokenNotFoundException(); + } + + if (currentToken.isExpired()) { + throw new TokenExpiredException(); + } + + return currentToken; + } + + public Token refresh() throws IncogniaException { + lock.lock(); + try { + token = tokenRequester.requestToken(); + return token; + } finally { + lock.unlock(); + } + } + + private static NetworkingClient createNetworkingClient(CustomOptions options) { + Asserts.assertNotNull(options, "custom options"); + OkHttpClient httpClient = + new OkHttpClient.Builder() + .callTimeout(options.getTimeoutMillis(), TimeUnit.MILLISECONDS) + .connectionPool( + new ConnectionPool( + options.getMaxConnections(), options.getKeepAliveSeconds(), TimeUnit.SECONDS)) + .build(); + return new NetworkingClient(httpClient, API_URL); + } +} diff --git a/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java b/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java index 9db737c0..35c80545 100644 --- a/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java +++ b/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java @@ -1,5 +1,6 @@ package com.incognia.api.clients; +import com.incognia.common.Token; import com.incognia.common.exceptions.IncogniaException; import java.util.HashMap; import java.util.Map; @@ -26,44 +27,67 @@ public class TokenAwareNetworkingClient { public TokenAwareNetworkingClient( OkHttpClient httpClient, String baseUrl, String clientId, String clientSecret) { - this.networkingClient = new NetworkingClient(httpClient, baseUrl); - this.tokenProvider = new TokenProvider(clientId, clientSecret, networkingClient); + this( + httpClient, + baseUrl, + new AutoRefreshTokenProvider( + clientId, clientSecret, new NetworkingClient(httpClient, baseUrl))); } - private Map buildHeaders() throws IncogniaException { - Map headers = new HashMap<>(); - headers.put(USER_AGENT_HEADER, USER_AGENT_HEADER_CONTENT); - headers.put(AUTHORIZATION_HEADER, tokenProvider.buildAuthorizationHeader()); - Long latency = lastLatency.get(); - if (latency != null) { - headers.put(LATENCY_HEADER, Long.toString(latency)); - } - return headers; + public TokenAwareNetworkingClient( + OkHttpClient httpClient, String baseUrl, TokenProvider tokenProvider) { + this.networkingClient = new NetworkingClient(httpClient, baseUrl); + this.tokenProvider = tokenProvider; } public U doPost( String path, T body, Class responseType, Map queryParameters) throws IncogniaException { - tokenProvider.getToken(); + Token token = tokenProvider.getToken(); long start = System.nanoTime(); - U result = networkingClient.doPost(path, body, responseType, buildHeaders(), queryParameters); + U result = + networkingClient.doPost(path, body, responseType, buildHeaders(token), queryParameters); lastLatency.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); return result; } public U doPost(String path, T body, Class responseType) throws IncogniaException { - tokenProvider.getToken(); + Token token = tokenProvider.getToken(); long start = System.nanoTime(); - U result = networkingClient.doPost(path, body, responseType, buildHeaders()); + U result = networkingClient.doPost(path, body, responseType, buildHeaders(token)); lastLatency.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); return result; } public void doPost(String path, T body, Map queryParameters) throws IncogniaException { - tokenProvider.getToken(); + Token token = tokenProvider.getToken(); long start = System.nanoTime(); - networkingClient.doPost(path, body, buildHeaders(), queryParameters); + networkingClient.doPost(path, body, buildHeaders(token), queryParameters); lastLatency.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } + + private Map buildHeaders(Token token) throws IncogniaException { + validateToken(token); + Map headers = new HashMap<>(); + headers.put(USER_AGENT_HEADER, USER_AGENT_HEADER_CONTENT); + headers.put(AUTHORIZATION_HEADER, token.getTokenType() + " " + token.getAccessToken()); + Long latency = lastLatency.get(); + if (latency != null) { + headers.put(LATENCY_HEADER, Long.toString(latency)); + } + return headers; + } + + private void validateToken(Token token) throws IncogniaException { + if (token == null) { + throw new IncogniaException("token provider returned a null token"); + } + if (token.getTokenType() == null || token.getTokenType().isEmpty()) { + throw new IncogniaException("token provider returned a token without token type"); + } + if (token.getAccessToken() == null || token.getAccessToken().isEmpty()) { + throw new IncogniaException("token provider returned a token without access token"); + } + } } diff --git a/src/main/java/com/incognia/api/clients/TokenProvider.java b/src/main/java/com/incognia/api/clients/TokenProvider.java index 74c5d0e5..d4455b55 100644 --- a/src/main/java/com/incognia/api/clients/TokenProvider.java +++ b/src/main/java/com/incognia/api/clients/TokenProvider.java @@ -1,78 +1,8 @@ package com.incognia.api.clients; +import com.incognia.common.Token; import com.incognia.common.exceptions.IncogniaException; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Base64; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.locks.ReentrantLock; -class TokenProvider { - private static final int TOKEN_REFRESH_BEFORE_SECONDS = 10; - private static final String TOKEN_REQUEST_BODY = "grant_type=client_credentials"; - private static final String TOKEN_PATH = "api/v2/token"; - - // This implementation assumes that only one instance of this class - // is created per IncogniaAPI instance. - // Therefore, for each (clientId, clientSecret) pair, there is exactly - // one instance of this class. - private final ReentrantLock lock = new ReentrantLock(); - private volatile TokenResponse token; - - private final String clientId; - private final String clientSecret; - private final NetworkingClient networkingClient; - - public TokenProvider(String clientId, String clientSecret, NetworkingClient networkingClient) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.networkingClient = networkingClient; - } - - public TokenResponse getToken() throws IncogniaException { - refreshTokenIfNeeded(); - return token; - } - - public String buildAuthorizationHeader() throws IncogniaException { - if (token == null) { - refreshTokenIfNeeded(); - } - - return token.getTokenType() + " " + token.getAccessToken(); - } - - private boolean needsRefresh() { - return token == null - || Instant.now().until(token.getExpiresAt(), ChronoUnit.SECONDS) - <= TOKEN_REFRESH_BEFORE_SECONDS; - } - - private void refreshTokenIfNeeded() throws IncogniaException { - if (needsRefresh()) { - lock.lock(); - try { - if (needsRefresh()) { - token = getNewToken(); - token.computeExpiresAt(); - } - } finally { - lock.unlock(); - } - } - } - - private TokenResponse getNewToken() throws IncogniaException { - String clientIdSecret = clientId + ":" + clientSecret; - Map headers = - Collections.singletonMap( - "Authorization", - "Basic " - + Base64.getUrlEncoder() - .encodeToString(clientIdSecret.getBytes(StandardCharsets.UTF_8))); - return networkingClient.doPostFormUrlEncoded( - TOKEN_PATH, TOKEN_REQUEST_BODY, TokenResponse.class, headers); - } +public interface TokenProvider { + Token getToken() throws IncogniaException; } diff --git a/src/main/java/com/incognia/api/clients/TokenRequester.java b/src/main/java/com/incognia/api/clients/TokenRequester.java new file mode 100644 index 00000000..2d13cd46 --- /dev/null +++ b/src/main/java/com/incognia/api/clients/TokenRequester.java @@ -0,0 +1,41 @@ +package com.incognia.api.clients; + +import com.incognia.common.Token; +import com.incognia.common.exceptions.IncogniaException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; + +final class TokenRequester { + private static final String TOKEN_REQUEST_BODY = "grant_type=client_credentials"; + private static final String TOKEN_PATH = "api/v2/token"; + + private final String clientId; + private final String clientSecret; + private final NetworkingClient networkingClient; + + TokenRequester(String clientId, String clientSecret, NetworkingClient networkingClient) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.networkingClient = networkingClient; + } + + Token requestToken() throws IncogniaException { + String clientIdSecret = clientId + ":" + clientSecret; + Map headers = + Collections.singletonMap( + "Authorization", + "Basic " + + Base64.getEncoder() + .encodeToString(clientIdSecret.getBytes(StandardCharsets.UTF_8))); + TokenResponse tokenResponse = + networkingClient.doPostFormUrlEncoded( + TOKEN_PATH, TOKEN_REQUEST_BODY, TokenResponse.class, headers); + return new Token( + tokenResponse.getAccessToken(), + tokenResponse.getTokenType(), + Instant.now().plusSeconds(tokenResponse.getExpiresIn())); + } +} diff --git a/src/main/java/com/incognia/api/clients/TokenResponse.java b/src/main/java/com/incognia/api/clients/TokenResponse.java index ebd89d0d..c53c9161 100644 --- a/src/main/java/com/incognia/api/clients/TokenResponse.java +++ b/src/main/java/com/incognia/api/clients/TokenResponse.java @@ -1,6 +1,5 @@ package com.incognia.api.clients; -import java.time.Instant; import lombok.AllArgsConstructor; import lombok.Data; @@ -10,9 +9,4 @@ public class TokenResponse { private String accessToken; private long expiresIn; private String tokenType; - private Instant expiresAt; - - public void computeExpiresAt() { - this.expiresAt = Instant.now().plusSeconds(this.expiresIn); - } } diff --git a/src/main/java/com/incognia/common/Token.java b/src/main/java/com/incognia/common/Token.java new file mode 100644 index 00000000..66205e00 --- /dev/null +++ b/src/main/java/com/incognia/common/Token.java @@ -0,0 +1,15 @@ +package com.incognia.common; + +import java.time.Instant; +import lombok.Value; + +@Value +public class Token { + String accessToken; + String tokenType; + Instant expiresAt; + + public boolean isExpired() { + return !expiresAt.isAfter(Instant.now()); + } +} diff --git a/src/main/java/com/incognia/common/exceptions/TokenExpiredException.java b/src/main/java/com/incognia/common/exceptions/TokenExpiredException.java new file mode 100644 index 00000000..31d980d6 --- /dev/null +++ b/src/main/java/com/incognia/common/exceptions/TokenExpiredException.java @@ -0,0 +1,7 @@ +package com.incognia.common.exceptions; + +public class TokenExpiredException extends IncogniaException { + public TokenExpiredException() { + super("token is expired"); + } +} diff --git a/src/main/java/com/incognia/common/exceptions/TokenNotFoundException.java b/src/main/java/com/incognia/common/exceptions/TokenNotFoundException.java new file mode 100644 index 00000000..9447b48e --- /dev/null +++ b/src/main/java/com/incognia/common/exceptions/TokenNotFoundException.java @@ -0,0 +1,7 @@ +package com.incognia.common.exceptions; + +public class TokenNotFoundException extends IncogniaException { + public TokenNotFoundException() { + super("token not found in memory"); + } +} diff --git a/src/main/java/com/incognia/common/utils/CustomOptions.java b/src/main/java/com/incognia/common/utils/CustomOptions.java index ab073905..b0d94b9a 100644 --- a/src/main/java/com/incognia/common/utils/CustomOptions.java +++ b/src/main/java/com/incognia/common/utils/CustomOptions.java @@ -1,12 +1,14 @@ package com.incognia.common.utils; +import com.incognia.api.clients.TokenProvider; import lombok.Builder; import lombok.Value; @Value -@Builder +@Builder(toBuilder = true) public class CustomOptions { @Builder.Default long timeoutMillis = 10000L; @Builder.Default int maxConnections = 5; @Builder.Default long keepAliveSeconds = 300; + TokenProvider tokenProvider; } diff --git a/src/test/java/com/incognia/api/IncogniaAPITest.java b/src/test/java/com/incognia/api/IncogniaAPITest.java index 34f4b120..33155a3b 100644 --- a/src/test/java/com/incognia/api/IncogniaAPITest.java +++ b/src/test/java/com/incognia/api/IncogniaAPITest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.verify; import com.incognia.api.clients.TokenAwareDispatcher; +import com.incognia.api.clients.TokenProvider; import com.incognia.common.Address; import com.incognia.common.Coordinates; import com.incognia.common.FinancialAccount; @@ -21,6 +22,7 @@ import com.incognia.common.ReasonCode; import com.incognia.common.ReasonSource; import com.incognia.common.StructuredAddress; +import com.incognia.common.Token; import com.incognia.common.exceptions.IncogniaException; import com.incognia.common.utils.ClientCredentials; import com.incognia.common.utils.CustomOptions; @@ -28,6 +30,7 @@ import com.incognia.feedback.FeedbackIdentifiers; import com.incognia.feedback.PostFeedbackRequestBody; import com.incognia.fixtures.AddressFixture; +import com.incognia.fixtures.TokenCreationFixture; import com.incognia.onboarding.RegisterSignupRequest; import com.incognia.onboarding.RegisterWebSignupRequest; import com.incognia.onboarding.SignupAssessment; @@ -45,7 +48,6 @@ import com.incognia.transaction.payment.PaymentValue; import com.incognia.transaction.payment.PixKey; import com.incognia.transaction.payment.RegisterPaymentRequest; -import java.io.IOException; import java.lang.reflect.Field; import java.time.Instant; import java.util.ArrayList; @@ -87,7 +89,9 @@ class IncogniaAPITest { private IncogniaAPI clientWithLowTimeout; @BeforeEach - void setUp() { + void setUp() throws Exception { + resetIncogniaApiInstances(); + TokenAwareDispatcher.setToken(TokenCreationFixture.createToken()); mockServer = new MockWebServer(); client = new IncogniaAPI( @@ -104,9 +108,9 @@ void setUp() { } @AfterEach - void tearDown() throws NoSuchFieldException, IOException, IllegalAccessException { - resetIncogniaApiInstances(); + void tearDown() throws Exception { mockServer.shutdown(); + resetIncogniaApiInstances(); } @Test @@ -126,6 +130,13 @@ void testInit_shouldReturnAnInstance() { assertThat(instance2).isNotSameAs(instance1); } + @Test + void testInit_whenCustomOptionsIsNull_shouldThrowIllegalArgumentException() { + assertThatThrownBy(() -> IncogniaAPI.init(CLIENT_ID, CLIENT_SECRET, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'custom options' cannot be null"); + } + @Test void testInstanceWithoutCredentials_shouldReturnAnInstance() { IncogniaAPI instance1 = @@ -242,6 +253,48 @@ void testInit_whenNoCustomOptions_shouldCreateOkHttpWithDefaultParameters() { } } + @Test + void testConstructor_whenCustomTokenProviderIsProvided_shouldUseIt() throws Exception { + TokenProvider customTokenProvider = mock(TokenProvider.class); + TokenAwareDispatcher.setToken("custom-token"); + doReturn(new Token("custom-token", "Bearer", Instant.now().plusSeconds(100))) + .when(customTokenProvider) + .getToken(); + dispatcher.setExpectedRequestToken("request-token-web-signup"); + mockServer.setDispatcher(dispatcher); + + IncogniaAPI api = + new IncogniaAPI( + CLIENT_ID, + CLIENT_SECRET, + CustomOptions.builder().tokenProvider(customTokenProvider).build(), + mockServer.url("").toString()); + + api.registerWebSignup( + RegisterWebSignupRequest.builder().requestToken("request-token-web-signup").build()); + + verify(customTokenProvider).getToken(); + assertThat(dispatcher.getTokenRequestCount()).isZero(); + } + + @Test + void testConstructor_whenCustomTokenProviderIsNotProvided_shouldUseDefaultTokenProvider() + throws Exception { + dispatcher.setExpectedRequestToken("request-token-web-signup"); + mockServer.setDispatcher(dispatcher); + IncogniaAPI api = + new IncogniaAPI( + CLIENT_ID, + CLIENT_SECRET, + CustomOptions.builder().build(), + mockServer.url("").toString()); + + api.registerWebSignup( + RegisterWebSignupRequest.builder().requestToken("request-token-web-signup").build()); + + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); + } + @Test @DisplayName("should return the expected signup response") @SneakyThrows diff --git a/src/test/java/com/incognia/api/clients/AutoRefreshTokenProviderTest.java b/src/test/java/com/incognia/api/clients/AutoRefreshTokenProviderTest.java new file mode 100644 index 00000000..afc9a790 --- /dev/null +++ b/src/test/java/com/incognia/api/clients/AutoRefreshTokenProviderTest.java @@ -0,0 +1,137 @@ +package com.incognia.api.clients; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.incognia.common.Token; +import com.incognia.common.exceptions.IncogniaException; +import java.lang.reflect.Field; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AutoRefreshTokenProviderTest { + private final String CLIENT_ID = "client-id"; + private final String ANOTHER_CLIENT_ID = "another-client-id"; + private final String CLIENT_SECRET = "client-secret"; + private MockWebServer mockServer; + private AutoRefreshTokenProvider tokenProvider; + private AutoRefreshTokenProvider anotherTokenProvider; + + @BeforeEach + void setUp() { + mockServer = new MockWebServer(); + tokenProvider = + new AutoRefreshTokenProvider( + CLIENT_ID, + CLIENT_SECRET, + new NetworkingClient(new OkHttpClient(), mockServer.url("").toString())); + anotherTokenProvider = + new AutoRefreshTokenProvider( + ANOTHER_CLIENT_ID, + CLIENT_SECRET, + new NetworkingClient(new OkHttpClient(), mockServer.url("").toString())); + } + + @AfterEach + void tearDown() throws Exception { + mockServer.shutdown(); + } + + @Test + void testGetToken_whenTokenIsNull_shouldReturnTheSameTokenForTheSameInstance() + throws IncogniaException { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + + Token firstToken = tokenProvider.getToken(); + Token secondToken = tokenProvider.getToken(); + + assertThat(firstToken).isSameAs(secondToken); + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); + } + + @Test + void testGetToken_whenCalledConcurrently_shouldRequestTheTokenOnlyOnce() throws Exception { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + int threadCount = 8; + CountDownLatch ready = new CountDownLatch(threadCount); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + try { + List> futures = new ArrayList<>(); + for (int i = 0; i < threadCount; i++) { + futures.add( + executor.submit( + () -> { + ready.countDown(); + assertThat(start.await(5, TimeUnit.SECONDS)).isTrue(); + return tokenProvider.getToken(); + })); + } + + assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue(); + start.countDown(); + + Token firstToken = futures.get(0).get(5, TimeUnit.SECONDS); + for (Future future : futures) { + assertThat(future.get(5, TimeUnit.SECONDS)).isSameAs(firstToken); + } + } finally { + executor.shutdownNow(); + } + + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); + } + + @Test + void testGetToken_whenDifferentProvidersUseDifferentCredentials_shouldNotShareTokens() + throws IncogniaException { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + TokenAwareDispatcher anotherDispatcher = + new TokenAwareDispatcher(ANOTHER_CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + + Token token = tokenProvider.getToken(); + + mockServer.setDispatcher(anotherDispatcher); + Token anotherToken = anotherTokenProvider.getToken(); + + assertThat(anotherToken).isNotSameAs(token); + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); + assertThat(anotherDispatcher.getTokenRequestCount()).isEqualTo(1); + } + + @Test + void testGetToken_whenTokenIsExpired_shouldRefreshToken() throws Exception { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + + Token token = tokenProvider.getToken(); + expireToken(tokenProvider, token); + + Token refreshedToken = tokenProvider.getToken(); + + assertThat(refreshedToken).isNotSameAs(token); + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(2); + } + + private static void expireToken(AutoRefreshTokenProvider tokenProvider, Token token) + throws Exception { + Field tokenField = AutoRefreshTokenProvider.class.getDeclaredField("token"); + tokenField.setAccessible(true); + tokenField.set( + tokenProvider, new Token(token.getAccessToken(), token.getTokenType(), Instant.EPOCH)); + } +} diff --git a/src/test/java/com/incognia/api/clients/ManualRefreshTokenProviderTest.java b/src/test/java/com/incognia/api/clients/ManualRefreshTokenProviderTest.java new file mode 100644 index 00000000..f8730d10 --- /dev/null +++ b/src/test/java/com/incognia/api/clients/ManualRefreshTokenProviderTest.java @@ -0,0 +1,122 @@ +package com.incognia.api.clients; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; + +import com.incognia.common.Token; +import com.incognia.common.exceptions.IncogniaException; +import com.incognia.common.exceptions.TokenExpiredException; +import com.incognia.common.exceptions.TokenNotFoundException; +import com.incognia.common.utils.CustomOptions; +import java.lang.reflect.Field; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; + +class ManualRefreshTokenProviderTest { + private final String CLIENT_ID = "client-id"; + private final String CLIENT_SECRET = "client-secret"; + private ManualRefreshTokenProvider tokenProvider; + private MockWebServer mockServer; + + @BeforeEach + void setUp() { + mockServer = new MockWebServer(); + tokenProvider = + new ManualRefreshTokenProvider( + CLIENT_ID, + CLIENT_SECRET, + new NetworkingClient(new OkHttpClient(), mockServer.url("").toString())); + } + + @AfterEach + void tearDown() throws Exception { + mockServer.shutdown(); + } + + @Test + void testGetToken_whenTokenWasNotRefreshed_shouldThrowTokenNotFoundException() { + assertThatThrownBy(() -> tokenProvider.getToken()).isInstanceOf(TokenNotFoundException.class); + } + + @Test + void testGetToken_whenTokenIsExpired_shouldThrowTokenExpiredException() throws Exception { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + tokenProvider.refresh(); + expireToken(tokenProvider); + + assertThatThrownBy(() -> tokenProvider.getToken()).isInstanceOf(TokenExpiredException.class); + } + + @Test + void testRefresh_shouldStoreAndReturnAToken() throws IncogniaException { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + + Token refreshedToken = tokenProvider.refresh(); + + assertThat(tokenProvider.getToken()).isSameAs(refreshedToken); + assertThat(refreshedToken.getExpiresAt()).isAfter(Instant.now()); + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); + } + + @Test + @SuppressWarnings("unchecked") + void testConstructor_whenCustomOptionsProvided_shouldCreateOkHttpWithRightParameters() { + AtomicReference> poolArgs = new AtomicReference<>(); + + try (MockedConstruction builderConstruction = + mockConstruction( + OkHttpClient.Builder.class, + (mock, context) -> { + doReturn(mock).when(mock).callTimeout(anyLong(), any()); + doReturn(mock).when(mock).connectionPool(any()); + doReturn(mock(OkHttpClient.class)).when(mock).build(); + }); + MockedConstruction ignored = + mockConstruction( + ConnectionPool.class, + (mock, context) -> poolArgs.set((List) context.arguments()))) { + long timeoutMillis = 1234L; + int maxConnections = 8; + long keepAliveSeconds = 45L; + + new ManualRefreshTokenProvider( + CLIENT_ID, + CLIENT_SECRET, + CustomOptions.builder() + .timeoutMillis(timeoutMillis) + .maxConnections(maxConnections) + .keepAliveSeconds(keepAliveSeconds) + .build()); + + verify(builderConstruction.constructed().get(0)) + .callTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + assertThat(poolArgs.get()) + .containsExactly(maxConnections, keepAliveSeconds, TimeUnit.SECONDS); + } + } + + private static void expireToken(ManualRefreshTokenProvider tokenProvider) throws Exception { + Field tokenField = ManualRefreshTokenProvider.class.getDeclaredField("token"); + tokenField.setAccessible(true); + Token token = (Token) tokenField.get(tokenProvider); + tokenField.set( + tokenProvider, new Token(token.getAccessToken(), token.getTokenType(), Instant.EPOCH)); + } +} diff --git a/src/test/java/com/incognia/api/clients/TokenAwareDispatcher.java b/src/test/java/com/incognia/api/clients/TokenAwareDispatcher.java index 4c0da281..3e62c9be 100644 --- a/src/test/java/com/incognia/api/clients/TokenAwareDispatcher.java +++ b/src/test/java/com/incognia/api/clients/TokenAwareDispatcher.java @@ -182,7 +182,7 @@ private MockResponse handleTokenRequest(@NotNull RecordedRequest request) { assertThat(body).isEqualTo("grant_type=client_credentials"); String[] idAndSecret = new String( - Base64.getUrlDecoder().decode(authorizationHeader.split(" ")[1]), + Base64.getDecoder().decode(authorizationHeader.split(" ")[1]), StandardCharsets.UTF_8) .split(":", 2); String requestClientId = idAndSecret[0]; diff --git a/src/test/java/com/incognia/api/clients/TokenAwareNetworkingClientTest.java b/src/test/java/com/incognia/api/clients/TokenAwareNetworkingClientTest.java index a40e2d36..05e66c66 100644 --- a/src/test/java/com/incognia/api/clients/TokenAwareNetworkingClientTest.java +++ b/src/test/java/com/incognia/api/clients/TokenAwareNetworkingClientTest.java @@ -2,13 +2,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import com.incognia.api.ProjectVersion; +import com.incognia.common.Token; import com.incognia.common.exceptions.IncogniaAPIException; import com.incognia.common.exceptions.IncogniaException; +import com.incognia.common.exceptions.TokenExpiredException; +import com.incognia.common.exceptions.TokenNotFoundException; import com.incognia.fixtures.TestRequestBody; import com.incognia.fixtures.TestResponseBody; import java.io.IOException; +import java.lang.reflect.Field; +import java.time.Instant; import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -22,14 +29,25 @@ class TokenAwareNetworkingClientTest { private final String CLIENT_ID = "client-id"; private final String CLIENT_SECRET = "client-secret"; private TokenAwareNetworkingClient client; + private ManualRefreshTokenProvider manualRefreshTokenProvider; + private TokenAwareNetworkingClient manualClient; private MockWebServer mockServer; @BeforeEach void setUp() { mockServer = new MockWebServer(); + OkHttpClient httpClient = new OkHttpClient(); client = new TokenAwareNetworkingClient( - new OkHttpClient(), mockServer.url("").toString(), CLIENT_ID, CLIENT_SECRET); + httpClient, mockServer.url("").toString(), CLIENT_ID, CLIENT_SECRET); + manualRefreshTokenProvider = + new ManualRefreshTokenProvider( + CLIENT_ID, + CLIENT_SECRET, + new NetworkingClient(httpClient, mockServer.url("").toString())); + manualClient = + new TokenAwareNetworkingClient( + httpClient, mockServer.url("").toString(), manualRefreshTokenProvider); } @AfterEach @@ -39,13 +57,7 @@ void tearDown() throws IOException { @Test @DisplayName("should call the api with the same valid token") - void testDoPost_whenNoTokenExistsYetAndCredentialsAreValid() - throws IncogniaException, InterruptedException { - - synchronized (this) { - wait(15000); // Wait for token to expire (15 seconds) - } - + void testDoPost_whenNoTokenExistsYetAndCredentialsAreValid() throws IncogniaException { TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); mockServer.setDispatcher(dispatcher); @@ -82,6 +94,22 @@ void testDoPost_whenSendingRequest_shouldUseUpdatedUserAgent() assertThat(userAgent).doesNotContain("/v" + ProjectVersion.PROJECT_VERSION); } + @Test + @DisplayName("should use a manually refreshed token without refreshing during the API call") + void testDoPost_whenUsingManualRefreshProvider_shouldReuseTheCachedToken() + throws IncogniaException { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + manualRefreshTokenProvider.refresh(); + + TestResponseBody testResponseBody = + manualClient.doPost( + "api/v2/onboarding", new TestRequestBody("my-id", 1234), TestResponseBody.class); + + assertThat(testResponseBody.getName()).isEqualTo("my awesome name"); + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); + } + @Test @DisplayName("should get a 401 error") void testDoPost_whenNoTokenExistsYetAndCredentialsAreInvalid() { @@ -102,4 +130,107 @@ void testDoPost_whenNoTokenExistsYetAndCredentialsAreInvalid() { .extracting(IncogniaAPIException::getStatusCode) .isEqualTo(401)); } + + @Test + @DisplayName("should fail before issuing the API call when no manual token exists") + void testDoPost_whenUsingManualRefreshProviderWithoutAToken_shouldFailFast() { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + + assertThatThrownBy( + () -> + manualClient.doPost( + "api/v2/onboarding", + new TestRequestBody("my-id", 1234), + TestResponseBody.class)) + .isInstanceOf(TokenNotFoundException.class); + + assertThat(dispatcher.getTokenRequestCount()).isZero(); + } + + @Test + @DisplayName("should fail before issuing the API call when the manual token is expired") + void testDoPost_whenUsingManualRefreshProviderWithExpiredToken_shouldFailFast() throws Exception { + TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); + mockServer.setDispatcher(dispatcher); + manualRefreshTokenProvider.refresh(); + + Field tokenField = ManualRefreshTokenProvider.class.getDeclaredField("token"); + tokenField.setAccessible(true); + Token token = (Token) tokenField.get(manualRefreshTokenProvider); + tokenField.set( + manualRefreshTokenProvider, + new Token(token.getAccessToken(), token.getTokenType(), Instant.EPOCH)); + + assertThatThrownBy( + () -> + manualClient.doPost( + "api/v2/onboarding", + new TestRequestBody("my-id", 1234), + TestResponseBody.class)) + .isInstanceOf(TokenExpiredException.class); + + assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); + } + + @Test + @DisplayName("should fail with a clear message when custom token provider returns null") + void testDoPost_whenCustomTokenProviderReturnsNull_shouldFailWithClearMessage() + throws IncogniaException { + TokenProvider tokenProvider = mock(TokenProvider.class); + doReturn(null).when(tokenProvider).getToken(); + TokenAwareNetworkingClient clientWithInvalidTokenProvider = + new TokenAwareNetworkingClient( + new OkHttpClient(), mockServer.url("").toString(), tokenProvider); + + assertThatThrownBy( + () -> + clientWithInvalidTokenProvider.doPost( + "api/v2/onboarding", + new TestRequestBody("my-id", 1234), + TestResponseBody.class)) + .isInstanceOf(IncogniaException.class) + .hasMessage("token provider returned a null token"); + } + + @Test + @DisplayName("should fail with a clear message when custom token provider returns no token type") + void testDoPost_whenCustomTokenProviderReturnsTokenWithoutType_shouldFailWithClearMessage() + throws IncogniaException { + TokenProvider tokenProvider = mock(TokenProvider.class); + doReturn(new Token("token", "", Instant.now().plusSeconds(10))).when(tokenProvider).getToken(); + TokenAwareNetworkingClient clientWithInvalidTokenProvider = + new TokenAwareNetworkingClient( + new OkHttpClient(), mockServer.url("").toString(), tokenProvider); + + assertThatThrownBy( + () -> + clientWithInvalidTokenProvider.doPost( + "api/v2/onboarding", + new TestRequestBody("my-id", 1234), + TestResponseBody.class)) + .isInstanceOf(IncogniaException.class) + .hasMessage("token provider returned a token without token type"); + } + + @Test + @DisplayName( + "should fail with a clear message when custom token provider returns no access token") + void testDoPost_whenCustomTokenProviderReturnsTokenWithoutAccessToken_shouldFailWithClearMessage() + throws IncogniaException { + TokenProvider tokenProvider = mock(TokenProvider.class); + doReturn(new Token("", "Bearer", Instant.now().plusSeconds(10))).when(tokenProvider).getToken(); + TokenAwareNetworkingClient clientWithInvalidTokenProvider = + new TokenAwareNetworkingClient( + new OkHttpClient(), mockServer.url("").toString(), tokenProvider); + + assertThatThrownBy( + () -> + clientWithInvalidTokenProvider.doPost( + "api/v2/onboarding", + new TestRequestBody("my-id", 1234), + TestResponseBody.class)) + .isInstanceOf(IncogniaException.class) + .hasMessage("token provider returned a token without access token"); + } } diff --git a/src/test/java/com/incognia/api/clients/TokenProviderTest.java b/src/test/java/com/incognia/api/clients/TokenProviderTest.java deleted file mode 100644 index f58cd935..00000000 --- a/src/test/java/com/incognia/api/clients/TokenProviderTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.incognia.api.clients; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.incognia.common.exceptions.IncogniaException; -import java.io.IOException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import okhttp3.OkHttpClient; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TokenProviderTest { - private final String CLIENT_ID = "client-id"; - private final String CLIENT_SECRET = "client-secret"; - private final String DIFFERENT_CLIENT_ID = "different-client-id"; - private final String DIFFERENT_CLIENT_SECRET = "different-client-secret"; - private MockWebServer mockServer; - private TokenProvider tokenProvider; - private TokenProvider differentTokenProvider; - - @BeforeEach - void setUp() { - mockServer = new MockWebServer(); - tokenProvider = - new TokenProvider( - CLIENT_ID, - CLIENT_SECRET, - new NetworkingClient(new OkHttpClient(), mockServer.url("").toString())); - differentTokenProvider = - new TokenProvider( - DIFFERENT_CLIENT_ID, - DIFFERENT_CLIENT_SECRET, - new NetworkingClient(new OkHttpClient(), mockServer.url("").toString())); - } - - @AfterEach - void tearDown() throws IOException { - mockServer.shutdown(); - } - - @Test - public void testGetToken_whenTokenIsNotNullAndExpired_shouldUpdateToken() - throws IncogniaException, InterruptedException { - TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); - mockServer.setDispatcher(dispatcher); - - TokenResponse token = tokenProvider.getToken(); - assertThat(token).isNotNull(); - - synchronized (this) { - wait(15000); // Wait for token to expire (15 seconds) - } - - tokenProvider.getToken(); - - assertThat(dispatcher.getTokenRequestCount()).isEqualTo(2); - } - - @Test - public void testGetToken_whenTokenIsNotExpired_shouldNotUpdateToken() - throws IncogniaException, InterruptedException { - synchronized (this) { - wait(15000); // Wait for token to expire (15 seconds) - } - TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); - mockServer.setDispatcher(dispatcher); - - TokenResponse token = tokenProvider.getToken(); - assertThat(token).isNotNull(); - - tokenProvider.getToken(); - - assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); - } - - @Test - public void testGetToken_whenMultipleCredentialsExists_shouldReturnDifferentTokens() - throws IncogniaException, InterruptedException { - synchronized (this) { - wait(15000); // Wait for token to expire (15 seconds) - } - TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); - mockServer.setDispatcher(dispatcher); - - TokenResponse token = tokenProvider.getToken(); - assertThat(token).isNotNull(); - - TokenAwareDispatcher dispatcher2 = - new TokenAwareDispatcher(DIFFERENT_CLIENT_ID, DIFFERENT_CLIENT_SECRET); - mockServer.setDispatcher(dispatcher2); - - TokenResponse anotherToken = differentTokenProvider.getToken(); - assertThat(token).isNotNull(); - assertThat(anotherToken).isNotSameAs(token); - } - - @Test - public void testGetToken_whenCalledConcurrent_shouldCallDispatcherOnlyOnce() - throws InterruptedException { - synchronized (this) { - wait(15000); // Wait for token to expire (15 seconds) - } - TokenAwareDispatcher dispatcher = new TokenAwareDispatcher(CLIENT_ID, CLIENT_SECRET); - mockServer.setDispatcher(dispatcher); - - int numThreads = 10; - ExecutorService executor = Executors.newFixedThreadPool(numThreads); - for (int i = 0; i < numThreads; i++) { - executor.submit(() -> tokenProvider.getToken()); - } - - executor.shutdown(); - executor.awaitTermination(10, TimeUnit.SECONDS); - assertThat(dispatcher.getTokenRequestCount()).isEqualTo(1); - } -}