diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/RefreshableTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/RefreshableTokenSource.java index 750aae967..d84f6de96 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/RefreshableTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/RefreshableTokenSource.java @@ -5,29 +5,245 @@ import com.databricks.sdk.core.http.FormRequest; import com.databricks.sdk.core.http.HttpClient; import com.databricks.sdk.core.http.Request; +import com.databricks.sdk.core.utils.ClockSupplier; +import com.databricks.sdk.core.utils.UtcClockSupplier; +import java.time.Duration; import java.time.Instant; import java.util.Base64; import java.util.Map; +import java.util.concurrent.CompletableFuture; import org.apache.http.HttpHeaders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * An OAuth TokenSource which can be refreshed. * - *

Calls to getToken() will first check if the token is still valid (currently defined by having - * at least 10 seconds until expiry). If not, refresh() is called first to refresh the token. + *

This class supports both synchronous and asynchronous token refresh. When async is enabled, + * stale tokens will trigger a background refresh, while expired tokens will block until a new token + * is fetched. */ public abstract class RefreshableTokenSource implements TokenSource { - protected Token token; + /** + * Enum representing the state of the token. FRESH: Token is valid and not close to expiry. STALE: + * Token is valid but will expire soon - an async refresh will be triggered if enabled. EXPIRED: + * Token has expired and must be refreshed using a blocking call. + */ + protected enum TokenState { + FRESH, + STALE, + EXPIRED + } + + private static final Logger logger = LoggerFactory.getLogger(RefreshableTokenSource.class); + // Default duration before expiry to consider a token as 'stale'. + private static final Duration DEFAULT_STALE_DURATION = Duration.ofMinutes(3); + // Default additional buffer before expiry to consider a token as expired. + private static final Duration DEFAULT_EXPIRY_BUFFER = Duration.ofSeconds(40); + + // The current OAuth token. May be null if not yet fetched. + protected volatile Token token; + // Whether asynchronous refresh is enabled. + private boolean asyncEnabled = false; + // Duration before expiry to consider a token as 'stale'. + private Duration staleDuration = DEFAULT_STALE_DURATION; + // Additional buffer before expiry to consider a token as expired. + private Duration expiryBuffer = DEFAULT_EXPIRY_BUFFER; + // Whether a refresh is currently in progress (for async refresh). + private boolean refreshInProgress = false; + // Whether the last refresh attempt succeeded. + private boolean lastRefreshSucceeded = true; + // Clock supplier for current time. + private ClockSupplier clockSupplier = new UtcClockSupplier(); + + /** Constructs a new {@code RefreshableTokenSource} with no initial token. */ public RefreshableTokenSource() {} + /** + * Constructor with initial token. + * + * @param token The initial token to use. + */ public RefreshableTokenSource(Token token) { this.token = token; } + /** + * Set the clock supplier for current time. + * + *

Experimental: This method may change or be removed in future releases. + * + * @param clockSupplier The clock supplier to use. + * @return this instance for chaining + */ + public RefreshableTokenSource withClockSupplier(ClockSupplier clockSupplier) { + this.clockSupplier = clockSupplier; + return this; + } + + /** + * Enable or disable asynchronous token refresh. + * + *

Experimental: This method may change or be removed in future releases. + * + * @param enabled true to enable async refresh, false to disable + * @return this instance for chaining + */ + public RefreshableTokenSource withAsyncRefresh(boolean enabled) { + this.asyncEnabled = enabled; + return this; + } + + /** + * Set the expiry buffer. If the token's lifetime is less than this buffer, it is considered + * expired. + * + *

Experimental: This method may change or be removed in future releases. + * + * @param buffer the expiry buffer duration + * @return this instance for chaining + */ + public RefreshableTokenSource withExpiryBuffer(Duration buffer) { + this.expiryBuffer = buffer; + return this; + } + + /** + * Refresh the OAuth token. Subclasses must implement this to define how the token is refreshed. + * + *

This method may throw an exception if the token cannot be refreshed. The specific exception + * type depends on the implementation. + * + * @return The newly refreshed Token. + */ + protected abstract Token refresh(); + + /** + * Gets the current token, refreshing if necessary. If async refresh is enabled, may return a + * stale token while a refresh is in progress. + * + *

This method may throw an exception if the token cannot be refreshed, depending on the + * implementation of {@link #refresh()}. + * + * @return The current valid token + */ + public Token getToken() { + if (asyncEnabled) { + return getTokenAsync(); + } + return getTokenBlocking(); + } + + /** + * Determine the state of the current token (fresh, stale, or expired). + * + * @return The token state + */ + protected TokenState getTokenState(Token t) { + if (t == null) { + return TokenState.EXPIRED; + } + Duration lifeTime = Duration.between(Instant.now(clockSupplier.getClock()), t.getExpiry()); + if (lifeTime.compareTo(expiryBuffer) <= 0) { + return TokenState.EXPIRED; + } + if (lifeTime.compareTo(staleDuration) <= 0) { + return TokenState.STALE; + } + return TokenState.FRESH; + } + + /** + * Get the current token, blocking to refresh if expired. + * + *

This method may throw an exception if the token cannot be refreshed, depending on the + * implementation of {@link #refresh()}. + * + * @return The current valid token + */ + protected Token getTokenBlocking() { + // Use double-checked locking to minimize synchronization overhead on reads: + // 1. Check if the token is expired without locking. + // 2. If expired, synchronize and check again (another thread may have refreshed it). + // 3. If still expired, perform the refresh. + if (getTokenState(token) != TokenState.EXPIRED) { + return token; + } + synchronized (this) { + if (getTokenState(token) != TokenState.EXPIRED) { + return token; + } + lastRefreshSucceeded = false; + try { + token = refresh(); + } catch (Exception e) { + logger.error("Failed to refresh token synchronously", e); + throw e; + } + lastRefreshSucceeded = true; + return token; + } + } + + /** + * Get the current token, possibly triggering an async refresh if stale. If the token is expired, + * blocks to refresh. + * + *

This method may throw an exception if the token cannot be refreshed, depending on the + * implementation of {@link #refresh()}. + * + * @return The current valid or stale token + */ + protected Token getTokenAsync() { + Token currentToken = token; + + switch (getTokenState(currentToken)) { + case FRESH: + return currentToken; + case STALE: + triggerAsyncRefresh(); + return currentToken; + case EXPIRED: + return getTokenBlocking(); + default: + throw new IllegalStateException("Invalid token state."); + } + } + + /** + * Trigger an asynchronous refresh of the token if not already in progress and last refresh + * succeeded. + */ + private synchronized void triggerAsyncRefresh() { + // Check token state again inside the synchronized block to avoid triggering a refresh if + // another thread updated the token in the meantime. + if (!refreshInProgress && lastRefreshSucceeded && getTokenState(token) != TokenState.FRESH) { + refreshInProgress = true; + CompletableFuture.runAsync( + () -> { + try { + // Attempt to refresh the token in the background + Token newToken = refresh(); + synchronized (this) { + token = newToken; + refreshInProgress = false; + } + } catch (Exception e) { + synchronized (this) { + lastRefreshSucceeded = false; + refreshInProgress = false; + logger.error("Asynchronous token refresh failed", e); + } + } + }); + } + } + /** * Helper method implementing OAuth token refresh. * + * @param hc The HTTP client to use for the request. * @param clientId The client ID to authenticate with. * @param clientSecret The client secret to authenticate with. * @param tokenUrl The authorization URL for fetching tokens. @@ -35,6 +251,8 @@ public RefreshableTokenSource(Token token) { * @param headers Additional headers. * @param position The position of the authentication parameters in the request. * @return The newly fetched Token. + * @throws DatabricksException if the refresh fails + * @throws IllegalArgumentException if the OAuth response contains an error */ protected static Token retrieveToken( HttpClient hc, @@ -75,13 +293,4 @@ protected static Token retrieveToken( throw new DatabricksException("Failed to refresh credentials: " + e.getMessage(), e); } } - - protected abstract Token refresh(); - - public synchronized Token getToken() { - if (token == null || !token.isValid()) { - token = refresh(); - } - return token; - } } diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Token.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Token.java index ac6fbc3ac..4a3b42a7e 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Token.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Token.java @@ -1,7 +1,5 @@ package com.databricks.sdk.core.oauth; -import com.databricks.sdk.core.utils.ClockSupplier; -import com.databricks.sdk.core.utils.SystemClockSupplier; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Instant; @@ -23,16 +21,9 @@ public class Token { */ @JsonProperty private Instant expiry; - private final ClockSupplier clockSupplier; - /** Constructor for non-refreshable tokens (e.g. M2M). */ public Token(String accessToken, String tokenType, Instant expiry) { - this(accessToken, tokenType, null, expiry, new SystemClockSupplier()); - } - - /** Constructor for non-refreshable tokens (e.g. M2M) with ClockSupplier */ - public Token(String accessToken, String tokenType, Instant expiry, ClockSupplier clockSupplier) { - this(accessToken, tokenType, null, expiry, clockSupplier); + this(accessToken, tokenType, null, expiry); } /** Constructor for refreshable tokens. */ @@ -42,51 +33,48 @@ public Token( @JsonProperty("tokenType") String tokenType, @JsonProperty("refreshToken") String refreshToken, @JsonProperty("expiry") Instant expiry) { - this(accessToken, tokenType, refreshToken, expiry, new SystemClockSupplier()); - } - - /** Constructor for refreshable tokens with ClockSupplier. */ - public Token( - String accessToken, - String tokenType, - String refreshToken, - Instant expiry, - ClockSupplier clockSupplier) { Objects.requireNonNull(accessToken, "accessToken must be defined"); Objects.requireNonNull(tokenType, "tokenType must be defined"); Objects.requireNonNull(expiry, "expiry must be defined"); - Objects.requireNonNull(clockSupplier, "clockSupplier must be defined"); this.accessToken = accessToken; this.tokenType = tokenType; this.refreshToken = refreshToken; this.expiry = expiry; - this.clockSupplier = clockSupplier; - } - - public boolean isExpired() { - if (expiry == null) { - return false; - } - // Azure Databricks rejects tokens that expire in 30 seconds or less, - // so we refresh the token 40 seconds before it expires. - Instant potentiallyExpired = expiry.minusSeconds(40); - Instant now = Instant.now(clockSupplier.getClock()); - return potentiallyExpired.isBefore(now); - } - - public boolean isValid() { - return accessToken != null && !isExpired(); } + /** + * Returns the type of the token (e.g., "Bearer"). + * + * @return the token type + */ public String getTokenType() { return tokenType; } + /** + * Returns the refresh token, if available. May be null for non-refreshable tokens. + * + * @return the refresh token or null + */ public String getRefreshToken() { return refreshToken; } + /** + * Returns the access token string. + * + * @return the access token + */ public String getAccessToken() { return accessToken; } + + /** + * Returns the expiry time of the token as a Instant. + * + * @return the expiry time + */ + public Instant getExpiry() { + return this.expiry; + } } diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SystemClockSupplier.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/UtcClockSupplier.java similarity index 70% rename from databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SystemClockSupplier.java rename to databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/UtcClockSupplier.java index c79e6884d..73e6d2bc4 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SystemClockSupplier.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/UtcClockSupplier.java @@ -2,7 +2,7 @@ import java.time.Clock; -public class SystemClockSupplier implements ClockSupplier { +public class UtcClockSupplier implements ClockSupplier { @Override public Clock getClock() { return Clock.systemUTC(); diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java index aa98730cb..05ffd805d 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java @@ -154,7 +154,7 @@ public void testRefreshWithExpiry( Token token = tokenSource.refresh(); assertEquals("Bearer", token.getTokenType()); assertEquals("test-token", token.getAccessToken()); - assertEquals(shouldBeExpired, token.isExpired()); + assertEquals(shouldBeExpired, token.getExpiry().isBefore(Instant.now())); } } } diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DataPlaneTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DataPlaneTokenSourceTest.java index 1fb96a559..bac591212 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DataPlaneTokenSourceTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DataPlaneTokenSourceTest.java @@ -194,7 +194,7 @@ void testDataPlaneTokenSource( assertEquals(expectedToken.getAccessToken(), token.getAccessToken()); assertEquals(expectedToken.getTokenType(), token.getTokenType()); assertEquals(expectedToken.getRefreshToken(), token.getRefreshToken()); - assertTrue(token.isValid()); + assertTrue(expectedToken.getExpiry().isAfter(Instant.now())); } } diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java index 8217179f2..ee226cd42 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java @@ -330,7 +330,6 @@ void testTokenSource(TestCase testCase) { assertEquals(TOKEN, token.getAccessToken()); assertEquals(TOKEN_TYPE, token.getTokenType()); assertEquals(REFRESH_TOKEN, token.getRefreshToken()); - assertFalse(token.isExpired()); // Verify correct audience was used verify(testCase.idTokenSource, atLeastOnce()).getIDToken(testCase.expectedAudience); diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/FileTokenCacheTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/FileTokenCacheTest.java index 303f6de66..710fbf1b8 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/FileTokenCacheTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/FileTokenCacheTest.java @@ -54,24 +54,7 @@ void testSaveAndLoadToken() { assertEquals("access-token", loadedToken.getAccessToken()); assertEquals("Bearer", loadedToken.getTokenType()); assertEquals("refresh-token", loadedToken.getRefreshToken()); - assertFalse(loadedToken.isExpired(), "Token should not be expired"); - } - - @Test - void testTokenExpiry() { - // Create an expired token - Instant pastTime = Instant.now().minusSeconds(3600); - Token expiredToken = new Token("access-token", "Bearer", "refresh-token", pastTime); - - // Verify it's marked as expired - assertTrue(expiredToken.isExpired(), "Token should be expired"); - - // Create a valid token - Instant futureTime = Instant.now().plusSeconds(1800); - Token validToken = new Token("access-token", "Bearer", "refresh-token", futureTime); - - // Verify it's not marked as expired - assertFalse(validToken.isExpired(), "Token should not be expired"); + assertEquals(expiry, loadedToken.getExpiry()); } @Test diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/RefreshableTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/RefreshableTokenSourceTest.java new file mode 100644 index 000000000..194c3a2ec --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/RefreshableTokenSourceTest.java @@ -0,0 +1,158 @@ +package com.databricks.sdk.core.oauth; + +import static org.junit.jupiter.api.Assertions.*; + +import com.databricks.sdk.core.utils.TestClockSupplier; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class RefreshableTokenSourceTest { + private static final String TOKEN_TYPE = "Bearer"; + private static final String INITIAL_TOKEN = "initial-token"; + private static final String REFRESH_TOKEN = "refreshed-token"; + private static final long FRESH_MINUTES = 10; + private static final long STALE_MINUTES = 1; + private static final long EXPIRED_MINUTES = -1; + + private static Stream provideAsyncRefreshScenarios() { + return Stream.of( + Arguments.of("Fresh token, async enabled", FRESH_MINUTES, true, false, INITIAL_TOKEN), + Arguments.of("Stale token, async enabled", STALE_MINUTES, true, true, INITIAL_TOKEN), + Arguments.of("Expired token, async enabled", EXPIRED_MINUTES, true, true, REFRESH_TOKEN), + Arguments.of("Fresh token, async disabled", FRESH_MINUTES, false, false, INITIAL_TOKEN), + Arguments.of("Stale token, async disabled", STALE_MINUTES, false, false, INITIAL_TOKEN), + Arguments.of("Expired token, async disabled", EXPIRED_MINUTES, false, true, REFRESH_TOKEN)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideAsyncRefreshScenarios") + void testAsyncRefreshParametrized( + String testName, + long minutesUntilExpiry, + boolean asyncEnabled, + boolean expectRefresh, + String expectedToken) + throws Exception { + + Token initialToken = + new Token( + INITIAL_TOKEN, + TOKEN_TYPE, + null, + Instant.now().plus(Duration.ofMinutes(minutesUntilExpiry))); + Token refreshedToken = + new Token(REFRESH_TOKEN, TOKEN_TYPE, null, Instant.now().plus(Duration.ofMinutes(10))); + CountDownLatch refreshCalled = new CountDownLatch(1); + + RefreshableTokenSource source = + new RefreshableTokenSource(initialToken) { + @Override + protected Token refresh() { + refreshCalled.countDown(); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return refreshedToken; + } + }.withAsyncRefresh(asyncEnabled); + + Token token = source.getToken(); + + boolean refreshed = refreshCalled.await(1, TimeUnit.SECONDS); + assertEquals(expectRefresh, refreshed, "Refresh should have been triggered"); + assertEquals(expectedToken, token.getAccessToken(), "Token value did not match expected"); + } + + /** + * This test verifies that if an asynchronous token refresh fails, the next refresh attempt is + * forced to be synchronous. It ensures that after an async failure, the system does not + * repeatedly attempt async refreshes while the token is stale, and only performs a synchronous + * refresh when the token is expired. After a successful sync refresh, async refreshes resume as + * normal. + */ + @Test + void testAsyncRefreshFailureFallback() throws Exception { + // Create a test clock starting at current time + TestClockSupplier clockSupplier = new TestClockSupplier(Instant.now()); + + // Create a token that expires in 2 minutes from the initial clock time + Token staleToken = + new Token( + INITIAL_TOKEN, + TOKEN_TYPE, + null, + Instant.now(clockSupplier.getClock()).plus(Duration.ofMinutes(2))); + + class TestSource extends RefreshableTokenSource { + int refreshCallCount = 0; + boolean isFirstRefresh = true; + + TestSource(Token token) { + super(token); + } + + @Override + protected Token refresh() { + refreshCallCount++; + if (isFirstRefresh) { + isFirstRefresh = false; + throw new RuntimeException("Simulated async failure"); + } + return new Token( + REFRESH_TOKEN, + TOKEN_TYPE, + null, + Instant.now(clockSupplier.getClock()).plus(Duration.ofMinutes(10))); + } + } + + TestSource source = new TestSource(staleToken); + source.withAsyncRefresh(true); + source.withClockSupplier(clockSupplier); + + // First call triggers async refresh, which fails + source.getToken(); + Thread.sleep(300); + assertEquals( + 1, source.refreshCallCount, "refresh() should have been called once (async, failed)"); + + // Token is still stale, so next call should NOT trigger another refresh since the last refresh + // failed + source.getToken(); + Thread.sleep(200); + assertEquals( + 1, + source.refreshCallCount, + "refresh() should NOT be called again while stale after async failure"); + + // Advance the clock by 3 minutes to make the token expired + clockSupplier.advanceTime(Duration.ofMinutes(3)); + + // Now getToken() should call refresh synchronously and return the refreshed token + Token token = source.getToken(); + assertEquals( + REFRESH_TOKEN, + token.getAccessToken(), + "Should return the refreshed token after sync refresh"); + assertEquals( + 2, source.refreshCallCount, "refresh() should have been called synchronously after expiry"); + + // Advance time by 8 minutes to make the token stale again + clockSupplier.advanceTime(Duration.ofMinutes(8)); + source.getToken(); + Thread.sleep(300); + assertEquals( + 3, + source.refreshCallCount, + "refresh() should have been called again asynchronously after making token stale"); + } +} diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/TokenTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/TokenTest.java index 16b726222..a0173cbf8 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/TokenTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/TokenTest.java @@ -2,9 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import com.databricks.sdk.core.utils.FakeClockSupplier; import java.time.Instant; -import java.time.ZoneId; import org.junit.jupiter.api.Test; class TokenTest { @@ -12,62 +10,23 @@ class TokenTest { private static final String accessToken = "testAccessToken"; private static final String refreshToken = "testRefreshToken"; private static final String tokenType = "testTokenType"; - private final Instant currentInstant; - private final FakeClockSupplier fakeClockSupplier; - - TokenTest() { - Instant instant = Instant.parse("2023-10-18T12:00:00.00Z"); - ZoneId zoneId = ZoneId.of("UTC"); - fakeClockSupplier = new FakeClockSupplier(instant, zoneId); - currentInstant = Instant.now(fakeClockSupplier.getClock()); - } + private static final Instant currentInstant = Instant.now(); @Test void createNonRefreshableToken() { - Token token = - new Token(accessToken, tokenType, currentInstant.plusSeconds(300), fakeClockSupplier); + Token token = new Token(accessToken, tokenType, currentInstant.plusSeconds(300)); assertEquals(accessToken, token.getAccessToken()); assertEquals(tokenType, token.getTokenType()); assertNull(token.getRefreshToken()); - assertTrue(token.isValid()); + assertEquals(currentInstant.plusSeconds(300), token.getExpiry()); } @Test void createRefreshableToken() { - Token token = - new Token( - accessToken, - tokenType, - refreshToken, - currentInstant.plusSeconds(300), - fakeClockSupplier); + Token token = new Token(accessToken, tokenType, refreshToken, currentInstant.plusSeconds(300)); assertEquals(accessToken, token.getAccessToken()); assertEquals(tokenType, token.getTokenType()); assertEquals(refreshToken, token.getRefreshToken()); - assertTrue(token.isValid()); - } - - @Test - void tokenExpiryMoreThan40Seconds() { - Token token = - new Token(accessToken, tokenType, currentInstant.plusSeconds(50), fakeClockSupplier); - assertFalse(token.isExpired()); - assertTrue(token.isValid()); - } - - @Test - void tokenExpiryLessThan40Seconds() { - Token token = - new Token(accessToken, tokenType, currentInstant.plusSeconds(30), fakeClockSupplier); - assertTrue(token.isExpired()); - assertFalse(token.isValid()); - } - - @Test - void expiredToken() { - Token token = - new Token(accessToken, tokenType, currentInstant.minusSeconds(10), fakeClockSupplier); - assertTrue(token.isExpired()); - assertFalse(token.isValid()); + assertEquals(currentInstant.plusSeconds(300), token.getExpiry()); } } diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/FakeClockSupplier.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/FakeClockSupplier.java deleted file mode 100644 index 78f8df076..000000000 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/FakeClockSupplier.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.databricks.sdk.core.utils; - -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneId; - -public class FakeClockSupplier implements ClockSupplier { - private final Clock clock; - - public FakeClockSupplier(Instant fixedInstant, ZoneId zoneId) { - clock = Clock.fixed(fixedInstant, zoneId); - } - - @Override - public Clock getClock() { - return clock; - } -} diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/TestClockSupplier.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/TestClockSupplier.java new file mode 100644 index 000000000..fb97149dc --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/TestClockSupplier.java @@ -0,0 +1,23 @@ +package com.databricks.sdk.core.utils; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; + +public class TestClockSupplier implements ClockSupplier { + private Clock clock; + + public TestClockSupplier(Instant fixedInstant) { + clock = Clock.fixed(fixedInstant, ZoneId.of("UTC")); + } + + public void advanceTime(Duration duration) { + clock = Clock.offset(clock, duration); + } + + @Override + public Clock getClock() { + return clock; + } +}