diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 457c4eb04..3fd82c107 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,33 +4,53 @@ on: [push] jobs: build: + name: Build Java ${{ matrix.java-version }} runs-on: ubuntu-latest + strategy: + matrix: + java-version: ['8', '11', '17', '21', '25'] + include: + - java-version: '8' + maven-args: '-Dmaven.test.skip=true -Dspotless.check.skip=true clean package' + - java-version: '11' + maven-args: '-Dspotless.check.skip=true clean package javadoc:javadoc' + - java-version: '17' + maven-args: 'clean package javadoc:javadoc' + - java-version: '21' + maven-args: 'clean package javadoc:javadoc' + - java-version: '25' + maven-args: 'clean package javadoc:javadoc' steps: - uses: actions/checkout@v3 - - name: Set up JDK + - name: Set up JDK ${{ matrix.java-version }} uses: actions/setup-java@v3 with: - java-version: '21' + java-version: ${{ matrix.java-version }} distribution: 'temurin' cache: maven - name: Build with Maven - run: mvn -B -DskipITs clean package javadoc:javadoc --file pom.xml + run: mvn -B -DskipITs ${{ matrix.maven-args }} --file pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 e2e-tests: + name: E2E Java ${{ matrix.java-version }} needs: build runs-on: ubuntu-latest + + strategy: + matrix: + java-version: ['8', '11', '17', '21', '25'] steps: - uses: actions/checkout@v3 - - name: Set up JDK + - name: Set up JDK ${{ matrix.java-version }} uses: actions/setup-java@v3 with: - java-version: '21' + java-version: ${{ matrix.java-version }} distribution: 'temurin' cache: maven - name: Checkout sinch-sdk-mockserver repository diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd0d8b31..e6696bb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,26 @@ All notable changes to the **Sinch Java SDK** are documented in this file. --- +## v2.1.1 patch - 2026-07-21 + +### Build & CI + +- **[tech]** Build across a Java version matrix (`8`, `11`, `17`, `21`, `25`) in GitHub Actions, replacing the single Java 21 build — unit tests run on `11`, `17`, `21`, and `25`, and artifacts are built on Java 8. +- **[tech]** e2e tests across a Java version matrix (`8`, `11`, `17`, `21`, `25`) in GitHub Actions, replacing the single Java 21 validation. +- **[tech]** Apply Spotless / Google Java Format per JDK via version-activated Maven profiles (`spotless-jdk8`, `spotless-jdk11`, `spotless-jdk17`, `spotless-jdk21-plus`), keeping formatting working across the whole matrix. +- **[tech]** Enable `-Dnet.bytebuddy.experimental=true` for Surefire so Mockito/ByteBuddy run on Java 25. +- **[dependency]** Bump `maven-compiler-plugin` to `3.13.0`, Surefire and Failsafe to `3.5.6`, and Mockito to `5.23.0`. +- **[dependency]** Remove unused dependency on `org.apache.maven:maven-model`. +- **[dependency]** Bump `jackson.version` to `2.21.4` to solve a vulnerability in `jackson-databind` (CVE-2026-54513). + + +### Documentation +- **[doc]** README: document the supported Java versions (`8`, `11`, `17`, `21`, `25`) with per-version badges and download links, replacing the generic "Java 8+" badge. + +### Tests +- **[test]** Remove `DateUtilTest` cases for unsupported timezone abbreviations (`MST`, `EST`): as of Java 25 the JDK no longer resolves legacy COMPAT timezone names ([JDK-8325568](https://bugs.openjdk.org/browse/JDK-8325568)). + + ## v2.1 – 2026-07-02 ### Numbers diff --git a/README.md b/README.md index c65232c8c..77f95b375 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Sinch Java SDK -[![Java](https://img.shields.io/badge/Java-8+-orange.svg)](https://www.java.com) +[![Java 8](https://img.shields.io/badge/Java-8-orange.svg)](https://www.oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html) +[![Java 11](https://img.shields.io/badge/Java-11-orange.svg)](https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html) +[![Java 17](https://img.shields.io/badge/Java-17-orange.svg)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) +[![Java 21](https://img.shields.io/badge/Java-21-orange.svg)](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html) +[![Java 25](https://img.shields.io/badge/Java-25-orange.svg)](https://www.oracle.com/java/technologies/downloads/#java25) [![Latest Release](https://img.shields.io/maven-central/v/com.sinch.sdk/sinch-sdk-java?label=sinch&labelColor=FFC658)](https://central.sonatype.com/artifact/com.sinch.sdk/sinch-sdk-java) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/sinch/sinch-sdk-java/blob/main/LICENSE) @@ -28,7 +32,7 @@ For more information on the SDK, refer to the dedicated [Java SDK documentation ## Prerequisites -- [JDK 8 or later](https://www.java.com) +- Java in one of the supported versions - [8](https://www.oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html), [11](https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html), [17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html), [21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html), [25](https://www.oracle.com/java/technologies/downloads/#java25) - [Maven](https://maven.apache.org/) - [Sinch account](https://dashboard.sinch.com/) diff --git a/client/src/main/com/sinch/sdk/auth/adapters/OAuthManager.java b/client/src/main/com/sinch/sdk/auth/adapters/OAuthManager.java index 65b7f2051..81a684ede 100644 --- a/client/src/main/com/sinch/sdk/auth/adapters/OAuthManager.java +++ b/client/src/main/com/sinch/sdk/auth/adapters/OAuthManager.java @@ -9,6 +9,7 @@ import com.sinch.sdk.core.http.HttpMethod; import com.sinch.sdk.core.http.HttpRequest; import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.HttpStatus; import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.core.utils.Pair; import com.sinch.sdk.models.UnifiedCredentials; @@ -17,6 +18,7 @@ import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; import java.util.function.Supplier; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -28,12 +30,17 @@ public class OAuthManager implements AuthManager { public static final String BEARER_AUTHENTICATE_RESPONSE_HEADER_KEYWORD = "www-authenticate"; private static final Logger LOGGER = Logger.getLogger(OAuthManager.class.getName()); private static final String AUTH_KEYWORD = "Bearer"; - private static final int maxRefreshAttempt = 5; + // Total refresh attempts for 429 and other failures; sufficient given the backoff. + protected static final int MAX_REFRESH_ATTEMPT = 3; + + private static final double BACKOFF_BASE_SECONDS = 1.0; + private static final int BACKOFF_GROWTH = 4; + private final ServerConfiguration oAuthServer; private final HttpMapper mapper; private final Supplier httpClientSupplier; private final Map authManagers; - private String token; + private volatile String token; public OAuthManager( UnifiedCredentials credentials, @@ -77,17 +84,25 @@ public void resetToken() { public Collection> getAuthorizationHeaders( String timestamp, String method, String httpContentType, String path, String body) { - if (token == null) { - refreshToken(); + String currentToken = token; + if (currentToken == null) { + synchronized (this) { + currentToken = token; + if (currentToken == null) { + refreshToken(); + currentToken = token; + } + } } - return Collections.singletonList(new Pair<>("Authorization", AUTH_KEYWORD + " " + token)); + return Collections.singletonList( + new Pair<>("Authorization", AUTH_KEYWORD + " " + currentToken)); } private void refreshToken() { int attempt = 0; - while (attempt < maxRefreshAttempt) { - Optional newValue = getNewToken(); + while (attempt < MAX_REFRESH_ATTEMPT) { + Optional newValue = getNewToken(attempt); if (newValue.isPresent()) { token = newValue.get(); return; @@ -97,7 +112,7 @@ private void refreshToken() { throw new ApiAuthException("Unable to get new token"); } - private Optional getNewToken() { + private Optional getNewToken(int attempt) { LOGGER.fine("Refreshing OAuth token"); HttpRequest request = @@ -110,17 +125,58 @@ private Optional getNewToken() { null, Collections.singletonList("application/x-www-form-urlencoded"), Collections.singletonList(SCHEMA_KEYWORD_BASIC)); + HttpResponse httpResponse; + try { + httpResponse = httpClientSupplier.get().invokeAPI(oAuthServer, authManagers, request); + } catch (Exception e) { + throw new ApiAuthException( + "Token refresh failed: network or client error: " + e.getMessage()); + } + if (httpResponse == null) { + throw new ApiAuthException("Token refresh failed: no response received"); + } + + if (httpResponse.getCode() == HttpStatus.TOO_MANY_REQUESTS) { + // Only back off if another attempt will follow; on the last attempt we give up immediately. + if (attempt < MAX_REFRESH_ATTEMPT - 1) { + long sleepMillis = computeBackoffMillis(attempt); + LOGGER.fine( + "Rate limited (HTTP 429) during token refresh, attempt " + + (attempt + 1) + + "/" + + MAX_REFRESH_ATTEMPT + + ", waiting " + + sleepMillis + + "ms before next attempt"); + try { + Thread.sleep(sleepMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ApiAuthException("Token refresh interrupted"); + } + } + return Optional.empty(); + } + + if (!HttpStatus.isSuccessfulStatus(httpResponse.getCode())) { + throw new ApiAuthException("Token refresh failed with HTTP " + httpResponse.getCode()); + } + try { - HttpResponse httpResponse = - httpClientSupplier.get().invokeAPI(oAuthServer, authManagers, request); BearerAuthResponse authResponse = mapper.deserialize(httpResponse, new TypeReference() {}); return Optional.ofNullable(authResponse.getAccessToken()); } catch (Exception e) { - return Optional.empty(); + throw new ApiAuthException( + "Token refresh failed: could not deserialize response: " + e.getMessage()); } } + long computeBackoffMillis(int attempt) { + double maxDelay = BACKOFF_BASE_SECONDS * Math.pow(BACKOFF_GROWTH, attempt); + return (long) (ThreadLocalRandom.current().nextDouble(maxDelay) * 1000); + } + public boolean validateAuthenticatedRequest( String method, String path, Map headers, String jsonPayload) { LOGGER.severe("checkAuthentication not implemented"); diff --git a/client/src/main/com/sinch/sdk/http/HttpClientApache.java b/client/src/main/com/sinch/sdk/http/HttpClientApache.java index bf3f1dad2..365710d60 100644 --- a/client/src/main/com/sinch/sdk/http/HttpClientApache.java +++ b/client/src/main/com/sinch/sdk/http/HttpClientApache.java @@ -263,21 +263,18 @@ private boolean processUnauthorizedResponse( } Collection auths = request.getAuthNames(); - Optional requestSupportBearerAuthentication = + + boolean requestSupportsBearer = auths.stream() - .filter( - f -> - null != authManagersByOasSecuritySchemes.get(f) - && authManagersByOasSecuritySchemes - .get(f) - .getSchema() - .equals(OAuthManager.SCHEMA_KEYWORD_BEARER)) - .findFirst(); - - if (!requestSupportBearerAuthentication.isPresent()) { + .anyMatch( + authName -> { + AuthManager authManager = authManagersByOasSecuritySchemes.get(authName); + return authManager != null + && authManager.getSchema().equals(OAuthManager.SCHEMA_KEYWORD_BEARER); + }); + if (!requestSupportsBearer) { return false; } - // looking for "expired" keyword present in "www-authenticate" header Map> responseHeaders = response.getHeaders(); Collection header = responseHeaders.get(BEARER_AUTHENTICATE_RESPONSE_HEADER_KEYWORD); @@ -320,7 +317,8 @@ private void addFormParams( } MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create(); - if (contentType.stream().noneMatch(cType -> cType.toLowerCase().contains("charset="))) { + if (null == contentType + || contentType.stream().noneMatch(cType -> cType.toLowerCase().contains("charset="))) { multiPartBuilder.setCharset(StandardCharsets.UTF_8); } formParams.forEach((key, value) -> addMultiPart(requestBuilder, multiPartBuilder, key, value)); diff --git a/client/src/test/java/com/sinch/sdk/SinchClientTestIT.java b/client/src/test/java/com/sinch/sdk/SinchClientUserAgentTest.java similarity index 81% rename from client/src/test/java/com/sinch/sdk/SinchClientTestIT.java rename to client/src/test/java/com/sinch/sdk/SinchClientUserAgentTest.java index 1a5bdf7e2..051a2d291 100644 --- a/client/src/test/java/com/sinch/sdk/SinchClientTestIT.java +++ b/client/src/test/java/com/sinch/sdk/SinchClientUserAgentTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; import com.adelean.inject.resources.junit.jupiter.TestWithResources; import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.models.Configuration; @@ -15,7 +16,11 @@ import org.junit.jupiter.api.Test; @TestWithResources -class SinchClientTestIT extends BaseTest { +class SinchClientUserAgentTest extends BaseTest { + + @GivenTextResource("/client/auth/BearerAuthResponse.json") + String bearerAuthResponse; + static MockWebServer mockBackEnd; String serverUrl = String.format("http://localhost:%s", mockBackEnd.getPort()); @@ -24,6 +29,7 @@ class SinchClientTestIT extends BaseTest { .setKeyId("foo") .setKeySecret("foo") .setProjectId("foo") + .setOAuthUrl(serverUrl) .setNumbersContext(NumbersContext.builder().setNumbersUrl(serverUrl).build()) .build(); @@ -43,6 +49,10 @@ static void tearDown() throws IOException { @Test void sdkUserAgent() throws InterruptedException { + mockBackEnd.enqueue( + new MockResponse() + .setBody(bearerAuthResponse) + .addHeader("Content-Type", "application/json")); mockBackEnd.enqueue( new MockResponse().setBody("foo").addHeader("Content-Type", "application/json")); diff --git a/client/src/test/java/com/sinch/sdk/auth/adapters/OAuthManagerTest.java b/client/src/test/java/com/sinch/sdk/auth/adapters/OAuthManagerTest.java index 466859473..e3cd3fea2 100644 --- a/client/src/test/java/com/sinch/sdk/auth/adapters/OAuthManagerTest.java +++ b/client/src/test/java/com/sinch/sdk/auth/adapters/OAuthManagerTest.java @@ -21,6 +21,7 @@ import java.util.Collections; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -117,4 +118,163 @@ void noInfiniteLoopAndException() { () -> authManager.getAuthorizationHeaders(null, null, null, null)); assertEquals(exception.getCode(), 401); } + + @Nested + class RetryOn429WithBackoff { + + private OAuthManager spyAuthManager; + + @BeforeEach + void setup() { + spyAuthManager = + spy( + new OAuthManager( + credentials, + new ServerConfiguration("OAuth url"), + HttpMapper.getInstance(), + () -> httpClient)); + } + + @Test + void retriesOn429ThenSucceeds() { + doReturn(0L).when(spyAuthManager).computeBackoffMillis(anyInt()); + HttpResponse rateLimited = + new HttpResponse(429, "Too Many Requests", Collections.emptyMap(), null); + HttpResponse ok = + new HttpResponse(200, "foo message", null, jsonResponse.getBytes(StandardCharsets.UTF_8)); + + when(httpClient.invokeAPI(any(), any(), any())).thenReturn(rateLimited, rateLimited, ok); + + Collection> headers = + spyAuthManager.getAuthorizationHeaders(null, null, null, null); + + assertNotNull(headers); + verify(httpClient, times(OAuthManager.MAX_REFRESH_ATTEMPT)).invokeAPI(any(), any(), any()); + } + + @Test + void givesUpAfterMaxRetries() { + doReturn(0L).when(spyAuthManager).computeBackoffMillis(anyInt()); + HttpResponse rateLimited = + new HttpResponse(429, "Too Many Requests", Collections.emptyMap(), null); + + when(httpClient.invokeAPI(any(), any(), any())).thenReturn(rateLimited); + + ApiAuthException exception = + assertThrows( + ApiAuthException.class, + () -> spyAuthManager.getAuthorizationHeaders(null, null, null, null)); + assertTrue( + exception.getMessage().startsWith("Unable to get new token"), + "expected the give-up message, got: " + exception.getMessage()); + + verify(httpClient, times(OAuthManager.MAX_REFRESH_ATTEMPT)).invokeAPI(any(), any(), any()); + } + + @Test + void noBackoffOnNon429Failure() { + HttpResponse serverError = + new HttpResponse(500, "Internal Server Error", Collections.emptyMap(), new byte[0]); + when(httpClient.invokeAPI(any(), any(), any())).thenReturn(serverError); + + assertThrows( + ApiAuthException.class, + () -> spyAuthManager.getAuthorizationHeaders(null, null, null, null)); + + verify(spyAuthManager, never()).computeBackoffMillis(anyInt()); + } + } + + @Nested + class FailFastErrors { + + private OAuthManager spyAuthManager; + + @BeforeEach + void setup() { + spyAuthManager = + spy( + new OAuthManager( + credentials, + new ServerConfiguration("OAuth url"), + HttpMapper.getInstance(), + () -> httpClient)); + } + + @Test + void networkErrorThrowsWithoutRetryOrBackoff() { + when(httpClient.invokeAPI(any(), any(), any())) + .thenThrow(new RuntimeException("connection refused")); + + ApiAuthException exception = + assertThrows( + ApiAuthException.class, + () -> spyAuthManager.getAuthorizationHeaders(null, null, null, null)); + assertTrue( + exception.getMessage().startsWith("Token refresh failed: network or client error"), + "expected a network/client-error cause, got: " + exception.getMessage()); + + verify(httpClient, times(1)).invokeAPI(any(), any(), any()); + verify(spyAuthManager, never()).computeBackoffMillis(anyInt()); + } + + @Test + void nonSuccessfulStatusThrowsWithoutRetryOrBackoff() { + HttpResponse serverError = + new HttpResponse(503, "Service Unavailable", Collections.emptyMap(), new byte[0]); + when(httpClient.invokeAPI(any(), any(), any())).thenReturn(serverError); + + ApiAuthException exception = + assertThrows( + ApiAuthException.class, + () -> spyAuthManager.getAuthorizationHeaders(null, null, null, null)); + assertTrue( + exception.getMessage().startsWith("Token refresh failed with HTTP 503"), + "expected an HTTP-status cause, got: " + exception.getMessage()); + + verify(httpClient, times(1)).invokeAPI(any(), any(), any()); + verify(spyAuthManager, never()).computeBackoffMillis(anyInt()); + } + + @Test + void deserializationFailureThrowsWithoutRetryOrBackoff() { + HttpResponse invalidBody = + new HttpResponse(200, "foo message", null, "not-json".getBytes(StandardCharsets.UTF_8)); + when(httpClient.invokeAPI(any(), any(), any())).thenReturn(invalidBody); + + ApiAuthException exception = + assertThrows( + ApiAuthException.class, + () -> spyAuthManager.getAuthorizationHeaders(null, null, null, null)); + assertTrue( + exception.getMessage().startsWith("Token refresh failed: could not deserialize response"), + "expected a deserialization cause, got: " + exception.getMessage()); + + verify(httpClient, times(1)).invokeAPI(any(), any(), any()); + verify(spyAuthManager, never()).computeBackoffMillis(anyInt()); + } + } + + @Nested + class ComputeBackoff { + + @Test + void exponentialGrowth() { + OAuthManager manager = + new OAuthManager( + credentials, + new ServerConfiguration("OAuth url"), + HttpMapper.getInstance(), + () -> httpClient); + + long b0 = manager.computeBackoffMillis(0); + assertTrue(b0 >= 0 && b0 <= 1000); + + long b1 = manager.computeBackoffMillis(1); + assertTrue(b1 >= 0 && b1 <= 4000); + + long b2 = manager.computeBackoffMillis(2); + assertTrue(b2 >= 0 && b2 <= 16000); + } + } } diff --git a/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTestIT.java b/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTest.java similarity index 99% rename from client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTestIT.java rename to client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTest.java index a17235885..44952366c 100644 --- a/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTestIT.java +++ b/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTest.java @@ -13,7 +13,6 @@ import com.sinch.sdk.core.http.HttpMapper; import com.sinch.sdk.core.http.HttpMethod; import com.sinch.sdk.core.http.HttpRequest; -import com.sinch.sdk.core.http.HttpStatus; import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.http.HttpClientApache; import com.sinch.sdk.models.UnifiedCredentials; @@ -21,6 +20,7 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.util.AbstractMap; import java.util.Arrays; @@ -37,7 +37,7 @@ import org.junit.jupiter.api.Test; @TestWithResources -class HttpClientTestIT extends BaseTest { +class HttpClientTest extends BaseTest { static final String regExpBoundaryMarker = "--"; static final String regExpBoundaryItem = @@ -135,7 +135,7 @@ void bearerAutoRefresh() throws InterruptedException { // return unauthorized mockBackEnd.enqueue( new MockResponse() - .setResponseCode(HttpStatus.UNAUTHORIZED) + .setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED) .setBody("foo2") .addHeader("www-authenticate", "token invalid: expired") .addHeader("Content-Type", "application/json")); diff --git a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTest.java b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTest.java index f2835cbac..79799e13a 100644 --- a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTest.java +++ b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTest.java @@ -110,22 +110,8 @@ void processResponseDecodesBodyAsUtf8() throws Exception { } } - /** - * Verifies that a 407 Proxy Authentication Required response returned by {@code processRequest} - * (i.e. Apache's internal 407 handling failed) is returned immediately to the caller without - * triggering the OAuth token-refresh logic. - * - *

Some enterprise proxies include a {@code www-authenticate: Bearer error="expired"} header on - * 407 responses. Without the explicit 407 guard in {@link - * HttpClientApache#invokeAPI(com.sinch.sdk.core.models.ServerConfiguration, Map, - * com.sinch.sdk.core.http.HttpRequest)}, that header would cause the OAuthManager to reset its - * token and retry the request — incorrectly treating a proxy auth failure as an expired API - * token. - */ @Test void testInvokeApi407DoesNotTriggerOAuthRefresh() throws Exception { - // GIVEN: a 407 response that also carries www-authenticate: Bearer error="expired" - // (non-standard but seen on some corporate proxies) Map> proxyHeaders = new HashMap<>(); proxyHeaders.put( OAuthManager.BEARER_AUTHENTICATE_RESPONSE_HEADER_KEYWORD, @@ -155,16 +141,10 @@ void testInvokeApi407DoesNotTriggerOAuthRefresh() throws Exception { when(mockAuthManager.getAuthorizationHeaders(any(), any(), any(), any())) .thenReturn(Collections.emptyList()); - // WHEN: invokeAPI is called HttpResponse response = client.invokeAPI(serverConfig, authManagers, request); - // THEN: the 407 is returned as-is assertEquals(407, response.getCode()); - - // AND: OAuth token reset must NOT be triggered by the proxy 407 verify(mockAuthManager, never()).resetToken(); - - // AND: processRequest is called exactly once — no OAuth retry verify(client, times(1)).processRequest(any(), any()); } } diff --git a/core/src/main/com/sinch/sdk/core/http/HttpStatus.java b/core/src/main/com/sinch/sdk/core/http/HttpStatus.java index 56d42d052..bc7de86f6 100644 --- a/core/src/main/com/sinch/sdk/core/http/HttpStatus.java +++ b/core/src/main/com/sinch/sdk/core/http/HttpStatus.java @@ -4,7 +4,9 @@ public class HttpStatus { - public static Integer UNAUTHORIZED = HttpURLConnection.HTTP_UNAUTHORIZED; + @Deprecated public static final int UNAUTHORIZED = HttpURLConnection.HTTP_UNAUTHORIZED; + + public static final int TOO_MANY_REQUESTS = 429; public static boolean isSuccessfulStatus(int statusCode) { return statusCode >= 200 && statusCode < 300; diff --git a/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java b/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java index d28506bce..da1a6f857 100644 --- a/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java +++ b/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java @@ -88,14 +88,8 @@ void RFC822FromBlankString() { @Test void RFC822FromStringWithBlanks() { - Instant instant = DateUtil.RFC822StringToInstant(" Mon, 2 Jan 2006 15:04:05 MST "); - assertEquals("2006-01-02T22:04:05Z", instant.toString()); - } - - @Test - void RFC822WithUnsupportedMST() { - Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 MST"); - assertEquals("2006-01-02T22:04:05Z", instant.toString()); + Instant instant = DateUtil.RFC822StringToInstant(" Mon, 2 Jan 2006 15:04:05 GMT "); + assertEquals("2006-01-02T15:04:05Z", instant.toString()); } @Test @@ -110,12 +104,6 @@ void RFC822WithUnsupportedUT() { assertEquals("2006-01-02T15:04:05Z", instant.toString()); } - @Test - void RFC822WithUnsupportedEST() { - Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 EST"); - assertEquals("2006-01-02T20:04:05Z", instant.toString()); - } - @Test void RFC822WithOffset() { Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 +0100"); diff --git a/examples/sinch-events/pom.xml b/examples/sinch-events/pom.xml index 1eba25ac5..edb783586 100644 --- a/examples/sinch-events/pom.xml +++ b/examples/sinch-events/pom.xml @@ -59,7 +59,7 @@ com.diffplug.spotless spotless-maven-plugin - 2.40.0 + 3.8.0 @@ -69,7 +69,7 @@ - 1.22.0 + 1.35.0 true diff --git a/examples/snippets/pom.xml b/examples/snippets/pom.xml index b92dd2040..3a028f2fc 100644 --- a/examples/snippets/pom.xml +++ b/examples/snippets/pom.xml @@ -109,13 +109,6 @@ 2.0.9 - - - org.apache.maven - maven-model - [3.9.14,) - - diff --git a/pom.xml b/pom.xml index 44e1f524e..391b2704e 100644 --- a/pom.xml +++ b/pom.xml @@ -37,8 +37,8 @@ https://github.com/sinch/sinch-sdk-java.git scm:git:${project.scm.url} scm:git:${project.scm.url} - HEAD - + HEAD + https://github.com/sinch/sinch-sdk-java/issues @@ -54,19 +54,19 @@ 1.8 1.8 8 - 3.8.0 + 3.13.0 true true UTF-8 - 2.21.1 + 2.21.4 5.2.1 - 5.8.0 - 3.4.0 - 3.4.0 + 5.23.0 + 3.5.6 + 3.5.6 7.18.1 @@ -312,6 +312,8 @@ ${maven-surefire-plugin.version} ${skipUTs} + + -Dnet.bytebuddy.experimental=true @@ -333,7 +335,7 @@ com.diffplug.spotless spotless-maven-plugin - 2.40.0 + ${spotless.version} @@ -347,7 +349,7 @@ - 1.22.0 + ${googleformat.version} true @@ -423,7 +425,7 @@ org.assertj assertj-core - [3.27.7,) + 3.27.7 test @@ -490,16 +492,74 @@ test - - - org.apache.maven - maven-model - [3.9.14,) - - + + + + spotless-jdk8 + + [,9) + + + 2.30.0 + 1.22.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + + + **/*Test.java + **/ModelArgMatcher.java + **/LogRecorder.java + **/PaginationFillerHelper.java + **/CredentialsValidationHelper.java + + + + + + + + + + spotless-jdk11 + + [11,17) + + + 2.43.0 + 1.22.0 + + + + + spotless-jdk17 + [17,21) + + + 3.0.0 + 1.22.0 + + + + + spotless-jdk21-plus + [21,) + + 3.8.0 + 1.35.0 + + + release @@ -569,4 +629,4 @@ - \ No newline at end of file +