From b3651386b5c1e7083b8dc9b49d3f712ff488c1c1 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sun, 15 Dec 2024 14:29:49 +0300 Subject: [PATCH 1/3] test: authentication package unit tests --- code/build.gradle | 3 +- .../BearerAuthenticationInterceptorTest.kt | 361 ++++++++++++ .../bearer/BearerAuthenticationManagerTest.kt | 552 ++++++++++++++++++ .../bearer/BearerTokenStorageTest.kt | 257 ++++++++ .../bearer/TokenResponseTest.kt | 129 ++++ .../authentication/common/CredentialsTest.kt | 112 ++++ 6 files changed, 1413 insertions(+), 1 deletion(-) create mode 100644 code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt create mode 100644 code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt create mode 100644 code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt create mode 100644 code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt create mode 100644 code/src/test/kotlin/com/expediagroup/sdk/core/authentication/common/CredentialsTest.kt diff --git a/code/build.gradle b/code/build.gradle index f85eb426..e8420b1e 100644 --- a/code/build.gradle +++ b/code/build.gradle @@ -37,7 +37,8 @@ dependencies { /* Testing */ testImplementation platform('org.junit:junit-bom:5.11.3') testImplementation 'org.junit.jupiter:junit-jupiter-api' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.jupiter:junit-jupiter-params' testImplementation 'io.mockk:mockk:1.13.13' } diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt new file mode 100644 index 00000000..e6a83744 --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt @@ -0,0 +1,361 @@ +package com.expediagroup.sdk.core.authentication.bearer + +import com.expediagroup.sdk.core.http.Request +import com.expediagroup.sdk.core.interceptor.Interceptor +import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException +import io.mockk.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.io.IOException +import java.net.URL +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +class BearerAuthenticationInterceptorTest { + @Test + fun `intercept should proceed without Authorization header for authentication requests`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + val authUrl = "https://api.example.com/auth" + every { authManager.authUrl } returns authUrl + + // Mocking the authentication request + val authRequestBuilder = mockk(relaxed = true) + val authRequest = mockk(relaxed = true) + every { authRequest.url } returns URL(authUrl) + every { authRequest.newBuilder() } returns authRequestBuilder + every { chain.request() } returns authRequest + every { chain.proceed(authRequest) } returns mockk() + + // Act + val response = interceptor.intercept(chain) + + // Assert + // Verify that authUrl was called once + verify(exactly = 1) { authManager.authUrl } + + // Verify that no authentication methods were called + verify(exactly = 0) { authManager.authenticate() } + verify(exactly = 0) { authManager.isTokenAboutToExpire() } + verify(exactly = 0) { authManager.getAuthorizationHeaderValue() } + + // Optionally, confirm that no other interactions occurred + confirmVerified(authManager) + + // Verify that chain.proceed was called once with authRequest + verify(exactly = 1) { chain.proceed(authRequest) } + + // Ensure the response is not null + assertNotNull(response) + } + + + @Test + fun `intercept should add Authorization header and not authenticate when token is valid`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + // Mocking a non-authentication request + val requestBuilder = mockk(relaxed = true) + val originalRequest = mockk(relaxed = true) + val authorizedRequest = mockk(relaxed = true) + val requestUrl = "https://api.example.com/data" + + every { originalRequest.url } returns URL(requestUrl) + every { originalRequest.newBuilder() } returns requestBuilder + every { requestBuilder.addHeader("Authorization", "Bearer valid_token") } returns requestBuilder + every { requestBuilder.build() } returns authorizedRequest + every { chain.request() } returns originalRequest + every { chain.proceed(authorizedRequest) } returns mockk() + + // Mocking authManager behavior + every { authManager.isTokenAboutToExpire() } returns false + every { authManager.getAuthorizationHeaderValue() } returns "Bearer valid_token" + + // Act + val response = interceptor.intercept(chain) + + // Assert + verify(exactly = 1) { authManager.isTokenAboutToExpire() } + verify(exactly = 1) { authManager.getAuthorizationHeaderValue() } + verify(exactly = 0) { authManager.authenticate() } + verify(exactly = 1) { requestBuilder.addHeader("Authorization", "Bearer valid_token") } + verify(exactly = 1) { chain.proceed(authorizedRequest) } + assertNotNull(response) + } + + @Test + fun `intercept should authenticate and add Authorization header when token is about to expire`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + // Mocking a non-authentication request + val requestBuilder = mockk(relaxed = true) + val originalRequest = mockk(relaxed = true) + val authorizedRequest = mockk(relaxed = true) + val requestUrl = "https://api.example.com/data" + + every { originalRequest.url } returns URL(requestUrl) + every { originalRequest.newBuilder() } returns requestBuilder + every { requestBuilder.addHeader("Authorization", "Bearer refreshed_token") } returns requestBuilder + every { requestBuilder.build() } returns authorizedRequest + every { chain.request() } returns originalRequest + every { chain.proceed(authorizedRequest) } returns mockk() + + // Mocking authManager behavior + // Both calls to isTokenAboutToExpire() return true to trigger authenticate() + every { authManager.isTokenAboutToExpire() } returnsMany listOf(true, true) + every { authManager.authenticate() } just Runs + every { authManager.getAuthorizationHeaderValue() } returns "Bearer refreshed_token" + + // Act + val response = interceptor.intercept(chain) + + // Assert + verify(exactly = 2) { authManager.isTokenAboutToExpire() } // Initial check and inside synchronized block + verify(exactly = 1) { authManager.authenticate() } + verify(exactly = 1) { authManager.getAuthorizationHeaderValue() } + verify(exactly = 1) { requestBuilder.addHeader("Authorization", "Bearer refreshed_token") } + verify(exactly = 1) { chain.proceed(authorizedRequest) } + assertNotNull(response) + } + + @Test + fun `intercept should handle concurrent authentications and authenticate only once`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + val numberOfThreads = 10 + val latch = CountDownLatch(numberOfThreads) + val executor = Executors.newFixedThreadPool(numberOfThreads) + + // Mocking a non-authentication request + val requestBuilder = mockk(relaxed = true) + val originalRequest = mockk(relaxed = true) + val authorizedRequest = mockk(relaxed = true) + val requestUrl = "https://api.example.com/data" + + every { originalRequest.url } returns URL(requestUrl) + every { originalRequest.newBuilder() } returns requestBuilder + every { requestBuilder.addHeader("Authorization", "Bearer concurrent_token") } returns requestBuilder + every { requestBuilder.build() } returns authorizedRequest + every { chain.request() } returns originalRequest + every { chain.proceed(authorizedRequest) } returns mockk() + + // Mocking authManager behavior + // First call returns true to trigger authentication, subsequent calls return false + every { authManager.isTokenAboutToExpire() } returnsMany listOf( + true, + true + ).plus(List(numberOfThreads - 1) { false }) + every { authManager.authenticate() } just Runs + every { authManager.getAuthorizationHeaderValue() } returns "Bearer concurrent_token" + + // Act + repeat(numberOfThreads) { + executor.submit { + try { + interceptor.intercept(chain) + } finally { + latch.countDown() + } + } + } + + // Wait for all threads to complete + val completed = latch.await(5, TimeUnit.SECONDS) + executor.shutdown() + + // Assert + assertTrue(completed, "All threads should complete within timeout") + verify(exactly = 1) { authManager.authenticate() } // Should authenticate only once + verify(exactly = numberOfThreads + 1) { authManager.isTokenAboutToExpire() } // Initial check and per thread + verify(exactly = numberOfThreads) { authManager.getAuthorizationHeaderValue() } + verify(exactly = numberOfThreads) { requestBuilder.addHeader("Authorization", "Bearer concurrent_token") } + verify(exactly = numberOfThreads) { chain.proceed(authorizedRequest) } + } + + @Test + fun `intercept should throw ExpediaGroupAuthException when authentication fails`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + // Mocking a non-authentication request + val requestBuilder = mockk(relaxed = true) + val originalRequest = mockk(relaxed = true) + val requestUrl = "https://api.example.com/data" + + every { originalRequest.url } returns URL(requestUrl) + every { originalRequest.newBuilder() } returns requestBuilder + every { chain.request() } returns originalRequest + + // Mocking authManager behavior + // Both calls to isTokenAboutToExpire() return true to trigger authenticate(), which throws IOException + every { authManager.isTokenAboutToExpire() } returnsMany listOf(true, true) + every { authManager.authenticate() } throws IOException("Network error") + + // Act & Assert + val exception = assertThrows { + interceptor.intercept(chain) + } + + assertEquals("Failed to authenticate", exception.message) + assertTrue(exception.cause is IOException) + verify(exactly = 2) { authManager.isTokenAboutToExpire() } // Initial check and inside synchronized block + verify(exactly = 1) { authManager.authenticate() } + verify(exactly = 0) { authManager.getAuthorizationHeaderValue() } + verify(exactly = 0) { chain.proceed(any()) } + } + + @Test + fun `intercept should add correct Authorization header`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + val customToken = "Bearer custom_token_123" + + // Mocking a non-authentication request + val requestBuilder = mockk(relaxed = true) + val originalRequest = mockk(relaxed = true) + val authorizedRequest = mockk(relaxed = true) + val requestUrl = "https://api.example.com/data" + + every { originalRequest.url } returns URL(requestUrl) + every { originalRequest.newBuilder() } returns requestBuilder + every { requestBuilder.addHeader("Authorization", customToken) } returns requestBuilder + every { requestBuilder.build() } returns authorizedRequest + every { chain.request() } returns originalRequest + every { chain.proceed(authorizedRequest) } returns mockk() + + // Mocking authManager behavior + every { authManager.isTokenAboutToExpire() } returns false + every { authManager.getAuthorizationHeaderValue() } returns customToken + + // Act + val response = interceptor.intercept(chain) + + // Assert + verify(exactly = 1) { authManager.isTokenAboutToExpire() } + verify(exactly = 1) { authManager.getAuthorizationHeaderValue() } + verify(exactly = 0) { authManager.authenticate() } + verify(exactly = 1) { requestBuilder.addHeader("Authorization", customToken) } + verify(exactly = 1) { chain.proceed(authorizedRequest) } + assertNotNull(response) + } + + @Test + fun `ensureValidAuthentication should not call authenticate if token is not about to expire`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + // Mocking a non-authentication request + val requestBuilder = mockk(relaxed = true) + val originalRequest = mockk(relaxed = true) + val authorizedRequest = mockk(relaxed = true) + val requestUrl = "https://api.example.com/data" + + every { originalRequest.url } returns URL(requestUrl) + every { originalRequest.newBuilder() } returns requestBuilder + every { requestBuilder.addHeader("Authorization", "Bearer valid_token") } returns requestBuilder + every { requestBuilder.build() } returns authorizedRequest + every { chain.request() } returns originalRequest + every { chain.proceed(authorizedRequest) } returns mockk() + + // Mocking authManager behavior + every { authManager.isTokenAboutToExpire() } returns false + every { authManager.getAuthorizationHeaderValue() } returns "Bearer valid_token" + + // Act + val response = interceptor.intercept(chain) + + // Assert + verify(exactly = 1) { authManager.isTokenAboutToExpire() } + verify(exactly = 0) { authManager.authenticate() } + verify(exactly = 1) { authManager.getAuthorizationHeaderValue() } + verify(exactly = 1) { requestBuilder.addHeader("Authorization", "Bearer valid_token") } + verify(exactly = 1) { chain.proceed(authorizedRequest) } + assertNotNull(response) + } + + + @Test + fun `ensureValidAuthentication should call authenticate only once when multiple threads detect token expiration`() { + // Arrange + val authManager = mockk(relaxed = true) + val interceptor = BearerAuthenticationInterceptor(authManager) + val chain = mockk(relaxed = true) + + val numberOfThreads = 5 + val latch = CountDownLatch(numberOfThreads) + val executor = Executors.newFixedThreadPool(numberOfThreads) + + // Mocking a non-authentication request + val requestBuilder = mockk(relaxed = true) + val originalRequest = mockk(relaxed = true) + val authorizedRequest = mockk(relaxed = true) + val requestUrl = "https://api.example.com/data" + + every { originalRequest.url } returns URL(requestUrl) + every { originalRequest.newBuilder() } returns requestBuilder + every { requestBuilder.addHeader("Authorization", "Bearer refreshed_token") } returns requestBuilder + every { requestBuilder.build() } returns authorizedRequest + every { chain.request() } returns originalRequest + every { chain.proceed(authorizedRequest) } returns mockk() + + // Mocking authManager behavior + // Use AtomicBoolean to simulate token expiration + val tokenExpired = AtomicBoolean(true) + every { authManager.isTokenAboutToExpire() } answers { + tokenExpired.get() + } + every { authManager.authenticate() } answers { + // Simulate some delay in authentication + Thread.sleep(100) + tokenExpired.set(false) + } + every { authManager.getAuthorizationHeaderValue() } returns "Bearer refreshed_token" + every { authManager.authUrl } returns "https://api.example.com/auth" + + // Act + repeat(numberOfThreads) { + executor.submit { + try { + interceptor.intercept(chain) + } finally { + latch.countDown() + } + } + } + + // Wait for all threads to complete + val completed = latch.await(3, TimeUnit.SECONDS) + executor.shutdown() + + // Assert + assertTrue(completed, "All threads should complete within timeout") + verify(exactly = 1) { authManager.authenticate() } // Should authenticate only once + verify(atLeast = numberOfThreads) { authManager.isTokenAboutToExpire() } + verify(exactly = numberOfThreads) { authManager.getAuthorizationHeaderValue() } + verify(exactly = numberOfThreads) { requestBuilder.addHeader("Authorization", "Bearer refreshed_token") } + verify(exactly = numberOfThreads) { chain.proceed(authorizedRequest) } + } + +} diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt new file mode 100644 index 00000000..e2c3673f --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt @@ -0,0 +1,552 @@ +package com.expediagroup.sdk.core.authentication.bearer + +import com.expediagroup.sdk.core.authentication.common.Credentials +import com.expediagroup.sdk.core.client.RequestExecutor +import com.expediagroup.sdk.core.http.CommonMediaTypes +import com.expediagroup.sdk.core.http.Method +import com.expediagroup.sdk.core.http.Protocol +import com.expediagroup.sdk.core.http.Request +import com.expediagroup.sdk.core.http.Response +import com.expediagroup.sdk.core.http.ResponseBody +import com.expediagroup.sdk.core.http.Status +import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupResponseParsingException +import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException +import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupNetworkException +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class BearerAuthenticationManagerTest { + + private lateinit var requestExecutor: RequestExecutor + private lateinit var credentials: Credentials + private lateinit var authenticationManager: BearerAuthenticationManager + private val authUrl = "http://auth.example.com/token" + + @BeforeAll + fun setup() { + requestExecutor = mockk() + credentials = Credentials("client_key", "client_secret") + authenticationManager = BearerAuthenticationManager(authUrl, requestExecutor, credentials) + } + + @AfterEach + fun tearDown() { + clearMocks(requestExecutor) + authenticationManager.clearAuthentication() + } + + + /** + * Test successful authentication flow. + */ + @Test + fun `authenticate should store token on successful response`() { + // Arrange + val expiresIn: Long = 3600L + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "first_token", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returns response + + // Act + authenticationManager.authenticate() + + // Assert + assertEquals("Bearer first_token", authenticationManager.getAuthorizationHeaderValue()) + verify(exactly = 1) { requestExecutor.execute(any()) } + } + + /** + * Test authentication failure due to server error. + */ + @Test + fun `authenticate should throw ExpediaGroupAuthException on failure response`() { + // Arrange + val request = Request.builder().url("http://localhost").method(Method.POST).build() + val response = Response.builder() + .request(request) + .status(Status.INTERNAL_SERVER_ERROR) + .protocol(Protocol.HTTP_1_1) + .message(Status.INTERNAL_SERVER_ERROR.name) + .build() + every { requestExecutor.execute(any()) } returns response + + // Act & Assert + val exception = assertThrows { + authenticationManager.authenticate() + } + assertEquals("[${Status.INTERNAL_SERVER_ERROR.code}] Authentication failed", exception.message) + verify(exactly = 1) { requestExecutor.execute(any()) } + } + + /** + * Test authentication failure due to network issues. + */ + @Test + fun `authenticate should throw ExpediaGroupNetworkException on network failure`() { + // Arrange + every { requestExecutor.execute(any()) } throws ExpediaGroupNetworkException("Network error") + + // Act & Assert + val exception = assertThrows { + authenticationManager.authenticate() + } + assertEquals("Network error", exception.message) + verify(exactly = 1) { requestExecutor.execute(any()) } + } + + /** + * Test authentication failure due to response parsing issues. + */ + @Test + fun `authenticate should throw ExpediaGroupResponseParsingException on parsing failure`() { + // Arrange + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + """{ "id": 1 }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returns response + + // Act & Assert + val exception = assertThrows { + authenticationManager.authenticate() + } + assertTrue(exception.message!!.contains("Failed to parse")) + verify(exactly = 1) { requestExecutor.execute(any()) } + } + + /** + * Test isTokenAboutToExpire when token is not about to expire. + */ + @Test + fun `isTokenAboutToExpire should return false when token is valid`() { + // Arrange + val expiresIn: Long = 3600L + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "accessToken", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returns response + + // Act + authenticationManager.authenticate() + val isAboutToExpire = authenticationManager.isTokenAboutToExpire() + + // Assert + assertFalse(isAboutToExpire) + } + + /** + * Test isTokenAboutToExpire when token is about to expire. + */ + @Test + fun `isTokenAboutToExpire should return true when token is about to expire`() { + // Arrange + val expiresIn: Long = 1L + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "first_token", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returns response + + // Authenticate to store the token + authenticationManager.authenticate() + + // Create a spy of the authentication manager to manipulate isTokenAboutToExpire + val spyManager = spyk(authenticationManager) + + // Mock isTokenAboutToExpire to return true + every { spyManager.isTokenAboutToExpire() } returns true + + // Act + val isAboutToExpire = spyManager.isTokenAboutToExpire() + + // Assert + assertTrue(isAboutToExpire) + verify(exactly = 1) { spyManager.isTokenAboutToExpire() } + } + + /** + * Test clearing authentication. + */ + @Test + fun `clearAuthentication should reset the token`() { + // Arrange + val expiresIn: Long = 3600L + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "first_token", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returns response + + // Act + authenticationManager.authenticate() + assertEquals("Bearer first_token", authenticationManager.getAuthorizationHeaderValue()) + + authenticationManager.clearAuthentication() + + // Assert + assertEquals("Bearer ", authenticationManager.getAuthorizationHeaderValue()) + } + + /** + * Test getAuthorizationHeaderValue when no token is present. + */ + @Test + fun `getAuthorizationHeaderValue should return empty bearer when no token is present`() { + // Arrange + authenticationManager.clearAuthentication() + + // Act + val authHeader = authenticationManager.getAuthorizationHeaderValue() + + // Assert + assertEquals("Bearer ", authHeader) + } + + /** + * Test authenticate multiple times sequentially. + */ + @Test + fun `authenticate multiple times should update the token each time`() { + // Arrange + val expiresIn: Long = 3600L + var available: Int? = null + val response1 = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "first_token", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + val response2 = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "second_token", "expires_in": $expiresIn }""".toByteArray().inputStream() + .also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returnsMany listOf(response1, response2) + + // Act + authenticationManager.authenticate() + val firstAuthHeader = authenticationManager.getAuthorizationHeaderValue() + + authenticationManager.authenticate() + val secondAuthHeader = authenticationManager.getAuthorizationHeaderValue() + + // Assert + assertEquals("Bearer first_token", firstAuthHeader) + assertEquals("Bearer second_token", secondAuthHeader) + verify(exactly = 2) { requestExecutor.execute(any()) } + } + + /** + * Test authentication failure due to invalid credentials. + */ + @Test + fun `authenticate should throw ExpediaGroupAuthException on invalid credentials`() { + // Arrange + // Assuming server returns 401 Unauthorized for invalid credentials + val request = Request.builder().url("http://localhost").method(Method.POST).build() + val response = Response.builder() + .request(request) + .status(Status.UNAUTHORIZED) + .protocol(Protocol.HTTP_1_1) + .message(Status.UNAUTHORIZED.name) + .build() + every { requestExecutor.execute(any()) } returns response + + // Act & Assert + val exception = assertThrows { + authenticationManager.authenticate() + } + assertEquals("[${Status.UNAUTHORIZED.code}] Authentication failed", exception.message) + verify(exactly = 1) { requestExecutor.execute(any()) } + } + + /** + * Test authentication failure when response body is null. + */ + @Test + fun `authenticate should throw ExpediaGroupResponseParsingException when response body is null`() { + // Arrange + + val response = Response.builder() + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message(Status.INTERNAL_SERVER_ERROR.name) + .body(null) + .build() + every { requestExecutor.execute(any()) } returns response + + // Act & Assert + assertThrows { + authenticationManager.authenticate() + } + + verify(exactly = 1) { requestExecutor.execute(any()) } + } + + /** + * Test multiple sequential clear and authenticate operations. + */ + @Test + fun `sequential clear and authenticate operations should maintain consistent state`() { + // Arrange + val expiresIn: Long = 3600L + var available: Int? = null + val response1 = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "first_token", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + val response2 = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "second_token", "expires_in": $expiresIn }""".toByteArray().inputStream() + .also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returnsMany listOf(response1, response2) + + // Act + authenticationManager.authenticate() + assertEquals("Bearer first_token", authenticationManager.getAuthorizationHeaderValue()) + + authenticationManager.clearAuthentication() + assertEquals("Bearer ", authenticationManager.getAuthorizationHeaderValue()) + + authenticationManager.authenticate() + assertEquals("Bearer second_token", authenticationManager.getAuthorizationHeaderValue()) + + // Assert + verify(exactly = 2) { requestExecutor.execute(any()) } + } + + /** + * Test isTokenAboutToExpire at the exact expiration threshold. + // */ + @Test + fun `isTokenAboutToExpire should return true at expiration threshold`() { + val expiresIn: Long = 0L + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "first_token", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() // Token expires immediately + + every { requestExecutor.execute(any()) } returns response + + // Act + authenticationManager.authenticate() + val isAboutToExpire = authenticationManager.isTokenAboutToExpire() + + // Assert + assertTrue(isAboutToExpire) + } + + /** + * Test authentication when the response body is empty. + */ + @Test + fun `authenticate should throw ExpediaGroupResponseParsingException when response body is empty`() { + // Arrange + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + ByteArray(0).inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + + every { requestExecutor.execute(any()) } returns response + + // Act & Assert + val exception = assertThrows { + authenticationManager.authenticate() + } + assertTrue(exception.message!!.contains("Failed to parse")) + verify(exactly = 1) { requestExecutor.execute(any()) } + } + + /** + * Test handling of delayed responses (timeout simulation). + */ + @Test + fun `authenticate should handle delayed responses gracefully`() { + val expiresIn: Long = 3600L + var available: Int? = null + val response = Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "first_token", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() // Token expires immediately + + every { requestExecutor.execute(any()) } answers { + Thread.sleep(500) // Simulate delay + response + } + + // Act + val future = Executors.newSingleThreadExecutor().submit { + authenticationManager.authenticate() + } + + // Assert + assertDoesNotThrow { + future.get(1, TimeUnit.SECONDS) + } + assertEquals("Bearer first_token", authenticationManager.getAuthorizationHeaderValue()) + verify(exactly = 1) { requestExecutor.execute(any()) } + } +} diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt new file mode 100644 index 00000000..32274f48 --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt @@ -0,0 +1,257 @@ +package com.expediagroup.sdk.core.authentication.bearer + +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * Comprehensive test suite for BearerTokenStorage. + * + * This test class covers various scenarios including: + * - Token creation + * - Expiration checks + * - Authorization header generation + * - Edge case handling + * - Concurrency considerations + * + * @since 1.0.0 + * @author Expedia Group SDK Team + */ +@DisplayName("BearerTokenStorage Test") +class BearerTokenStorageEnterpriseTest { + + @Nested + @DisplayName("Token Creation Scenarios") + inner class TokenCreationTests { + @Test + fun `create token with standard parameters`() { + val mockClock = mockk { + every { instant() } returns Instant.parse("2024-01-01T12:00:00Z") + every { zone } returns ZoneOffset.UTC + } + + val tokenStorage = BearerTokenStorage.create( + accessToken = "standard_token", + expiresIn = 3600, + clock = mockClock + ) + + assertAll( + "Token creation assertions", + { assertEquals("standard_token", tokenStorage.accessToken) }, + { assertFalse(tokenStorage.isAboutToExpire()) }, + { assertEquals("Bearer standard_token", tokenStorage.getAuthorizationHeaderValue()) } + ) + } + + @ParameterizedTest + @DisplayName("Create token with various expiration times") + @ValueSource(longs = [0, 1, 59, 60, 61, 3600, 3600 * 24 * 365]) // Use a more reasonable large value + fun `create token with varied expiration times`(expiresIn: Long) { + val tokenStorage = try { + BearerTokenStorage.create( + accessToken = "test_token", + expiresIn = expiresIn.coerceAtMost(3600 * 24 * 365) // Limit to max 1 year + ) + } catch (e: Exception) { + // If creation fails, the token should be considered expired + null + } + + if (expiresIn < 0) { + assertTrue( + tokenStorage == null || tokenStorage.isAboutToExpire(), + "Token with negative expiration should be null or expired" + ) + } else { + assertNotNull(tokenStorage, "Token storage should not be null for non-negative expiration") + } + } + } + + @Nested + inner class ExpirationTests { + @Test + fun `token with negative expiration time`() { + val tokenStorage = BearerTokenStorage.create( + accessToken = "expired_token", + expiresIn = -1 + ) + + assertTrue(tokenStorage.isAboutToExpire(), "Negative expiration should always be considered expired") + } + + @Test + fun `empty token always expires`() { + val emptyTokenStorage = BearerTokenStorage.empty + + assertAll( + "Empty token assertions", + { assertTrue(emptyTokenStorage.isAboutToExpire()) }, + { assertEquals("Bearer ", emptyTokenStorage.getAuthorizationHeaderValue()) }, + { assertEquals("", emptyTokenStorage.accessToken) } + ) + } + + @ParameterizedTest + @CsvSource( + "3, 1, false", // Not yet expired + "3, 2, true", // About to expire + ) + fun `token expiration with custom buffer`(expiresIn: Long, bufferSeconds: Long, expectedExpired: Boolean) { + val tokenStorage = BearerTokenStorage.create( + accessToken = "buffer_test_token", + expiresIn = expiresIn, + expirationBufferSeconds = bufferSeconds, + clock = Clock.systemUTC() + ) + + Thread.sleep(1000 * bufferSeconds) + + assertEquals(expectedExpired, tokenStorage.isAboutToExpire()) + } + } + + @Nested + inner class AuthorizationHeaderTests { + @Test + fun `authorization header with special characters`() { + val tokenWithSpecialChars = "token!@#$%^&*()_+" + val tokenStorage = BearerTokenStorage.create( + accessToken = tokenWithSpecialChars, + expiresIn = 3600 + ) + + assertEquals("Bearer token!@#\$%^&*()_+", tokenStorage.getAuthorizationHeaderValue()) + } + + @Test + fun `authorization header with unicode characters`() { + val tokenWithUnicode = "token_🚀_special@chars_😊" + val tokenStorage = BearerTokenStorage.create( + accessToken = tokenWithUnicode, + expiresIn = 3600 + ) + + assertEquals("Bearer token_🚀_special@chars_😊", tokenStorage.getAuthorizationHeaderValue()) + } + } + + @Nested + inner class ConcurrencyTests { + @Test + fun `concurrent token creation`() { + val executor = Executors.newFixedThreadPool(10) + val latch = CountDownLatch(100) + val tokens = ConcurrentLinkedQueue() + + repeat(100) { + executor.submit { + try { + val token = BearerTokenStorage.create( + accessToken = "concurrent_token_$it", + expiresIn = 3600 + ) + tokens.add(token) + } finally { + latch.countDown() + } + } + } + + latch.await(5, TimeUnit.SECONDS) + executor.shutdown() + + assertEquals(100, tokens.size, "All tokens should be created successfully") + assertTrue(tokens.all { it.accessToken.startsWith("concurrent_token_") }) + } + } + + @Nested + inner class EdgeCaseTests { + @Test + fun `extreme expiration buffer scenarios`() { + // Very small buffer + val smallBufferToken = BearerTokenStorage.create( + accessToken = "small_buffer_token", + expiresIn = 10, + expirationBufferSeconds = 1 + ) + + // Extremely large buffer + val largeBufferToken = BearerTokenStorage.create( + accessToken = "large_buffer_token", + expiresIn = 3600, + expirationBufferSeconds = 3599 + ) + + assertAll( + "Extreme buffer assertions", + { assertNotNull(smallBufferToken) }, + { assertNotNull(largeBufferToken) } + ) + } + + @Test + fun `clock precision edge cases`() { + val precisionTestClock = mockk { + every { instant() } returns Instant.now() + .plusSeconds(30) + .plusNanos(999_999_999) + every { zone } returns ZoneOffset.UTC + } + + val tokenStorage = BearerTokenStorage.create( + accessToken = "precision_test_token", + expiresIn = 30, + expirationBufferSeconds = 31, + clock = precisionTestClock + ) + + assertTrue( + tokenStorage.isAboutToExpire(), + "Token should be considered about to expire near precision boundaries" + ) + } + } + + @Nested + inner class StabilityTests { + @Test + fun `verify empty token singleton`() { + val emptyToken1 = BearerTokenStorage.empty + val emptyToken2 = BearerTokenStorage.empty + + assertSame(emptyToken1, emptyToken2, "Empty token should be a singleton") + } + + @Test + fun `multiple token creations consistency`() { + val tokens = (1..100).map { + BearerTokenStorage.create("token_$it", 3600) + } + + assertTrue( + tokens.all { !it.isAboutToExpire() }, + "None of the created tokens should be expired" + ) + assertTrue( + tokens.all { it.getAuthorizationHeaderValue().startsWith("Bearer token_") }, + "All tokens should have correct authorization header" + ) + } + } +} diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt new file mode 100644 index 00000000..6aa2192b --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt @@ -0,0 +1,129 @@ +package com.expediagroup.sdk.core.authentication.bearer + +import com.expediagroup.sdk.core.http.CommonMediaTypes +import com.expediagroup.sdk.core.http.Method +import com.expediagroup.sdk.core.http.Protocol +import com.expediagroup.sdk.core.http.Request +import com.expediagroup.sdk.core.http.Response +import com.expediagroup.sdk.core.http.ResponseBody +import com.expediagroup.sdk.core.http.Status +import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupResponseParsingException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + + +class TokenResponseTest { + @Test + fun `parsed instances accessToken value should map to api response`() { + val accessToken: String = "token" + var available: Int? = null + val tokenResponse = TokenResponse.parse( + Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "$accessToken", "expires_in": 3600 }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + ) + + assertEquals(tokenResponse.accessToken, accessToken) + } + + @Test + fun `parse results instances expiresIn value should map to api response`() { + val expiresIn: Long = 3600L + var available: Int? = null + val tokenResponse = TokenResponse.parse( + Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "accessToken", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + ) + + assertEquals(tokenResponse.expiresIn, expiresIn) + } + + @Test + fun `parse should throw ExpediaGroupResponseParsingException in case of unsuccessful response`() { + assertThrows { + TokenResponse.parse( + Response.builder() + .status(Status.INTERNAL_SERVER_ERROR) + .protocol(Protocol.HTTP_1_1) + .message(Status.INTERNAL_SERVER_ERROR.name) + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + ) + } + } + + @Test + fun `parse should throw ExpediaGroupResponseParsingException if access_token or expiresIn are missing`() { + var available: Int? = null + assertThrows { + TokenResponse.parse( + Response.builder() + .body( + ResponseBody.create( + """{ "expires_in": 3600 }""".toByteArray().inputStream().also { available = it.available() }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + ) + } + } + + @Test + fun `parse should ignore extra fields as long as access_token and expires_in are present`() { + val accessToken: String = "token" + val expiresIn: Long = 3600L + var available: Int? = null + val tokenResponse = TokenResponse.parse( + Response.builder() + .body( + ResponseBody.create( + """{ "access_token": "$accessToken", "expires_in": $expiresIn, "extra": "random" }""".toByteArray().inputStream().also { + available = it.available() + }, + CommonMediaTypes.APPLICATION_FORM_URLENCODED, + available!!.toLong() + ) + ) + .status(Status.ACCEPTED) + .protocol(Protocol.HTTP_1_1) + .message("Accepted") + .request(Request.builder().url("http://localhost").method(Method.POST).build()) + .build() + ) + + assertEquals(tokenResponse.accessToken, accessToken) + assertEquals(tokenResponse.expiresIn, expiresIn) + } +} diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/common/CredentialsTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/common/CredentialsTest.kt new file mode 100644 index 00000000..586c9bb8 --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/common/CredentialsTest.kt @@ -0,0 +1,112 @@ +package com.expediagroup.sdk.core.authentication.common + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import java.util.Base64 + + +class CredentialsTest { + @Test + fun `toString should not print secret`() { + val key = "username" + val secret = "password" + + val credentials = Credentials(key = key, secret = secret) + + assertFalse(credentials.toString().contains("secret")) + assertFalse(credentials.toString().contains(secret)) + } + + @Test + fun `toString should print key=$value`() { + val key = "username" + val secret = "password" + + val credentials = Credentials(key = key, secret = secret) + assertTrue(credentials.toString().contains("key=username")) + } + + @Test + fun `encodeBasic result should be prefixed with Basic`() { + val key = "username" + val secret = "password" + + val credentials = Credentials(key = key, secret = secret) + assertTrue(credentials.encodeBasic().startsWith("Basic")) + } + + @Test + fun `encodeBasic result should contain only one space`() { + val key = "username" + val secret = "password" + + val credentials = Credentials(key = key, secret = secret) + assertEquals(1, credentials.encodeBasic().count { it == ' ' }) + } + + @Test + fun `encodeBasic result should encode to base64`() { + val key = "username" + val secret = "password" + val iso8859DecodedCredentials = "$key:$secret" + val iso8859EncodedCredentials = "Basic ${Base64.getEncoder().encodeToString(iso8859DecodedCredentials.toByteArray())}" + + val encoded = Credentials(key = key, secret = secret).encodeBasic() + assertEquals(encoded, iso8859EncodedCredentials) + } + + @Test + fun `encodeBasic result should decode to key double-colon value`() { + val key = "username" + val secret = "password" + val iso8859DecodedCredentials = "$key:$secret" + val iso8859EncodedCredentials = "Basic ${Base64.getEncoder().encodeToString(iso8859DecodedCredentials.toByteArray())}" + + val encoded = Credentials(key = key, secret = secret).encodeBasic() + assertEquals(encoded, iso8859EncodedCredentials) + } + + @Test + fun `encodeBasic should handle empty key and secret`() { + val credentials = Credentials(key = "", secret = "") + val expected = "Basic ${Base64.getEncoder().encodeToString(":".toByteArray(Charsets.ISO_8859_1))}" + + assertEquals(expected, credentials.encodeBasic()) + } + + @Test + fun `encodeBasic should handle special characters in key and secret`() { + val specialKey = "user!@#" + val specialSecret = "pass$%^" + + val credentials = Credentials(key = specialKey, secret = specialSecret) + val expected = + "Basic ${Base64.getEncoder().encodeToString("$specialKey:$specialSecret".toByteArray(Charsets.ISO_8859_1))}" + + assertEquals(expected, credentials.encodeBasic()) + } + + @Test + fun `encodeBasic should handle non-ISO_8859_1 charset`() { + val key = "username" + val secret = "password" + + val credentials = Credentials(key = key, secret = secret) + val expected = "Basic ${Base64.getEncoder().encodeToString("$key:$secret".toByteArray(Charsets.UTF_8))}" + + assertEquals(expected, credentials.encodeBasic(Charsets.UTF_8)) + } + + @Test + fun `encodeBasic should use ISO_8859_1 as default charset`() { + val key = "username" + val secret = "password" + + val credentials = Credentials(key = key, secret = secret) + val expected = "Basic ${Base64.getEncoder().encodeToString("$key:$secret".toByteArray(Charsets.ISO_8859_1))}" + + assertEquals(expected, credentials.encodeBasic()) + } +} From c11ba08b8b0c00de9b1230a4e377ee99b14673f2 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Mon, 16 Dec 2024 23:46:51 +0300 Subject: [PATCH 2/3] test: authentication package unit tests --- .../bearer/BearerTokenStorageTest.kt | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt index 32274f48..0a09592e 100644 --- a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorageTest.kt @@ -17,24 +17,9 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -/** - * Comprehensive test suite for BearerTokenStorage. - * - * This test class covers various scenarios including: - * - Token creation - * - Expiration checks - * - Authorization header generation - * - Edge case handling - * - Concurrency considerations - * - * @since 1.0.0 - * @author Expedia Group SDK Team - */ -@DisplayName("BearerTokenStorage Test") -class BearerTokenStorageEnterpriseTest { +class BearerTokenStorageTest { @Nested - @DisplayName("Token Creation Scenarios") inner class TokenCreationTests { @Test fun `create token with standard parameters`() { @@ -58,7 +43,6 @@ class BearerTokenStorageEnterpriseTest { } @ParameterizedTest - @DisplayName("Create token with various expiration times") @ValueSource(longs = [0, 1, 59, 60, 61, 3600, 3600 * 24 * 365]) // Use a more reasonable large value fun `create token with varied expiration times`(expiresIn: Long) { val tokenStorage = try { From 22ca7f07da3468ad719537a54c4f2492f33f3277 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sun, 22 Dec 2024 18:03:58 +0300 Subject: [PATCH 3/3] test: address comments --- code/build.gradle | 2 +- .../BearerAuthenticationInterceptorTest.kt | 64 +++++++------ .../bearer/BearerAuthenticationManagerTest.kt | 89 ++----------------- .../bearer/TokenResponseTest.kt | 30 +------ 4 files changed, 46 insertions(+), 139 deletions(-) diff --git a/code/build.gradle b/code/build.gradle index e8420b1e..74d236db 100644 --- a/code/build.gradle +++ b/code/build.gradle @@ -37,7 +37,7 @@ dependencies { /* Testing */ testImplementation platform('org.junit:junit-bom:5.11.3') testImplementation 'org.junit.jupiter:junit-jupiter-api' - testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' testImplementation 'org.junit.jupiter:junit-jupiter-params' testImplementation 'io.mockk:mockk:1.13.13' } diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt index e6a83744..e5b61981 100644 --- a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptorTest.kt @@ -3,8 +3,17 @@ package com.expediagroup.sdk.core.authentication.bearer import com.expediagroup.sdk.core.http.Request import com.expediagroup.sdk.core.interceptor.Interceptor import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException -import io.mockk.* -import org.junit.jupiter.api.Assertions.* +import io.mockk.clearAllMocks +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.Runs +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.io.IOException @@ -13,8 +22,14 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger class BearerAuthenticationInterceptorTest { + @BeforeEach + fun setUp() { + clearAllMocks() + } + @Test fun `intercept should proceed without Authorization header for authentication requests`() { // Arrange @@ -137,54 +152,47 @@ class BearerAuthenticationInterceptorTest { val interceptor = BearerAuthenticationInterceptor(authManager) val chain = mockk(relaxed = true) - val numberOfThreads = 10 - val latch = CountDownLatch(numberOfThreads) + val numberOfThreads = 2 val executor = Executors.newFixedThreadPool(numberOfThreads) // Mocking a non-authentication request val requestBuilder = mockk(relaxed = true) - val originalRequest = mockk(relaxed = true) - val authorizedRequest = mockk(relaxed = true) - val requestUrl = "https://api.example.com/data" + val authRequest = mockk(relaxed = true) - every { originalRequest.url } returns URL(requestUrl) - every { originalRequest.newBuilder() } returns requestBuilder + every { authRequest.url } returns URL("https://api.example.com/data") + every { authRequest.newBuilder() } returns requestBuilder + every { authManager.authUrl } returns "https://api.example.com/auth" every { requestBuilder.addHeader("Authorization", "Bearer concurrent_token") } returns requestBuilder - every { requestBuilder.build() } returns authorizedRequest - every { chain.request() } returns originalRequest - every { chain.proceed(authorizedRequest) } returns mockk() + every { requestBuilder.build() } returns authRequest + every { chain.request() } returns authRequest + every { chain.proceed(authRequest) } returns mockk() // Mocking authManager behavior // First call returns true to trigger authentication, subsequent calls return false - every { authManager.isTokenAboutToExpire() } returnsMany listOf( - true, - true - ).plus(List(numberOfThreads - 1) { false }) + val callsToIsTokenExpired = AtomicInteger(0) + every { authManager.isTokenAboutToExpire() } answers { + synchronized(this@BearerAuthenticationInterceptorTest) { + return@synchronized callsToIsTokenExpired.incrementAndGet() <= numberOfThreads + 1 + } + } + every { authManager.authenticate() } just Runs every { authManager.getAuthorizationHeaderValue() } returns "Bearer concurrent_token" - // Act repeat(numberOfThreads) { executor.submit { - try { - interceptor.intercept(chain) - } finally { - latch.countDown() - } + interceptor.intercept(chain) } } - // Wait for all threads to complete - val completed = latch.await(5, TimeUnit.SECONDS) + executor.awaitTermination(5, TimeUnit.SECONDS) executor.shutdown() // Assert - assertTrue(completed, "All threads should complete within timeout") verify(exactly = 1) { authManager.authenticate() } // Should authenticate only once - verify(exactly = numberOfThreads + 1) { authManager.isTokenAboutToExpire() } // Initial check and per thread verify(exactly = numberOfThreads) { authManager.getAuthorizationHeaderValue() } verify(exactly = numberOfThreads) { requestBuilder.addHeader("Authorization", "Bearer concurrent_token") } - verify(exactly = numberOfThreads) { chain.proceed(authorizedRequest) } + verify(exactly = numberOfThreads) { chain.proceed(authRequest) } } @Test @@ -260,7 +268,7 @@ class BearerAuthenticationInterceptorTest { } @Test - fun `ensureValidAuthentication should not call authenticate if token is not about to expire`() { + fun `should not call authenticate if token is not about to expire`() { // Arrange val authManager = mockk(relaxed = true) val interceptor = BearerAuthenticationInterceptor(authManager) diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt index e2c3673f..78a86bd2 100644 --- a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManagerTest.kt @@ -48,12 +48,8 @@ class BearerAuthenticationManagerTest { } - /** - * Test successful authentication flow. - */ @Test - fun `authenticate should store token on successful response`() { - // Arrange + fun `should authenticate and store access token on successful response`() { val expiresIn: Long = 3600L var available: Int? = null val response = Response.builder() @@ -74,19 +70,14 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returns response - // Act authenticationManager.authenticate() - // Assert assertEquals("Bearer first_token", authenticationManager.getAuthorizationHeaderValue()) verify(exactly = 1) { requestExecutor.execute(any()) } } - /** - * Test authentication failure due to server error. - */ @Test - fun `authenticate should throw ExpediaGroupAuthException on failure response`() { + fun `should throw ExpediaGroupAuthException on failure response`() { // Arrange val request = Request.builder().url("http://localhost").method(Method.POST).build() val response = Response.builder() @@ -97,7 +88,6 @@ class BearerAuthenticationManagerTest { .build() every { requestExecutor.execute(any()) } returns response - // Act & Assert val exception = assertThrows { authenticationManager.authenticate() } @@ -105,15 +95,11 @@ class BearerAuthenticationManagerTest { verify(exactly = 1) { requestExecutor.execute(any()) } } - /** - * Test authentication failure due to network issues. - */ @Test fun `authenticate should throw ExpediaGroupNetworkException on network failure`() { // Arrange every { requestExecutor.execute(any()) } throws ExpediaGroupNetworkException("Network error") - // Act & Assert val exception = assertThrows { authenticationManager.authenticate() } @@ -121,12 +107,8 @@ class BearerAuthenticationManagerTest { verify(exactly = 1) { requestExecutor.execute(any()) } } - /** - * Test authentication failure due to response parsing issues. - */ @Test fun `authenticate should throw ExpediaGroupResponseParsingException on parsing failure`() { - // Arrange var available: Int? = null val response = Response.builder() .body( @@ -146,7 +128,6 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returns response - // Act & Assert val exception = assertThrows { authenticationManager.authenticate() } @@ -154,11 +135,8 @@ class BearerAuthenticationManagerTest { verify(exactly = 1) { requestExecutor.execute(any()) } } - /** - * Test isTokenAboutToExpire when token is not about to expire. - */ @Test - fun `isTokenAboutToExpire should return false when token is valid`() { + fun `should treat the stored token as a valid token when not expired`() { // Arrange val expiresIn: Long = 3600L var available: Int? = null @@ -180,20 +158,14 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returns response - // Act authenticationManager.authenticate() val isAboutToExpire = authenticationManager.isTokenAboutToExpire() - // Assert assertFalse(isAboutToExpire) } - /** - * Test isTokenAboutToExpire when token is about to expire. - */ @Test - fun `isTokenAboutToExpire should return true when token is about to expire`() { - // Arrange + fun `should treat the stored token as a invalid token if expired`() { val expiresIn: Long = 1L var available: Int? = null val response = Response.builder() @@ -220,23 +192,16 @@ class BearerAuthenticationManagerTest { // Create a spy of the authentication manager to manipulate isTokenAboutToExpire val spyManager = spyk(authenticationManager) - // Mock isTokenAboutToExpire to return true every { spyManager.isTokenAboutToExpire() } returns true - // Act val isAboutToExpire = spyManager.isTokenAboutToExpire() - // Assert assertTrue(isAboutToExpire) verify(exactly = 1) { spyManager.isTokenAboutToExpire() } } - /** - * Test clearing authentication. - */ @Test - fun `clearAuthentication should reset the token`() { - // Arrange + fun `should handle token clearance`() { val expiresIn: Long = 3600L var available: Int? = null val response = Response.builder() @@ -257,37 +222,26 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returns response - // Act authenticationManager.authenticate() assertEquals("Bearer first_token", authenticationManager.getAuthorizationHeaderValue()) authenticationManager.clearAuthentication() - // Assert assertEquals("Bearer ", authenticationManager.getAuthorizationHeaderValue()) } - /** - * Test getAuthorizationHeaderValue when no token is present. - */ @Test fun `getAuthorizationHeaderValue should return empty bearer when no token is present`() { // Arrange authenticationManager.clearAuthentication() - // Act val authHeader = authenticationManager.getAuthorizationHeaderValue() - // Assert assertEquals("Bearer ", authHeader) } - /** - * Test authenticate multiple times sequentially. - */ @Test fun `authenticate multiple times should update the token each time`() { - // Arrange val expiresIn: Long = 3600L var available: Int? = null val response1 = Response.builder() @@ -325,25 +279,19 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returnsMany listOf(response1, response2) - // Act authenticationManager.authenticate() val firstAuthHeader = authenticationManager.getAuthorizationHeaderValue() authenticationManager.authenticate() val secondAuthHeader = authenticationManager.getAuthorizationHeaderValue() - // Assert assertEquals("Bearer first_token", firstAuthHeader) assertEquals("Bearer second_token", secondAuthHeader) verify(exactly = 2) { requestExecutor.execute(any()) } } - /** - * Test authentication failure due to invalid credentials. - */ @Test fun `authenticate should throw ExpediaGroupAuthException on invalid credentials`() { - // Arrange // Assuming server returns 401 Unauthorized for invalid credentials val request = Request.builder().url("http://localhost").method(Method.POST).build() val response = Response.builder() @@ -354,7 +302,6 @@ class BearerAuthenticationManagerTest { .build() every { requestExecutor.execute(any()) } returns response - // Act & Assert val exception = assertThrows { authenticationManager.authenticate() } @@ -362,12 +309,8 @@ class BearerAuthenticationManagerTest { verify(exactly = 1) { requestExecutor.execute(any()) } } - /** - * Test authentication failure when response body is null. - */ @Test fun `authenticate should throw ExpediaGroupResponseParsingException when response body is null`() { - // Arrange val response = Response.builder() .request(Request.builder().url("http://localhost").method(Method.POST).build()) @@ -378,7 +321,6 @@ class BearerAuthenticationManagerTest { .build() every { requestExecutor.execute(any()) } returns response - // Act & Assert assertThrows { authenticationManager.authenticate() } @@ -386,12 +328,8 @@ class BearerAuthenticationManagerTest { verify(exactly = 1) { requestExecutor.execute(any()) } } - /** - * Test multiple sequential clear and authenticate operations. - */ @Test fun `sequential clear and authenticate operations should maintain consistent state`() { - // Arrange val expiresIn: Long = 3600L var available: Int? = null val response1 = Response.builder() @@ -429,7 +367,6 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returnsMany listOf(response1, response2) - // Act authenticationManager.authenticate() assertEquals("Bearer first_token", authenticationManager.getAuthorizationHeaderValue()) @@ -439,13 +376,9 @@ class BearerAuthenticationManagerTest { authenticationManager.authenticate() assertEquals("Bearer second_token", authenticationManager.getAuthorizationHeaderValue()) - // Assert verify(exactly = 2) { requestExecutor.execute(any()) } } - /** - * Test isTokenAboutToExpire at the exact expiration threshold. - // */ @Test fun `isTokenAboutToExpire should return true at expiration threshold`() { val expiresIn: Long = 0L @@ -468,20 +401,14 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returns response - // Act authenticationManager.authenticate() val isAboutToExpire = authenticationManager.isTokenAboutToExpire() - // Assert assertTrue(isAboutToExpire) } - /** - * Test authentication when the response body is empty. - */ @Test fun `authenticate should throw ExpediaGroupResponseParsingException when response body is empty`() { - // Arrange var available: Int? = null val response = Response.builder() .body( @@ -501,7 +428,6 @@ class BearerAuthenticationManagerTest { every { requestExecutor.execute(any()) } returns response - // Act & Assert val exception = assertThrows { authenticationManager.authenticate() } @@ -509,9 +435,6 @@ class BearerAuthenticationManagerTest { verify(exactly = 1) { requestExecutor.execute(any()) } } - /** - * Test handling of delayed responses (timeout simulation). - */ @Test fun `authenticate should handle delayed responses gracefully`() { val expiresIn: Long = 3600L @@ -537,12 +460,10 @@ class BearerAuthenticationManagerTest { response } - // Act val future = Executors.newSingleThreadExecutor().submit { authenticationManager.authenticate() } - // Assert assertDoesNotThrow { future.get(1, TimeUnit.SECONDS) } diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt index 6aa2192b..d2203225 100644 --- a/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponseTest.kt @@ -15,39 +15,16 @@ import org.junit.jupiter.api.assertThrows class TokenResponseTest { @Test - fun `parsed instances accessToken value should map to api response`() { + fun `should map to the expected api response`() { val accessToken: String = "token" - var available: Int? = null - val tokenResponse = TokenResponse.parse( - Response.builder() - .body( - ResponseBody.create( - """{ "access_token": "$accessToken", "expires_in": 3600 }""".toByteArray().inputStream().also { - available = it.available() - }, - CommonMediaTypes.APPLICATION_FORM_URLENCODED, - available!!.toLong() - ) - ) - .status(Status.ACCEPTED) - .protocol(Protocol.HTTP_1_1) - .message("Accepted") - .request(Request.builder().url("http://localhost").method(Method.POST).build()) - .build() - ) - - assertEquals(tokenResponse.accessToken, accessToken) - } - - @Test - fun `parse results instances expiresIn value should map to api response`() { val expiresIn: Long = 3600L + var available: Int? = null val tokenResponse = TokenResponse.parse( Response.builder() .body( ResponseBody.create( - """{ "access_token": "accessToken", "expires_in": $expiresIn }""".toByteArray().inputStream().also { + """{ "access_token": "$accessToken", "expires_in": $expiresIn }""".toByteArray().inputStream().also { available = it.available() }, CommonMediaTypes.APPLICATION_FORM_URLENCODED, @@ -61,6 +38,7 @@ class TokenResponseTest { .build() ) + assertEquals(tokenResponse.accessToken, accessToken) assertEquals(tokenResponse.expiresIn, expiresIn) }