diff --git a/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitBucket.java b/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitBucket.java index 948da80..5f5e02a 100644 --- a/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitBucket.java +++ b/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitBucket.java @@ -14,11 +14,6 @@ */ public class RateLimitBucket { - // The key is the subscription key, as global rate-limits are shared across the same account. - private static final Map globalRateLimitResetTimestamp = new ConcurrentHashMap<>(); - - private final AzureApiImpl api; - private final ConcurrentLinkedQueue> requestQueue = new ConcurrentLinkedQueue<>(); private final RestEndpoint endpoint; @@ -30,25 +25,14 @@ public class RateLimitBucket { /** * Creates a RateLimitBucket for the given endpoint / parameter combination. * - * @param api The api/shard to use. * @param endpoint The REST endpoint the rate-limit is tracked for. * @param majorUrlParameter The url parameter this bucket is specific for. Maybe null. */ - public RateLimitBucket(AzureApi api, RestEndpoint endpoint, String majorUrlParameter) { - this.api = (AzureApiImpl) api; + public RateLimitBucket(RestEndpoint endpoint, String majorUrlParameter) { this.endpoint = endpoint; this.majorUrlParameter = majorUrlParameter; } - /** - * Sets a global rate-limit. - * - * @param api A azure api instance. - * @param resetTimestamp The reset timestamp of the global rate-limit. - */ - public static void setGlobalRateLimitResetTimestamp(AzureApi api, long resetTimestamp) { - globalRateLimitResetTimestamp.put(api.getSubscriptionKey(), resetTimestamp); - } /** * Adds the given request to the bucket's queue. @@ -97,8 +81,7 @@ public void setRateLimitResetTimestamp(long rateLimitResetTimestamp) { * @return The time in seconds how long you have to wait till there's space in the bucket again. */ public int getTimeTillSpaceGetsAvailable() { - long globalRLResetTimestamp = - RateLimitBucket.globalRateLimitResetTimestamp.getOrDefault(api.getSubscriptionKey(), 0L); + long globalRLResetTimestamp = 0L; long timestamp = System.currentTimeMillis(); if (rateLimitRemaining > 0 && (globalRLResetTimestamp - timestamp) <= 0) { return 0; diff --git a/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitManager.java b/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitManager.java index ac409ab..5d56d74 100644 --- a/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitManager.java +++ b/src/main/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitManager.java @@ -7,256 +7,229 @@ import com.github.brenoepics.at4j.util.rest.RestRequestHandler; import com.github.brenoepics.at4j.util.rest.RestRequestResponseInformationImpl; import com.github.brenoepics.at4j.util.rest.RestRequestResult; + import java.util.HashSet; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Function; + import okhttp3.Response; import org.apache.logging.log4j.Logger; -/** This class manages rate-limits and keeps track of them. */ +/** + * This class manages rate-limits and keeps track of them. + */ public class RateLimitManager { - /** The (logger) of this class. */ - private static final Logger logger = LoggerUtil.getLogger(RateLimitManager.class); - - /** The Azure API instance for this rate-limit manager. */ - private final AzureApiImpl api; - - /** All buckets. */ - private final Set> buckets = new HashSet<>(); - - /** - * Creates a new rate-limit manager. - * - * @param api The azure api instance for this rate-limit manager. - */ - public RateLimitManager(AzureApiImpl api) { - this.api = api; - } - - /** - * Queues the given request. This method is automatically called when using {@link - * RestRequest#execute(Function)}! - * - * @param request The request to queue. - */ - public void queueRequest(RestRequest request) { - Optional> searchBucket = searchBucket(request); - - if (!searchBucket.isPresent()) { - return; - } - - final RateLimitBucket bucket = searchBucket.get(); - - api.getThreadPool() - .getExecutorService() - .submit( - () -> { - RestRequest currentRequest = bucket.peekRequestFromQueue(); - RestRequestResult result = null; - long responseTimestamp = System.currentTimeMillis(); - while (currentRequest != null) { - RestRequestHandler newResult = - handleCurrentRequest(result, currentRequest, bucket, responseTimestamp); - result = newResult.getResult(); - currentRequest = newResult.getCurrentRequest(); - responseTimestamp = newResult.getResponseTimestamp(); - } - }); - } - - /** - * Handles the current request. - * - * @param result The result of the previous request. - * @param currentRequest The current request. - * @param bucket The bucket the request belongs to. - * @param responseTimestamp The timestamp directly after the response finished. - * @return The result of the current request. - */ - private RestRequestHandler handleCurrentRequest( - RestRequestResult result, - RestRequest currentRequest, - RateLimitBucket bucket, - long responseTimestamp) { - try { - waitUntilSpaceGetsAvailable(bucket); - result = currentRequest.executeBlocking(); - responseTimestamp = System.currentTimeMillis(); - } catch (Exception e) { - responseTimestamp = System.currentTimeMillis(); - if (currentRequest.getResult().isDone()) { - logger.warn( - "Received exception for a request that is already done. This should not be able to" - + " happen!", - e); - } - - if (e instanceof AzureException) { - result = mapAzureException(e); - } - - currentRequest.getResult().completeExceptionally(e); - } finally { - try { - // Handle the response - handleResponse(currentRequest, result, bucket, responseTimestamp); - } catch (Exception e) { - logger.warn("Encountered unexpected exception.", e); - } - - // The request didn't finish, so let's try again - if (currentRequest.getResult().isDone()) { - currentRequest = retryRequest(bucket); - } - } - - return new RestRequestHandler<>(result, currentRequest, responseTimestamp); - } - - /** - * Waits until space gets available in the given bucket. - * - * @param bucket The bucket to wait for. - */ - private void waitUntilSpaceGetsAvailable(RateLimitBucket bucket) { - int sleepTime = bucket.getTimeTillSpaceGetsAvailable(); - if (sleepTime > 0) { - logger.debug( - "Delaying requests to {} for {}ms to prevent hitting rate-limits", bucket, sleepTime); - } - - while (sleepTime > 0) { - try { - Thread.sleep(sleepTime); - } catch (InterruptedException e) { - logger.warn("We got interrupted while waiting for a rate limit!", e); - Thread.currentThread().interrupt(); // Re-interrupt the thread - } - // Update in case something changed (e.g., because we hit a global rate-limit) - sleepTime = bucket.getTimeTillSpaceGetsAvailable(); - } - } - - /** - * Retries the request of the given bucket. - * - * @param bucket The bucket to retry the request for. - * @return The request that was retried. - */ - private RestRequest retryRequest(RateLimitBucket bucket) { - synchronized (buckets) { - bucket.pollRequestFromQueue(); - RestRequest request = bucket.peekRequestFromQueue(); - if (request == null) { - buckets.remove(bucket); - } - - return request; - } - } - - private RestRequestResult mapAzureException(Throwable t) { - return ((AzureException) t) - .getResponse() - .map(RestRequestResponseInformationImpl.class::cast) - .map(RestRequestResponseInformationImpl::getRestRequestResult) - .orElse(null); - } - - /** - * Searches for a bucket that fits to the given request and adds it to the queue. - * - * @param request The request. - * @return The bucket that fits to the request. - */ - private Optional> searchBucket(RestRequest request) { - synchronized (buckets) { - RateLimitBucket bucket = - buckets.stream() - .filter( - b -> b.equals(request.getEndpoint(), request.getMajorUrlParameter().orElse(null))) - .findAny() - .orElseGet( - () -> - new RateLimitBucket<>( - api, request.getEndpoint(), request.getMajorUrlParameter().orElse(null))); - - // Check if it is already in the queue, send not present - if (bucket.peekRequestFromQueue() != null) { - return Optional.empty(); - } - - // Add the bucket to the set of buckets (does nothing if it's already in the set) - buckets.add(bucket); - - // Add the request to the bucket's queue - bucket.addRequestToQueue(request); - return Optional.of(bucket); - } - } - - /** - * Updates the rate-limit information and sets the result if the request was successful. - * - * @param request The request. - * @param result The result of the request. - * @param bucket The bucket the request belongs to. - * @param responseTimestamp The timestamp directly after the response finished. - */ - private void handleResponse( - RestRequest request, - RestRequestResult result, - RateLimitBucket bucket, - long responseTimestamp) { - if (result == null || result.getResponse() == null) { - return; - } - - Response response = result.getResponse(); - int remaining = - Integer.parseInt(Objects.requireNonNull(response.header("X-RateLimit-Remaining", "1"))); - long reset = - (long) - (Double.parseDouble(Objects.requireNonNull(response.header("X-RateLimit-Reset", "0"))) - * 1000); - - // Check if we received a 429 response - if (result.getResponse().code() != 429) { - // Check if we didn't already complete it exceptionally. - CompletableFuture> requestResult = request.getResult(); - if (!requestResult.isDone()) { - requestResult.complete(result); - } - - // Update bucket information - bucket.setRateLimitRemaining(remaining); - bucket.setRateLimitResetTimestamp(reset); - return; - } - - if (response.header("Via") == null) { - logger.warn( - "Hit a CloudFlare API ban! This means you were sending a very large amount of invalid" - + " requests."); - long retryAfter = - Long.parseLong(Objects.requireNonNull(response.header("Retry-after"))) * 1000; - RateLimitBucket.setGlobalRateLimitResetTimestamp(api, responseTimestamp + retryAfter); - return; - } - - long retryAfter = - result.getJsonBody().isNull() - ? 0 - : (long) (result.getJsonBody().get("retry_after").asDouble() * 1000); - logger.debug("Received a 429 response from Azure! Recalculating time offset..."); - - // Update the bucket information - bucket.setRateLimitRemaining(0); - bucket.setRateLimitResetTimestamp(responseTimestamp + retryAfter); - } + /** + * The (logger) of this class. + */ + private static final Logger logger = LoggerUtil.getLogger(RateLimitManager.class); + + /** + * The Azure API instance for this rate-limit manager. + */ + private final AzureApiImpl api; + + /** + * All buckets. + */ + private final Set> buckets = new HashSet<>(); + + /** + * Creates a new rate-limit manager. + * + * @param api The azure api instance for this rate-limit manager. + */ + public RateLimitManager(AzureApiImpl api) { + this.api = api; + } + + /** + * Queues the given request. This method is automatically called when using {@link + * RestRequest#execute(Function)}! + * + * @param request The request to queue. + */ + public void queueRequest(RestRequest request) { + Optional> searchBucket = searchBucket(request); + + if (!searchBucket.isPresent()) { + return; + } + + final RateLimitBucket bucket = searchBucket.get(); + + api.getThreadPool().getExecutorService().submit(() -> { + RestRequest currentRequest = bucket.peekRequestFromQueue(); + RestRequestResult result = null; + long responseTimestamp = System.currentTimeMillis(); + while (currentRequest != null) { + RestRequestHandler newResult = handleCurrentRequest(result, currentRequest, bucket, responseTimestamp); + result = newResult.getResult(); + currentRequest = newResult.getCurrentRequest(); + responseTimestamp = newResult.getResponseTimestamp(); + } + }); + } + + /** + * Handles the current request. + * + * @param result The result of the previous request. + * @param currentRequest The current request. + * @param bucket The bucket the request belongs to. + * @param responseTimestamp The timestamp directly after the response finished. + * @return The result of the current request. + */ + RestRequestHandler handleCurrentRequest(RestRequestResult result, RestRequest currentRequest, RateLimitBucket bucket, long responseTimestamp) { + try { + waitUntilSpaceGetsAvailable(bucket); + result = currentRequest.executeBlocking(); + responseTimestamp = System.currentTimeMillis(); + } catch (Exception e) { + responseTimestamp = System.currentTimeMillis(); + if (currentRequest.getResult().isDone()) { + logger.warn("Received exception for a request that is already done. This should not be able to" + " happen!", e); + } + + if (e instanceof AzureException) { + result = mapAzureException(e); + } + + currentRequest.getResult().completeExceptionally(e); + } finally { + try { + // Handle the response + handleResponse(currentRequest, result, bucket, responseTimestamp); + } catch (Exception e) { + logger.warn("Encountered unexpected exception.", e); + } + + // The request didn't finish, so let's try again + if (currentRequest.getResult().isDone()) { + currentRequest = retryRequest(bucket); + } + } + + return new RestRequestHandler<>(result, currentRequest, responseTimestamp); + } + + /** + * Waits until space gets available in the given bucket. + * + * @param bucket The bucket to wait for. + */ + void waitUntilSpaceGetsAvailable(RateLimitBucket bucket) { + int sleepTime = bucket.getTimeTillSpaceGetsAvailable(); + if (sleepTime > 0) { + logger.debug("Delaying requests to {} for {}ms to prevent hitting rate-limits", bucket, sleepTime); + } + + while (sleepTime > 0) { + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + logger.warn("We got interrupted while waiting for a rate limit!", e); + Thread.currentThread().interrupt(); // Re-interrupt the thread + } + // Update in case something changed (e.g., because we hit a global rate-limit) + sleepTime = bucket.getTimeTillSpaceGetsAvailable(); + } + } + + /** + * Retries the request of the given bucket. + * + * @param bucket The bucket to retry the request for. + * @return The request that was retried. + */ + RestRequest retryRequest(RateLimitBucket bucket) { + synchronized (buckets) { + bucket.pollRequestFromQueue(); + RestRequest request = bucket.peekRequestFromQueue(); + if (request == null) { + buckets.remove(bucket); + } + + return request; + } + } + + private RestRequestResult mapAzureException(Throwable t) { + return ((AzureException) t).getResponse().map(RestRequestResponseInformationImpl.class::cast).map(RestRequestResponseInformationImpl::getRestRequestResult).orElse(null); + } + + /** + * Searches for a bucket that fits to the given request and adds it to the queue. + * + * @param request The request. + * @return The bucket that fits to the request. + */ + Optional> searchBucket(RestRequest request) { + synchronized (buckets) { + RateLimitBucket bucket = buckets.stream().filter(b -> b.equals(request.getEndpoint(), request.getMajorUrlParameter().orElse(null))).findAny().orElseGet(() -> new RateLimitBucket<>(request.getEndpoint(), request.getMajorUrlParameter().orElse(null))); + + // Check if it is already in the queue, send not present + if (bucket.peekRequestFromQueue() != null) { + return Optional.empty(); + } + + // Add the bucket to the set of buckets (does nothing if it's already in the set) + buckets.add(bucket); + + // Add the request to the bucket's queue + bucket.addRequestToQueue(request); + return Optional.of(bucket); + } + } + + /** + * Updates the rate-limit information and sets the result if the request was successful. + * + * @param request The request. + * @param result The result of the request. + * @param bucket The bucket the request belongs to. + * @param responseTimestamp The timestamp directly after the response finished. + */ + void handleResponse(RestRequest request, RestRequestResult result, RateLimitBucket bucket, long responseTimestamp) { + if (result == null || result.getResponse() == null) { + return; + } + + Response response = result.getResponse(); + int remaining = Integer.parseInt(Objects.requireNonNull(response.header("X-RateLimit-Remaining", "1"))); + long reset = (long) (Double.parseDouble(Objects.requireNonNull(response.header("X-RateLimit-Reset", "0"))) * 1000); + + // Check if we received a 429 response + if (result.getResponse().code() != 429) { + // Check if we didn't already complete it exceptionally. + CompletableFuture> requestResult = request.getResult(); + if (!requestResult.isDone()) { + requestResult.complete(result); + } + + // Update bucket information + bucket.setRateLimitRemaining(remaining); + bucket.setRateLimitResetTimestamp(reset); + return; + } + + if (response.header("Via") == null) { + logger.warn("Hit a CloudFlare API ban! This means you were sending a very large amount of invalid" + " requests."); + int retryAfter = Integer.parseInt(Objects.requireNonNull(response.header("Retry-after"))) * 1000; + bucket.setRateLimitRemaining(retryAfter); + bucket.setRateLimitResetTimestamp(responseTimestamp + retryAfter); + return; + } + + long retryAfter = result.getJsonBody().isNull() ? 0 : (long) (result.getJsonBody().get("retry_after").asDouble() * 1000); + logger.debug("Received a 429 response from Azure! Recalculating time offset..."); + + // Update the bucket information + bucket.setRateLimitRemaining(0); + bucket.setRateLimitResetTimestamp(responseTimestamp + retryAfter); + } } diff --git a/src/test/java/com/github/brenoepics/at4j/AzureApiBuilderTest.java b/src/test/java/com/github/brenoepics/at4j/AzureApiBuilderTest.java new file mode 100644 index 0000000..e3f2982 --- /dev/null +++ b/src/test/java/com/github/brenoepics/at4j/AzureApiBuilderTest.java @@ -0,0 +1,43 @@ +package com.github.brenoepics.at4j; + +import org.junit.jupiter.api.Test; +import com.github.brenoepics.at4j.azure.BaseURL; + +import static org.junit.jupiter.api.Assertions.*; + +class AzureApiBuilderTest { + + @Test + void shouldSetGlobalBaseUrlByDefault() { + AzureApiBuilder builder = new AzureApiBuilder(); + AzureApi api = builder.build(); + assertEquals(BaseURL.GLOBAL, api.getBaseURL()); + } + + @Test + void shouldSetBaseUrlWhenProvided() { + AzureApiBuilder builder = new AzureApiBuilder(); + AzureApi api = builder.baseURL(BaseURL.EUROPE).build(); + assertEquals(BaseURL.EUROPE, api.getBaseURL()); + } + + @Test + void shouldThrowExceptionWhenSubscriptionKeyIsNull() { + AzureApiBuilder builder = new AzureApiBuilder(); + assertThrows(NullPointerException.class, builder::build); + } + + @Test + void shouldSetSubscriptionKeyWhenProvided() { + AzureApiBuilder builder = new AzureApiBuilder(); + AzureApi api = builder.setKey("testKey").build(); + assertEquals("testKey", api.getSubscriptionKey()); + } + + @Test + void shouldSetSubscriptionRegionWhenProvided() { + AzureApiBuilder builder = new AzureApiBuilder(); + AzureApi api = builder.region("brazilsouth").build(); + assertEquals("brazilsouth", api.getSubscriptionRegion().orElse(null)); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/brenoepics/at4j/azure/lang/LanguageTest.java b/src/test/java/com/github/brenoepics/at4j/azure/lang/LanguageTest.java new file mode 100644 index 0000000..a56d87b --- /dev/null +++ b/src/test/java/com/github/brenoepics/at4j/azure/lang/LanguageTest.java @@ -0,0 +1,83 @@ +package com.github.brenoepics.at4j.azure.lang; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class LanguageTest { + + private Language language; + + @BeforeEach + void setUp() { + language = new Language("en", "English", "English", LanguageDirection.LTR); + } + + @Test + void ofJSON_createsLanguageFromJson() { + ObjectNode json = JsonNodeFactory.instance.objectNode(); + json.put("name", "English"); + json.put("nativeName", "English"); + json.put("dir", "LTR"); + + Language result = Language.ofJSON("en", json); + + assertEquals("en", result.getCode()); + assertEquals("English", result.getName()); + assertEquals("English", result.getNativeName()); + assertEquals(LanguageDirection.LTR, result.getDir()); + } + + @Test + void toString_returnsCorrectFormat() { + String expected = "Language{code='en', name='English', nativeName='English', direction=LTR}"; + assertEquals(expected, language.toString()); + } + + @Test + void getCode_returnsCorrectCode() { + assertEquals("en", language.getCode()); + } + + @Test + void setCode_updatesCode() { + language.setCode("fr"); + assertEquals("fr", language.getCode()); + } + + @Test + void getName_returnsCorrectName() { + assertEquals("English", language.getName()); + } + + @Test + void setName_updatesName() { + language.setName("French"); + assertEquals("French", language.getName()); + } + + @Test + void getNativeName_returnsCorrectNativeName() { + assertEquals("English", language.getNativeName()); + } + + @Test + void setNativeName_updatesNativeName() { + language.setNativeName("Anglais"); + assertEquals("Anglais", language.getNativeName()); + } + + @Test + void getDir_returnsCorrectDirection() { + assertEquals(LanguageDirection.LTR, language.getDir()); + } + + @Test + void setDir_updatesDirection() { + language.setDir(LanguageDirection.RTL); + assertEquals(LanguageDirection.RTL, language.getDir()); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitManagerTest.java b/src/test/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitManagerTest.java new file mode 100644 index 0000000..b9c853b --- /dev/null +++ b/src/test/java/com/github/brenoepics/at4j/core/ratelimit/RateLimitManagerTest.java @@ -0,0 +1,150 @@ +package com.github.brenoepics.at4j.core.ratelimit; + +import static org.junit.jupiter.api.Assertions.*; + +import com.github.brenoepics.at4j.core.AzureApiImpl; +import com.github.brenoepics.at4j.core.exceptions.AzureException; +import com.github.brenoepics.at4j.core.thread.ThreadPool; +import com.github.brenoepics.at4j.util.rest.RestRequest; +import com.github.brenoepics.at4j.util.rest.RestRequestResult; +import okhttp3.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +import static org.mockito.Mockito.*; + +class RateLimitManagerTest { + + @Mock + private AzureApiImpl api; + @Mock + private RestRequest request; + @Mock + private RestRequestResult result; + @Mock + private Response response; + @Mock + private RateLimitBucket bucket; + + private RateLimitManager rateLimitManager; + + @Mock + private ThreadPool threadPool; + @Mock + private ExecutorService executorService; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + rateLimitManager = new RateLimitManager<>(api); + + when(api.getThreadPool()).thenReturn(threadPool); + when(threadPool.getExecutorService()).thenReturn(executorService); + + when(response.header("X-RateLimit-Remaining", "1")).thenReturn("10"); + when(response.header("X-RateLimit-Reset", "0")).thenReturn("1000"); + } + + @Test + void queuesRequestWhenBucketIsPresent() { + when(request.getMajorUrlParameter()).thenReturn(Optional.empty()); + when(bucket.peekRequestFromQueue()).thenReturn(null); + + rateLimitManager.queueRequest(request); + + verify(executorService, times(1)).submit(any(Runnable.class)); + } + + @Test + void retriesRequestWhenBucketIsNotEmpty() { + when(bucket.peekRequestFromQueue()).thenReturn(request); + + RestRequest retriedRequest = rateLimitManager.retryRequest(bucket); + + assertEquals(request, retriedRequest); + } + + @Test + void doesNotRetryRequestWhenBucketIsEmpty() { + when(bucket.peekRequestFromQueue()).thenReturn(null); + + RestRequest retriedRequest = rateLimitManager.retryRequest(bucket); + + assertNull(retriedRequest); + } + + @Test + void doesNotHandleResponseWhenResultIsNull() { + assertDoesNotThrow(() -> rateLimitManager.handleResponse(request, null, bucket, System.currentTimeMillis())); + } + + @Test + void queuesRequestWhenBucketIsNotPresent() { + when(request.getMajorUrlParameter()).thenReturn(Optional.empty()); + when(bucket.peekRequestFromQueue()).thenReturn(request); + + + rateLimitManager.queueRequest(request); + assertDoesNotThrow(() -> rateLimitManager.handleResponse(request, result, bucket, System.currentTimeMillis())); + } + + @Test + void handleResponseUpdatesBucketInformationWhenResponseCodeIsNot429() { + when(response.header("X-RateLimit-Remaining", "1")).thenReturn("10"); + when(response.header("X-RateLimit-Reset", "0")).thenReturn("1000"); + when(result.getResponse()).thenReturn(response); + when(response.code()).thenReturn(200); + when(request.getResult()).thenReturn(CompletableFuture.completedFuture(result)); + + rateLimitManager.handleResponse(request, result, bucket, System.currentTimeMillis()); + + verify(bucket, times(1)).setRateLimitRemaining(10); + verify(bucket, times(1)).setRateLimitResetTimestamp(anyLong()); + } + + @Test + void handleResponseDoesNotUpdateBucketInformationWhenResponseCodeIs429AndViaHeaderIsNull() { + when(result.getResponse()).thenReturn(response); + when(response.code()).thenReturn(429); + when(response.header("Via")).thenReturn(null); + when(response.header("Retry-after")).thenReturn("1000"); + when(response.header("X-RateLimit-Remaining", "1")).thenReturn("10"); + when(response.header("X-RateLimit-Reset", "0")).thenReturn("1000"); + + assertDoesNotThrow(() -> rateLimitManager.handleResponse(request, result, bucket, System.currentTimeMillis())); + } + + @Test + void handleCurrentRequestThrowsException() throws AzureException, IOException { + when(request.executeBlocking()).thenThrow(new RuntimeException()); + + assertThrows(RuntimeException.class, () -> rateLimitManager.handleCurrentRequest(result, request, bucket, System.currentTimeMillis())); + } + + + @Test + void handleResponseDoesNotUpdateBucketInformationWhenResponseCodeIsNot429AndRequestResultIsDone() { + when(result.getResponse()).thenReturn(response); + when(response.code()).thenReturn(200); + when(request.getResult()).thenReturn(CompletableFuture.completedFuture(result)); + + assertDoesNotThrow(() -> rateLimitManager.handleResponse(request, result, bucket, System.currentTimeMillis())); + } + + @Test + void searchBucketReturnsBucketWhenBucketIsPresentAndRequestQueueIsEmpty() { + when(request.getMajorUrlParameter()).thenReturn(Optional.empty()); + when(bucket.peekRequestFromQueue()).thenReturn(null); + + Optional> searchBucket = rateLimitManager.searchBucket(request); + + assertTrue(searchBucket.isPresent()); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/brenoepics/at4j/core/thread/AT4JThreadFactoryTest.java b/src/test/java/com/github/brenoepics/at4j/core/thread/AT4JThreadFactoryTest.java new file mode 100644 index 0000000..2b83ec6 --- /dev/null +++ b/src/test/java/com/github/brenoepics/at4j/core/thread/AT4JThreadFactoryTest.java @@ -0,0 +1,60 @@ +package com.github.brenoepics.at4j.core.thread; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AT4JThreadFactoryTest { + + @Test + void shouldCreateDaemonThreadWhenDaemonIsTrue() { + // Given + AT4JThreadFactory factory = new AT4JThreadFactory("test-%d", true); + + // When + Thread thread = factory.newThread(() -> {}); + + // Then + assertTrue(thread.isDaemon()); + assertEquals("test-1", thread.getName()); + } + + @Test + void shouldCreateNonDaemonThreadWhenDaemonIsFalse() { + // Given + AT4JThreadFactory factory = new AT4JThreadFactory("test-%d", false); + + // When + Thread thread = factory.newThread(() -> {}); + + // Then + assertFalse(thread.isDaemon()); + assertEquals("test-1", thread.getName()); + } + + @Test + void shouldIncrementThreadNameCounter() { + // Given + AT4JThreadFactory factory = new AT4JThreadFactory("test-%d", true); + + // When + Thread thread1 = factory.newThread(() -> {}); + Thread thread2 = factory.newThread(() -> {}); + + // Then + assertEquals("test-1", thread1.getName()); + assertEquals("test-2", thread2.getName()); + } + + @Test + void shouldHandleNamePatternWithoutCounter() { + // Given + AT4JThreadFactory factory = new AT4JThreadFactory("test", true); + + // When + Thread thread = factory.newThread(() -> {}); + + // Then + assertEquals("test", thread.getName()); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/brenoepics/at4j/core/thread/ThreadPoolImplTest.java b/src/test/java/com/github/brenoepics/at4j/core/thread/ThreadPoolImplTest.java new file mode 100644 index 0000000..23aff5f --- /dev/null +++ b/src/test/java/com/github/brenoepics/at4j/core/thread/ThreadPoolImplTest.java @@ -0,0 +1,73 @@ +package com.github.brenoepics.at4j.core.thread; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +class ThreadPoolImplTest { + + private ThreadPoolImpl threadPool; + + @BeforeEach + void setUp() { + threadPool = new ThreadPoolImpl(); + } + + @Test + void shouldReturnExecutorService() { + ExecutorService executorService = threadPool.getExecutorService(); + assertNotNull(executorService); + } + + @Test + void shouldReturnScheduler() { + ScheduledExecutorService scheduler = threadPool.getScheduler(); + assertNotNull(scheduler); + } + + @Test + void shouldReturnDaemonScheduler() { + ScheduledExecutorService daemonScheduler = threadPool.getDaemonScheduler(); + assertNotNull(daemonScheduler); + } + + @Test + void shouldReturnSingleThreadExecutorService() { + ExecutorService singleThreadExecutorService = threadPool.getSingleThreadExecutorService("TestThread"); + assertNotNull(singleThreadExecutorService); + } + + @Test + void shouldReturnSingleDaemonThreadExecutorService() { + ExecutorService singleDaemonThreadExecutorService = threadPool.getSingleDaemonThreadExecutorService("TestDaemonThread"); + assertNotNull(singleDaemonThreadExecutorService); + } + + @Test + void shouldRemoveAndShutdownSingleThreadExecutorService() { + threadPool.getSingleThreadExecutorService("TestThread"); + assertTrue(threadPool.removeAndShutdownSingleThreadExecutorService("TestThread").isPresent()); + } + + @Test + void shouldNotRemoveNonExistentSingleThreadExecutorService() { + assertFalse(threadPool.removeAndShutdownSingleThreadExecutorService("NonExistentThread").isPresent()); + } + + @Test + void shouldRunAfterGivenDuration() { + assertDoesNotThrow(() -> threadPool.runAfter(() -> null, 1, TimeUnit.SECONDS)); + } + + @Test + void shouldShutdownAllServices() { + threadPool.getSingleThreadExecutorService("TestThread"); + threadPool.getSingleDaemonThreadExecutorService("TestDaemonThread"); + assertDoesNotThrow(() -> threadPool.shutdown()); + } +} \ No newline at end of file