From 6e16daf0019b3a0ffba6eb7e2f4347df7bf3fbbb Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 20 Apr 2026 09:21:39 -0500 Subject: [PATCH] PM-35273: feat: Add support for SDK API calls by providing base urls (#6805) --- .../auth/datasource/sdk/AuthSdkSourceImpl.kt | 53 ++++++-------- .../network/di/PlatformNetworkModule.kt | 36 +++++---- .../platform/datasource/sdk/BaseSdkSource.kt | 15 +++- .../data/platform/manager/SdkClientManager.kt | 15 +++- .../platform/manager/SdkClientManagerImpl.kt | 22 ++++-- .../manager/di/PlatformManagerModule.kt | 3 + .../manager/sdk/SdkRepositoryFactory.kt | 11 ++- .../manager/sdk/SdkRepositoryFactoryImpl.kt | 17 +++++ .../sdk/repository/SdkTokenRepository.kt | 5 +- .../datasource/sdk/GeneratorSdkSourceImpl.kt | 12 +-- .../auth/datasource/sdk/AuthSdkSourceTest.kt | 32 +++++++- .../platform/manager/SdkClientManagerTest.kt | 13 +++- .../manager/sdk/SdkRepositoryFactoryTests.kt | 73 ++++++++++++++++++- .../sdk/repository/SdkTokenRepositoryTest.kt | 14 +++- .../datasource/sdk/GeneratorSdkSourceTest.kt | 4 +- .../network/di/PlatformNetworkModule.kt | 18 +++-- .../model/BitwardenServiceClientConfig.kt | 2 +- 17 files changed, 270 insertions(+), 75 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt index 74fea4b1e3..9e710f215c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt @@ -27,25 +27,21 @@ class AuthSdkSourceImpl( override suspend fun getNewAuthRequest( email: String, ): Result = runCatchingWithLogs { - getClient() - .auth() - .newAuthRequest( - email = email.lowercase(), - ) + useClient { auth().newAuthRequest(email = email.lowercase()) } } override suspend fun getUserFingerprint( email: String, publicKey: String, ): Result = runCatchingWithLogs { - getClient() - .platform() - .fingerprint( + useClient { + platform().fingerprint( req = FingerprintRequest( fingerprintMaterial = email.lowercase(), publicKey = publicKey, ), ) + } } override suspend fun hashPassword( @@ -54,21 +50,19 @@ class AuthSdkSourceImpl( kdf: Kdf, purpose: HashPurpose, ): Result = runCatchingWithLogs { - getClient() - .auth() - .hashPassword( + useClient { + auth().hashPassword( email = email, password = password, kdfParams = kdf, purpose = purpose, ) + } } override suspend fun makeKeyConnectorKeys(): Result = runCatchingWithLogs { - getClient() - .auth() - .makeKeyConnectorKeys() + useClient { auth().makeKeyConnectorKeys() } } override suspend fun makeRegisterKeys( @@ -76,13 +70,13 @@ class AuthSdkSourceImpl( password: String, kdf: Kdf, ): Result = runCatchingWithLogs { - getClient() - .auth() - .makeRegisterKeys( + useClient { + auth().makeRegisterKeys( email = email, password = password, kdf = kdf, ) + } } override suspend fun makeRegisterTdeKeysAndUnlockVault( @@ -105,15 +99,16 @@ class AuthSdkSourceImpl( password: String, additionalInputs: List, ): Result = runCatchingWithLogs { - @Suppress("UnsafeCallOnNullableType") - getClient() - .auth() - .passwordStrength( - password = password, - email = email, - additionalInputs = additionalInputs, - ) - .toPasswordStrengthOrNull()!! + useClient { + @Suppress("UnsafeCallOnNullableType") + auth() + .passwordStrength( + password = password, + email = email, + additionalInputs = additionalInputs, + ) + .toPasswordStrengthOrNull()!! + } } override suspend fun satisfiesPolicy( @@ -121,12 +116,12 @@ class AuthSdkSourceImpl( passwordStrength: PasswordStrength, policy: MasterPasswordPolicyOptions, ): Result = runCatchingWithLogs { - getClient() - .auth() - .satisfiesPolicy( + useClient { + auth().satisfiesPolicy( password = password, strength = passwordStrength.toUByte(), policy = policy, ) + } } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt index b09f827398..66bc11472b 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt @@ -50,7 +50,7 @@ object PlatformNetworkModule { @Provides @Singleton - fun provideBitwardenServiceClient( + fun provideBitwardenServiceClientConfig( authTokenManager: AuthTokenManager, baseUrlsProvider: BaseUrlsProvider, authDiskSource: AuthDiskSource, @@ -58,20 +58,26 @@ object PlatformNetworkModule { buildInfoManager: BuildInfoManager, networkCookieManager: NetworkCookieManager, clock: Clock, - ): BitwardenServiceClient = bitwardenServiceClient( - BitwardenServiceClientConfig( - clock = clock, - appIdProvider = authDiskSource, - clientData = BitwardenServiceClientConfig.ClientData( - userAgent = HEADER_VALUE_USER_AGENT, - clientName = HEADER_VALUE_CLIENT_NAME, - clientVersion = HEADER_VALUE_CLIENT_VERSION, - ), - authTokenProvider = authTokenManager, - baseUrlsProvider = baseUrlsProvider, - certificateProvider = certificateManager, - enableHttpBodyLogging = buildInfoManager.isDevBuild, - cookieProvider = networkCookieManager, + ): BitwardenServiceClientConfig = BitwardenServiceClientConfig( + clock = clock, + appIdProvider = authDiskSource, + clientData = BitwardenServiceClientConfig.ClientData( + userAgent = HEADER_VALUE_USER_AGENT, + clientName = HEADER_VALUE_CLIENT_NAME, + clientVersion = HEADER_VALUE_CLIENT_VERSION, ), + authTokenProvider = authTokenManager, + baseUrlsProvider = baseUrlsProvider, + certificateProvider = certificateManager, + enableHttpBodyLogging = buildInfoManager.isDevBuild, + cookieProvider = networkCookieManager, + ) + + @Provides + @Singleton + fun provideBitwardenServiceClient( + serviceClientConfig: BitwardenServiceClientConfig, + ): BitwardenServiceClient = bitwardenServiceClient( + config = serviceClientConfig, ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt index 9a88ae8120..cf8b3064ea 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt @@ -15,9 +15,22 @@ abstract class BaseSdkSource( * Helper function to retrieve the [Client] associated with the given [userId]. */ protected suspend fun getClient( - userId: String? = null, + userId: String, ): Client = sdkClientManager.getOrCreateClient(userId = userId) + /** + * Helper function to retrieve a new [Client] and use it in the given [block]. + */ + protected suspend fun useClient( + userId: String? = null, + accessToken: String? = null, + block: suspend Client.() -> T, + ): T = sdkClientManager.singleUseClient( + userId = userId, + accessToken = accessToken, + block = block, + ) + /** * Invokes the [block] with `this` value as its receiver and returns its result if it was * successful and catches any exception that was thrown from the `block` and wrapping it as a diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt index 9b3df862d0..e2798cd92d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt @@ -11,7 +11,20 @@ interface SdkClientManager { * Returns the cached [Client] instance for the given [userId], otherwise creates and caches * a new one and returns it. */ - suspend fun getOrCreateClient(userId: String?): Client + suspend fun getOrCreateClient(userId: String): Client + + /** + * Helper function to retrieve a new instance of the [Client] and use it in the given [block]. + * This client is never persisted after the [block] completes. + * + * @param userId The used to create the [Client]. If null, the SDK is unassociated with a user. + * @param accessToken The access token used in network requests. + */ + suspend fun singleUseClient( + userId: String? = null, + accessToken: String? = null, + block: suspend Client.() -> T, + ): T /** * Clears any resources from the [Client] associated with the given [userId] and removes it diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt index 445cbeac18..c92829f2c8 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt @@ -15,10 +15,16 @@ class SdkClientManagerImpl( sdkRepoFactory: SdkRepositoryFactory, sdkPlatformApiFactory: SdkPlatformApiFactory, private val featureFlagManager: FeatureFlagManager, - private val clientProvider: suspend (userId: String?) -> Client = { userId -> + private val clientProvider: suspend ( + userId: String?, + accessToken: String?, + ) -> Client = { userId, accessToken -> Client( - tokenProvider = sdkRepoFactory.getClientManagedTokens(userId = userId), - settings = null, + tokenProvider = sdkRepoFactory.getClientManagedTokens( + userId = userId, + accessToken = accessToken, + ), + settings = sdkRepoFactory.getClientSettings(), ) .apply { platform().loadFlags(featureFlagManager.sdkFeatureFlags) @@ -32,7 +38,7 @@ class SdkClientManagerImpl( } }, ) : SdkClientManager { - private val userIdToClientMap = mutableMapOf() + private val userIdToClientMap = mutableMapOf() init { // The SDK requires access to Android APIs that were not made public until API 31. In order @@ -44,8 +50,14 @@ class SdkClientManagerImpl( } override suspend fun getOrCreateClient( + userId: String, + ): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider(userId, null) } + + override suspend fun singleUseClient( userId: String?, - ): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider(userId) } + accessToken: String?, + block: suspend Client.() -> T, + ): T = clientProvider(userId, accessToken).use { it.block() } override fun destroyClient( userId: String?, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index c2a89a5d46..0a1c615873 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -15,6 +15,7 @@ import com.bitwarden.data.datasource.disk.ConfigDiskSource import com.bitwarden.data.manager.NativeLibraryManager import com.bitwarden.data.repository.ServerConfigRepository import com.bitwarden.network.BitwardenServiceClient +import com.bitwarden.network.model.BitwardenServiceClientConfig import com.bitwarden.network.service.EventService import com.bitwarden.network.service.PushService import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource @@ -367,11 +368,13 @@ object PlatformManagerModule { cookieDiskSource: CookieDiskSource, configDiskSource: ConfigDiskSource, authDiskSource: AuthDiskSource, + serviceClientConfig: BitwardenServiceClientConfig, ): SdkRepositoryFactory = SdkRepositoryFactoryImpl( vaultDiskSource = vaultDiskSource, cookieDiskSource = cookieDiskSource, configDiskSource = configDiskSource, authDiskSource = authDiskSource, + serviceClientConfig = serviceClientConfig, ) @Provides diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactory.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactory.kt index f1d4091a09..e1efaeb309 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactory.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactory.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.platform.manager.sdk import com.bitwarden.core.ClientManagedTokens +import com.bitwarden.core.ClientSettings import com.bitwarden.sdk.Repositories import com.bitwarden.sdk.ServerCommunicationConfigRepository @@ -16,7 +17,15 @@ interface SdkRepositoryFactory { /** * Retrieves or creates a [ClientManagedTokens] for use with the Bitwarden SDK. */ - fun getClientManagedTokens(userId: String?): ClientManagedTokens + fun getClientManagedTokens( + userId: String?, + accessToken: String?, + ): ClientManagedTokens + + /** + * Retrieves or creates a [ClientSettings] for use with the Bitwarden SDK. + */ + fun getClientSettings(): ClientSettings /** * Retrieves or creates a [ServerCommunicationConfigRepository] for use with the Bitwarden SDK. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt index 496aa1b18d..04a7811f84 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt @@ -1,7 +1,10 @@ package com.x8bit.bitwarden.data.platform.manager.sdk import com.bitwarden.core.ClientManagedTokens +import com.bitwarden.core.ClientSettings +import com.bitwarden.core.DeviceType import com.bitwarden.data.datasource.disk.ConfigDiskSource +import com.bitwarden.network.model.BitwardenServiceClientConfig import com.bitwarden.sdk.Repositories import com.bitwarden.sdk.ServerCommunicationConfigRepository import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource @@ -20,6 +23,7 @@ class SdkRepositoryFactoryImpl( private val cookieDiskSource: CookieDiskSource, private val configDiskSource: ConfigDiskSource, private val authDiskSource: AuthDiskSource, + private val serviceClientConfig: BitwardenServiceClientConfig, ) : SdkRepositoryFactory { override fun getRepositories(userId: String?): Repositories = Repositories( @@ -34,12 +38,25 @@ class SdkRepositoryFactoryImpl( override fun getClientManagedTokens( userId: String?, + accessToken: String?, ): ClientManagedTokens = SdkTokenRepository( userId = userId, + accessToken = accessToken, authDiskSource = authDiskSource, ) + override fun getClientSettings(): ClientSettings = + ClientSettings( + identityUrl = serviceClientConfig.baseUrlsProvider.getBaseIdentityUrl(), + apiUrl = serviceClientConfig.baseUrlsProvider.getBaseApiUrl(), + userAgent = serviceClientConfig.clientData.userAgent, + deviceType = DeviceType.ANDROID, + deviceIdentifier = serviceClientConfig.appIdProvider.uniqueAppId, + bitwardenClientVersion = serviceClientConfig.clientData.clientVersion, + bitwardenPackageType = null, + ) + override fun getServerCommunicationConfigRepository(): ServerCommunicationConfigRepository = ServerCommunicationConfigRepositoryImpl( cookieDiskSource = cookieDiskSource, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepository.kt index c20ea2a668..475e90c4ec 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepository.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepository.kt @@ -11,10 +11,9 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource */ class SdkTokenRepository( private val userId: String?, + private val accessToken: String?, private val authDiskSource: AuthDiskSource, ) : ClientManagedTokens { override suspend fun getAccessToken(): String? = - userId?.let { - authDiskSource.getAccountTokens(userId = it)?.accessToken - } + accessToken ?: userId?.let { authDiskSource.getAccountTokens(userId = it)?.accessToken } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt index 09c527a841..c13fbf0170 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt @@ -21,36 +21,36 @@ class GeneratorSdkSourceImpl( override suspend fun generatePassword( request: PasswordGeneratorRequest, ): Result = runCatchingWithLogs { - getClient().generators().password(request) + useClient { generators().password(request) } } override suspend fun generatePassphrase( request: PassphraseGeneratorRequest, ): Result = runCatchingWithLogs { - getClient().generators().passphrase(request) + useClient { generators().passphrase(request) } } override suspend fun generatePlusAddressedEmail( request: UsernameGeneratorRequest.Subaddress, ): Result = runCatchingWithLogs { - getClient().generators().username(request) + useClient { generators().username(request) } } override suspend fun generateCatchAllEmail( request: UsernameGeneratorRequest.Catchall, ): Result = runCatchingWithLogs { - getClient().generators().username(request) + useClient { generators().username(request) } } override suspend fun generateRandomWord( request: UsernameGeneratorRequest.Word, ): Result = runCatchingWithLogs { - getClient().generators().username(request) + useClient { generators().username(request) } } override suspend fun generateForwardedServiceEmail( request: UsernameGeneratorRequest.Forwarded, ): Result = runCatchingWithLogs { - getClient().generators().username(request) + useClient { generators().username(request) } } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt index fedf481f41..60c8222667 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt @@ -20,6 +20,7 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs +import io.mockk.slot import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -34,7 +35,7 @@ class AuthSdkSourceTest { every { platform() } returns clientPlatform } private val sdkClientManager = mockk { - coEvery { getOrCreateClient(userId = null) } returns client + coEvery { getOrCreateClient(userId = any()) } returns client } private val authSkdSource: AuthSdkSource = AuthSdkSourceImpl( @@ -43,6 +44,10 @@ class AuthSdkSourceTest { @Test fun `getNewAuthRequest should call SDK and return a Result with correct data`() = runBlocking { + val slot = slot AuthRequestResponse>() + coEvery { + sdkClientManager.singleUseClient(block = capture(slot)) + } coAnswers { slot.captured(client) } val email = "test@gmail.com" val expectedResult = mockk() coEvery { @@ -62,6 +67,10 @@ class AuthSdkSourceTest { @Test fun `getUserFingerprint should call SDK and return a Result with correct data`() = runBlocking { + val slot = slot String>() + coEvery { + sdkClientManager.singleUseClient(block = capture(slot)) + } coAnswers { slot.captured(client) } val email = "email@gmail.com" val publicKey = "publicKey" val expectedResult = "fingerprint" @@ -91,6 +100,10 @@ class AuthSdkSourceTest { @Test fun `hashPassword should call SDK and return a Result with the correct data`() = runBlocking { + val slot = slot String>() + coEvery { + sdkClientManager.singleUseClient(block = capture(slot)) + } coAnswers { slot.captured(client) } val email = "email" val password = "password" val kdf = mockk() @@ -128,6 +141,10 @@ class AuthSdkSourceTest { @Test fun `makeKeyConnectorKeys should call SDK and return a Result with the correct data`() = runBlocking { + val slot = slot KeyConnectorResponse>() + coEvery { + sdkClientManager.singleUseClient(block = capture(slot)) + } coAnswers { slot.captured(client) } val expectedResult = mockk() coEvery { clientAuth.makeKeyConnectorKeys() } returns expectedResult @@ -142,6 +159,10 @@ class AuthSdkSourceTest { @Test fun `makeRegisterKeys should call SDK and return a Result with the correct data`() = runBlocking { + val slot = slot RegisterKeyResponse>() + coEvery { + sdkClientManager.singleUseClient(block = capture(slot)) + } coAnswers { slot.captured(client) } val email = "email" val password = "password" val kdf = mockk() @@ -181,7 +202,6 @@ class AuthSdkSourceTest { val orgPublicKey = "orgPublicKey" val rememberDevice = true val expectedResult = mockk() - coEvery { sdkClientManager.getOrCreateClient(userId = userId) } returns client coEvery { clientAuth.makeRegisterTdeKeys( email = email, @@ -209,6 +229,10 @@ class AuthSdkSourceTest { @Test fun `passwordStrength should call SDK and return a Result with the correct data`() = runBlocking { + val slot = slot PasswordStrength>() + coEvery { + sdkClientManager.singleUseClient(block = capture(slot)) + } coAnswers { slot.captured(client) } val email = "email" val password = "password" val additionalInputs = listOf("test1", "test2") @@ -243,6 +267,10 @@ class AuthSdkSourceTest { @Test fun `satisfiesPolicy should call SDK and return a Result with the correct data`() = runBlocking { + val slot = slot Boolean>() + coEvery { + sdkClientManager.singleUseClient(block = capture(slot)) + } coAnswers { slot.captured(client) } val password = "password" val passwordStrength = PasswordStrength.LEVEL_3 val rawStrength = 3.toUByte() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt index 47f8d8be8c..031dab400e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt @@ -72,6 +72,17 @@ class SdkClientManagerTest { assertNotEquals(firstClient, thirdClient) } + @Test + fun `singleUseClient should create a new client everytime and run the lambda with it`() = + runTest { + val sdkClientManager = createSdkClientManager() + val firstClient = sdkClientManager.singleUseClient { this } + + // Additional calls should always create a new client + val secondClient = sdkClientManager.singleUseClient { this } + assertNotEquals(firstClient, secondClient) + } + @Test fun `destroyClient should call close on the Client and remove it from the cache`() = runTest { val sdkClientManager = createSdkClientManager() @@ -88,7 +99,7 @@ class SdkClientManagerTest { } private fun createSdkClientManager(): SdkClientManagerImpl = SdkClientManagerImpl( - clientProvider = { mockk(relaxed = true) }, + clientProvider = { _, _ -> mockk(relaxed = true) }, nativeLibraryManager = mockNativeLibraryManager, featureFlagManager = mockk(), sdkRepoFactory = sdkRepoFactory, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt index 33ef9d4434..7e215ce362 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt @@ -1,12 +1,21 @@ package com.x8bit.bitwarden.data.platform.manager.sdk +import com.bitwarden.core.ClientSettings +import com.bitwarden.core.DeviceType import com.bitwarden.data.datasource.disk.ConfigDiskSource +import com.bitwarden.network.interceptor.BaseUrlsProvider +import com.bitwarden.network.model.BitwardenServiceClientConfig +import com.bitwarden.network.provider.AppIdProvider import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset class SdkRepositoryFactoryTests { @@ -14,12 +23,32 @@ class SdkRepositoryFactoryTests { private val cookieDiskSource: CookieDiskSource = mockk() private val configDiskSource: ConfigDiskSource = mockk() private val authDiskSource: AuthDiskSource = mockk() + private val serviceClientConfig: BitwardenServiceClientConfig = BitwardenServiceClientConfig( + clientData = BitwardenServiceClientConfig.ClientData( + userAgent = USER_AGENT, + clientName = CLIENT_NAME, + clientVersion = CLIENT_VERSION, + ), + appIdProvider = object : AppIdProvider { + override val uniqueAppId: String get() = UNIQUE_APP_ID + }, + baseUrlsProvider = object : BaseUrlsProvider { + override fun getBaseApiUrl(): String = BASE_API_URL + override fun getBaseIdentityUrl(): String = BASE_IDENTITY_URL + override fun getBaseEventsUrl(): String = BASE_EVENTS_URL + }, + authTokenProvider = mockk(), + certificateProvider = mockk(), + cookieProvider = mockk(), + clock = FIXED_CLOCK, + ) private val sdkRepoFactory: SdkRepositoryFactory = SdkRepositoryFactoryImpl( vaultDiskSource = vaultDiskSource, cookieDiskSource = cookieDiskSource, configDiskSource = configDiskSource, authDiskSource = authDiskSource, + serviceClientConfig = serviceClientConfig, ) @Test @@ -45,18 +74,43 @@ class SdkRepositoryFactoryTests { @Test fun `getClientManagedTokens should create a new client`() { val userId = "userId" - val firstClient = sdkRepoFactory.getClientManagedTokens(userId = userId) + val firstClient = sdkRepoFactory.getClientManagedTokens( + userId = userId, + accessToken = null, + ) // Additional calls for the same userId should create a repo - val secondClient = sdkRepoFactory.getClientManagedTokens(userId = userId) + val secondClient = sdkRepoFactory.getClientManagedTokens( + userId = userId, + accessToken = null, + ) assertNotEquals(firstClient, secondClient) // Additional calls for different userIds should return a different repo val otherUserId = "otherUserId" - val thirdClient = sdkRepoFactory.getClientManagedTokens(userId = otherUserId) + val thirdClient = sdkRepoFactory.getClientManagedTokens( + userId = otherUserId, + accessToken = null, + ) assertNotEquals(firstClient, thirdClient) } + @Test + fun `getClientSettings should create correct getClientSettings`() { + assertEquals( + ClientSettings( + identityUrl = BASE_IDENTITY_URL, + apiUrl = BASE_API_URL, + userAgent = USER_AGENT, + deviceType = DeviceType.ANDROID, + deviceIdentifier = UNIQUE_APP_ID, + bitwardenClientVersion = CLIENT_VERSION, + bitwardenPackageType = null, + ), + sdkRepoFactory.getClientSettings(), + ) + } + @Test fun `getServerCommunicationConfigRepository should create a new repository`() { val firstRepo = sdkRepoFactory.getServerCommunicationConfigRepository() @@ -64,3 +118,16 @@ class SdkRepositoryFactoryTests { assertNotEquals(firstRepo, secondRepo) } } + +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, +) + +private const val BASE_API_URL: String = "https://api.bitwarden.com" +private const val BASE_EVENTS_URL: String = "https://events.bitwarden.com" +private const val BASE_IDENTITY_URL: String = "https://identity.bitwarden.com" +private const val CLIENT_NAME: String = "mobile" +private const val CLIENT_VERSION: String = "2026.4.1" +private const val UNIQUE_APP_ID: String = "app_id_12345" +private const val USER_AGENT: String = "user (agent)" diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepositoryTest.kt index f41c5ac601..0ca05b816a 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkTokenRepositoryTest.kt @@ -34,7 +34,6 @@ class SdkTokenRepositoryTest { } } - @Suppress("MaxLineLength") @Test fun `getAccessToken should return null when userId is valid and accessToken is null`() = runTest { @@ -69,10 +68,23 @@ class SdkTokenRepositoryTest { } } + @Test + fun `getAccessToken should return access token when accessToken is manually provided`() = + runTest { + val accessToken = "access_token" + val repository = createSdkTokenRepository(accessToken = accessToken) + assertEquals(accessToken, repository.getAccessToken()) + verify(exactly = 0) { + authDiskSource.getAccountTokens(userId = USER_ID) + } + } + private fun createSdkTokenRepository( userId: String? = USER_ID, + accessToken: String? = null, ): SdkTokenRepository = SdkTokenRepository( userId = userId, + accessToken = accessToken, authDiskSource = authDiskSource, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt index 4ef5e8271a..29671d8b93 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt @@ -13,6 +13,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.slot import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test @@ -23,7 +24,8 @@ class GeneratorSdkSourceTest { every { generators() } returns clientGenerators } private val sdkClientManager = mockk { - coEvery { getOrCreateClient(userId = null) } returns client + val slot = slot String>() + coEvery { singleUseClient(block = capture(slot)) } coAnswers { slot.captured(client) } } private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(sdkClientManager) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt index a217567a54..c1c093e479 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt @@ -42,11 +42,11 @@ object PlatformNetworkModule { @Provides @Singleton - fun provideBitwardenServiceClient( + fun provideBitwardenServiceClientConfig( baseUrlsProvider: BaseUrlsProvider, authDiskSource: AuthDiskSource, clock: Clock, - ): BitwardenServiceClient = bitwardenServiceClient( + ): BitwardenServiceClientConfig = BitwardenServiceClientConfig( clock = clock, appIdProvider = authDiskSource, @@ -59,6 +59,7 @@ object PlatformNetworkModule { enableHttpBodyLogging = BuildConfig.DEBUG, authTokenProvider = object : AuthTokenProvider { override fun getAuthTokenDataOrNull(): AuthTokenData? = null + override fun getAuthTokenDataOrNull(userId: String): AuthTokenData? = null }, certificateProvider = object : CertificateProvider { @@ -66,7 +67,7 @@ object PlatformNetworkModule { keyType: Array?, issuers: Array?, socket: Socket?, - ) = "" + ): String = "" override fun getCertificateChain(alias: String?): Array? = null @@ -77,9 +78,16 @@ object PlatformNetworkModule { override fun getCookies(hostname: String): List = emptyList() - override fun acquireCookies(hostname: String) = Unit + override fun acquireCookies(hostname: String): Unit = Unit }, - ), + ) + + @Provides + @Singleton + fun provideBitwardenServiceClient( + serviceClientConfig: BitwardenServiceClientConfig, + ): BitwardenServiceClient = bitwardenServiceClient( + config = serviceClientConfig, ) @Provides diff --git a/network/src/main/kotlin/com/bitwarden/network/model/BitwardenServiceClientConfig.kt b/network/src/main/kotlin/com/bitwarden/network/model/BitwardenServiceClientConfig.kt index 5fb5440112..8473c59aaa 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/BitwardenServiceClientConfig.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/BitwardenServiceClientConfig.kt @@ -18,7 +18,7 @@ data class BitwardenServiceClientConfig( val authTokenProvider: AuthTokenProvider, val certificateProvider: CertificateProvider, val cookieProvider: CookieProvider, - val clock: Clock = Clock.systemDefaultZone(), + val clock: Clock, val enableHttpBodyLogging: Boolean = false, ) { /**