diff --git a/demo-app/build.gradle.kts b/demo-app/build.gradle.kts index d69dbbc09eb..63edad05124 100644 --- a/demo-app/build.gradle.kts +++ b/demo-app/build.gradle.kts @@ -292,6 +292,9 @@ dependencies { implementation(libs.audioswitch) + // Logging + implementation(libs.okhttp.logging) + // Also Leak Canary added in the previous block // Instrumentation tests diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt index eb2262c0032..afe36995457 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt @@ -17,11 +17,14 @@ package io.getstream.video.android.data.services.stream import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import io.getstream.log.streamLog import io.getstream.video.android.model.User import io.getstream.video.android.models.UserCredentials import io.getstream.video.android.models.builtInCredentials import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.create import retrofit2.http.GET @@ -32,23 +35,34 @@ fun interface StreamService { suspend fun getAuthData( @Query("environment") environment: String, @Query("user_id") userId: String?, + @Query("exp") exp: Int, ): GetAuthDataResponse companion object { private const val BASE_URL = "https://pronto.getstream.io/" + const val TOKEN_EXPIRY_TIME = 7 * 24 * 60 * 60 // 7d * 24 hrs * 60 mins * 60 sec private val json = Json { ignoreUnknownKeys = true } - + private val okHttpClient = OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor { + streamLog(tag = "Video:Http") { it } + }.apply { + level = HttpLoggingInterceptor.Level.BODY + }, + ) + .build() private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .client(okHttpClient) .build() private val serviceInstance = retrofit.create() - val instance = StreamService { environment, userId -> + val instance = StreamService { environment, userId, exp -> User.builtInCredentials[userId]?.toAuthDataResponse() - ?: serviceInstance.getAuthData(environment, userId) + ?: serviceInstance.getAuthData(environment, userId, exp) } } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt index c66b2a9649d..a200efe5b29 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt @@ -92,6 +92,7 @@ class LoginViewModel @Inject constructor( val authData = StreamService.instance.getAuthData( environment = it.env, userId = userId, + StreamService.TOKEN_EXPIRY_TIME, ) val loggedInGoogleUser = if (autoLogIn) null else googleAccountRepository.getCurrentUser() @@ -128,6 +129,7 @@ class LoginViewModel @Inject constructor( val authData = StreamService.instance.getAuthData( environment = it.env, userId = user.id, + StreamService.TOKEN_EXPIRY_TIME, ) // Store the data in the demo app dataStore.updateUser(user) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt index f8cb6ea6b0c..0bd321a37fd 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt @@ -61,6 +61,7 @@ import io.getstream.video.android.util.config.AppConfig import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking public enum class InitializedState { NOT_STARTED, RUNNING, FINISHED, FAILED @@ -124,6 +125,7 @@ object StreamVideoInitHelper { authData = StreamService.instance.getAuthData( environment = AppConfig.currentEnvironment.value!!.env, userId = userId, + StreamService.TOKEN_EXPIRY_TIME, ) loggedInUser = User(id = authData.userId, role = "admin") @@ -140,6 +142,7 @@ object StreamVideoInitHelper { authData = StreamService.instance.getAuthData( environment = AppConfig.currentEnvironment.value!!.env, userId = loggedInUser.id, + StreamService.TOKEN_EXPIRY_TIME, ) } @@ -197,7 +200,19 @@ object StreamVideoInitHelper { chatClient.connectUser( user = chatUser, - token = token, + tokenProvider = object : io.getstream.chat.android.client.token.TokenProvider { + override fun loadToken(): String { + return runBlocking { + val email = user.custom?.get("email") + val authData = StreamService.instance.getAuthData( + environment = AppConfig.currentEnvironment.value!!.env, + userId = email, + StreamService.TOKEN_EXPIRY_TIME, + ) + authData.token + } + } + }, ).enqueue() } @@ -303,10 +318,13 @@ object StreamVideoInitHelper { ), tokenProvider = object : TokenProvider { override suspend fun loadToken(): String { - val email = user.custom?.get("email") + val userEmail = user.custom?.get("email") + val userId = user.id + val userIdForTokenRenewal = if (userEmail.isNullOrEmpty()) userId else userEmail val authData = StreamService.instance.getAuthData( environment = AppConfig.currentEnvironment.value!!.env, - userId = email, + userId = userIdForTokenRenewal, + StreamService.TOKEN_EXPIRY_TIME, ) return authData.token } diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 33372d00a4c..f4a89cf5d4f 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -13118,9 +13118,17 @@ public abstract interface class io/getstream/video/android/core/socket/common/to public abstract fun loadToken (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/getstream/video/android/core/socket/common/token/TokenRepository { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getToken ()Ljava/lang/String; + public final fun updateToken (Ljava/lang/String;)V +} + public class io/getstream/video/android/core/socket/coordinator/CoordinatorSocketConnection : io/getstream/video/android/core/socket/common/SocketListener, io/getstream/video/android/core/socket/common/SocketActions { - public fun (Ljava/lang/String;Ljava/lang/String;Lio/getstream/video/android/model/User;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/getstream/video/android/model/User;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Lio/getstream/video/android/model/User;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;Lio/getstream/video/android/core/socket/common/token/TokenRepository;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/getstream/video/android/model/User;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;Lio/getstream/video/android/core/socket/common/token/TokenRepository;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun connect (Lio/getstream/video/android/model/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public synthetic fun connect (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun connectionId ()Lkotlinx/coroutines/flow/StateFlow; @@ -13327,8 +13335,8 @@ public final class io/getstream/video/android/core/socket/coordinator/state/Vide public final class io/getstream/video/android/core/socket/sfu/SfuSocketConnection : io/getstream/video/android/core/socket/common/SocketListener, io/getstream/video/android/core/socket/common/SocketActions { public static final field Companion Lio/getstream/video/android/core/socket/sfu/SfuSocketConnection$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;Lio/getstream/video/android/core/socket/common/token/TokenRepository;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lokhttp3/OkHttpClient;Lio/getstream/video/android/core/internal/network/NetworkStateProvider;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/Lifecycle;Lio/getstream/video/android/core/socket/common/token/TokenProvider;Lio/getstream/video/android/core/socket/common/token/TokenRepository;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun connect (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun connect (Lstream/video/sfu/event/JoinRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun connectionId ()Lkotlinx/coroutines/flow/StateFlow; diff --git a/stream-video-android-core/build.gradle.kts b/stream-video-android-core/build.gradle.kts index eeb7212e355..fa87e3d674e 100644 --- a/stream-video-android-core/build.gradle.kts +++ b/stream-video-android-core/build.gradle.kts @@ -240,3 +240,27 @@ mavenPublishing { ), ) } + +afterEvaluate { + tasks.named("testDebugUnitTest") { + dependsOn("isolatedTest") + } +} + +tasks.register("isolatedTest") { + + description = "Runs StreamVideoBuilderTest in an isolation" + group = "verification" + + // Only this class + include("**/StreamVideoBuilderTest.class") + + // Force new JVM + forkEvery = 1 + + // Reuse Android's debug unit test configuration + val androidTestTask = tasks.named("testDebugUnitTest").get() + + testClassesDirs = androidTestTask.testClassesDirs + classpath = androidTestTask.classpath +} diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt index cc9dad27aed..be76766b99a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt @@ -37,8 +37,9 @@ import io.getstream.video.android.core.permission.android.DefaultStreamPermissio import io.getstream.video.android.core.permission.android.StreamPermissionCheck import io.getstream.video.android.core.socket.common.scope.ClientScope import io.getstream.video.android.core.socket.common.scope.UserScope -import io.getstream.video.android.core.socket.common.token.ConstantTokenProvider +import io.getstream.video.android.core.socket.common.token.RepositoryTokenProvider import io.getstream.video.android.core.socket.common.token.TokenProvider +import io.getstream.video.android.core.socket.common.token.TokenRepository import io.getstream.video.android.core.sounds.RingingCallVibrationConfig import io.getstream.video.android.core.sounds.Sounds import io.getstream.video.android.core.sounds.defaultResourcesRingingConfig @@ -95,6 +96,7 @@ import java.net.ConnectException * @see ClientState.connection * */ + public class StreamVideoBuilder @JvmOverloads constructor( context: Context, private val apiKey: ApiKey, @@ -106,7 +108,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( object : TokenProvider { override suspend fun loadToken(): String = legacy.invoke(null) } - } ?: ConstantTokenProvider(token), + } ?: RepositoryTokenProvider(tokenRepository), private val loggingLevel: LoggingLevel = LoggingLevel(), private val notificationConfig: NotificationConfig = NotificationConfig(), private val ringNotification: ((call: Call) -> Notification?)? = null, @@ -214,7 +216,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( // Android JSR-310 backport backport AndroidThreeTen.init(context) - + tokenRepository.updateToken(token) // This connection module class exposes the connections to the various retrofit APIs. val coordinatorConnectionModule = CoordinatorConnectionModule( context = context, @@ -225,9 +227,9 @@ public class StreamVideoBuilder @JvmOverloads constructor( loggingLevel = loggingLevel, user = user, apiKey = apiKey, - userToken = token, tokenProvider = tokenProvider, lifecycle = lifecycle, + tokenRepository = tokenRepository, ) val deviceTokenStorage = DeviceTokenStorage(context) @@ -272,6 +274,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( vibrationConfig = vibrationConfig, enableStereoForSubscriber = enableStereoForSubscriber, telecomConfig = telecomConfig, + tokenRepository = tokenRepository, ) if (user.type == UserType.Guest) { @@ -348,6 +351,11 @@ public class StreamVideoBuilder @JvmOverloads constructor( } } +/** + * Refactor Later + */ +internal val tokenRepository = TokenRepository("") + sealed class GEO { /** Run calls over our global edge network, this is the default and right for most applications */ object GlobalEdgeNetwork : GEO() diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt index eb536dde76b..35665871dc5 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt @@ -107,8 +107,9 @@ import io.getstream.video.android.core.permission.android.DefaultStreamPermissio import io.getstream.video.android.core.permission.android.StreamPermissionCheck import io.getstream.video.android.core.socket.ErrorResponse import io.getstream.video.android.core.socket.common.scope.ClientScope -import io.getstream.video.android.core.socket.common.token.ConstantTokenProvider +import io.getstream.video.android.core.socket.common.token.RepositoryTokenProvider import io.getstream.video.android.core.socket.common.token.TokenProvider +import io.getstream.video.android.core.socket.common.token.TokenRepository import io.getstream.video.android.core.socket.coordinator.state.VideoSocketState import io.getstream.video.android.core.sounds.CallSoundAndVibrationPlayer import io.getstream.video.android.core.sounds.RingingCallVibrationConfig @@ -161,7 +162,8 @@ internal class StreamVideoClient internal constructor( internal var token: String, private val lifecycle: Lifecycle, internal val coordinatorConnectionModule: CoordinatorConnectionModule, - internal val tokenProvider: TokenProvider = ConstantTokenProvider(token), + internal val tokenRepository: TokenRepository, + internal val tokenProvider: TokenProvider = RepositoryTokenProvider(tokenRepository), internal val streamNotificationManager: StreamNotificationManager, internal val enableCallNotificationUpdates: Boolean, internal val callServiceConfigRegistry: CallServiceConfigRegistry = CallServiceConfigRegistry(), @@ -273,6 +275,7 @@ internal class StreamVideoClient internal constructor( // Retry once with a new token if the token is expired if (e.isAuthError()) { val newToken = tokenProvider.loadToken() + tokenRepository.updateToken(newToken) token = newToken coordinatorConnectionModule.updateToken(newToken) apiCall() diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index 6fec1eab39e..daf2a60d7bc 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -78,6 +78,7 @@ import io.getstream.video.android.core.model.VideoTrack import io.getstream.video.android.core.model.toPeerType import io.getstream.video.android.core.socket.common.VideoParser import io.getstream.video.android.core.socket.common.parser2.MoshiVideoParser +import io.getstream.video.android.core.socket.common.token.TokenRepository import io.getstream.video.android.core.socket.sfu.state.SfuSocketState import io.getstream.video.android.core.toJson import io.getstream.video.android.core.trace.PeerConnectionTraceKey @@ -236,12 +237,12 @@ public class RtcSession internal constructor( apiUrl = sfuUrl, wssUrl = sfuWsUrl, connectionTimeoutInMs = 2000L, - userToken = sfuToken, lifecycle = lifecycle, onSignalingLost = { error -> call.debug.fastReconnect() }, tracer = sfuTracer, + tokenRepository = TokenRepository(sfuToken), ) }, ) { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModuleDeclaration.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModuleDeclaration.kt index 98ac4d93838..999a73deb19 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModuleDeclaration.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/ConnectionModuleDeclaration.kt @@ -76,11 +76,6 @@ internal interface ConnectionModuleDeclaration { // Internals - private val authInterceptor = CoordinatorAuthInterceptor(apiKey, userToken) + private val authInterceptor = CoordinatorAuthInterceptor(apiKey, tokenRepository) private val retrofit: Retrofit by lazy { Retrofit.Builder().baseUrl(apiUrl) .addConverterFactory(ScalarsConverterFactory.create()) @@ -96,17 +99,17 @@ internal class CoordinatorConnectionModule( apiKey = apiKey, url = wssUrl, user = user, - token = userToken, + token = tokenRepository.getToken(), httpClient = http, networkStateProvider = networkStateProvider, - scope = scope, + scope = UserScope(context = scope.coroutineContext + Dispatchers.IO.limitedParallelism(1)), lifecycle = lifecycle, tokenProvider = tokenProvider, + tokenRepository = tokenRepository, ) override fun updateToken(token: UserToken) { - socketConnection.updateToken(token) - authInterceptor.token = token + tokenRepository.updateToken(token) } override fun updateAuthType(authType: String) { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/SfuConnectionModule.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/SfuConnectionModule.kt index e0a95525ee2..708707f9594 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/SfuConnectionModule.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/module/SfuConnectionModule.kt @@ -23,6 +23,7 @@ import io.getstream.video.android.core.api.SignalServerService import io.getstream.video.android.core.call.utils.SignalLostSignalingServiceDecorator import io.getstream.video.android.core.internal.network.NetworkStateProvider import io.getstream.video.android.core.socket.common.token.ConstantTokenProvider +import io.getstream.video.android.core.socket.common.token.TokenRepository import io.getstream.video.android.core.socket.sfu.SfuSocketConnection import io.getstream.video.android.core.trace.Tracer import io.getstream.video.android.core.trace.tracedWith @@ -37,11 +38,11 @@ import java.util.concurrent.TimeUnit internal class SfuConnectionModule( context: Context, + val tokenRepository: TokenRepository, override val apiKey: ApiKey, override val apiUrl: String, override val wssUrl: String, override val connectionTimeoutInMs: Long, - override val userToken: SfuToken, override val lifecycle: Lifecycle, override val tracer: Tracer, val onSignalingLost: (Error) -> Unit, @@ -57,7 +58,7 @@ internal class SfuConnectionModule( private fun buildSfuOkHttpClient(): OkHttpClient { val connectionTimeoutInMs = 10000L // create a new OkHTTP client and set timeouts - val authInterceptor = CoordinatorAuthInterceptor(apiKey, userToken) + val authInterceptor = CoordinatorAuthInterceptor(apiKey, tokenRepository) return OkHttpClient.Builder().addInterceptor(authInterceptor).addInterceptor( HttpLoggingInterceptor().apply { level = loggingLevel.httpLoggingLevel.level @@ -93,13 +94,15 @@ internal class SfuConnectionModule( apiKey = apiKey, scope = scope, httpClient = http, - tokenProvider = ConstantTokenProvider(userToken), + tokenProvider = ConstantTokenProvider(tokenRepository.getToken()), lifecycle = lifecycle, networkStateProvider = networkStateProvider, + tokenRepository = tokenRepository, ) override val socketConnection: SfuSocketConnection = _internalSocketConnection override fun updateToken(token: SfuToken) { + tokenRepository.updateToken(token) _internalSocketConnection.updateToken(token) } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/SocketActions.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/SocketActions.kt index 76b476cd88e..2570bd9be50 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/SocketActions.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/SocketActions.kt @@ -41,6 +41,7 @@ interface SocketActions { /** * Send raw data to the socket. If you already have a parsed event that can be sent. + * Refactor later to return bool */ fun sendData(data: String) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/CacheableTokenProvider.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/CacheableTokenProvider.kt index 39d127234ac..5f7d092a53f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/CacheableTokenProvider.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/CacheableTokenProvider.kt @@ -22,8 +22,10 @@ package io.getstream.video.android.core.socket.common.token * * @property tokenProvider The [TokenProvider] used to obtain new tokens. */ -internal class CacheableTokenProvider(private val tokenProvider: TokenProvider) : TokenProvider { - private var cachedToken = "" +internal class CacheableTokenProvider( + private val tokenProvider: TokenProvider, +) : TokenProvider { + internal var cachedToken = "" override suspend fun loadToken(): String = tokenProvider.loadToken().also { cachedToken = it } /** diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/PersistingTokenProvider.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/PersistingTokenProvider.kt new file mode 100644 index 00000000000..38e8199a4fd --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/PersistingTokenProvider.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.socket.common.token + +internal class PersistingTokenProvider( + private val tokenProvider: TokenProvider, + private val tokenRepository: TokenRepository, +) : TokenProvider { + override suspend fun loadToken(): String = tokenProvider.loadToken().also { + tokenRepository.updateToken(it) + } +} diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/RepositoryTokenProvider.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/RepositoryTokenProvider.kt new file mode 100644 index 00000000000..54e6a1c1c8e --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/RepositoryTokenProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.socket.common.token + +internal class RepositoryTokenProvider(val tokenRepository: TokenRepository) : TokenProvider { + override suspend fun loadToken(): String { + return tokenRepository.getToken() + } +} diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManager.kt index abd71237c98..e7b11fdcc19 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManager.kt @@ -51,6 +51,8 @@ internal interface TokenManager { */ fun setTokenProvider(provider: CacheableTokenProvider) + fun setTokenProvider(provider: PersistingTokenProvider) + /** * Obtain last token loaded. * diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManagerImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManagerImpl.kt index e4c1bca4e62..c5fef2d54ec 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManagerImpl.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenManagerImpl.kt @@ -16,13 +16,12 @@ package io.getstream.video.android.core.socket.common.token -internal class TokenManagerImpl : TokenManager { - @Volatile - private var token: String = EMPTY_TOKEN +internal class TokenManagerImpl(private val tokenRepository: TokenRepository) : TokenManager { + private lateinit var provider: TokenProvider override fun updateToken(token: String) { - this.token = token + this.tokenRepository.updateToken(token) } override suspend fun ensureTokenLoaded() { @@ -33,27 +32,30 @@ internal class TokenManagerImpl : TokenManager { override suspend fun loadSync(): String { return provider.loadToken().also { - this.token = it + this.tokenRepository.updateToken(it) } } override fun setTokenProvider(provider: CacheableTokenProvider) { this.provider = provider - this.token = provider.getCachedToken() + } + + override fun setTokenProvider(provider: PersistingTokenProvider) { + this.provider = provider } override fun hasTokenProvider(): Boolean { return this::provider.isInitialized } - override fun getToken(): String = token + override fun getToken(): String = tokenRepository.getToken() override fun hasToken(): Boolean { - return token != EMPTY_TOKEN + return tokenRepository.getToken() != EMPTY_TOKEN } override fun expireToken() { - token = EMPTY_TOKEN + tokenRepository.updateToken(EMPTY_TOKEN) } companion object { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenRepository.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenRepository.kt new file mode 100644 index 00000000000..f9036448822 --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/common/token/TokenRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.socket.common.token + +import io.getstream.video.android.core.socket.common.token.TokenManagerImpl.Companion.EMPTY_TOKEN + +class TokenRepository(@Volatile private var token: String = EMPTY_TOKEN) { + + fun updateToken(token: String) { + this.token = token + } + + fun getToken(): String { + return token + } +} diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocket.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocket.kt index 7e939e92951..890e82b359a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocket.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocket.kt @@ -278,11 +278,20 @@ internal open class CoordinatorSocket( tokenManager.expireToken() } + when (error.serverErrorCode) { + VideoErrorCode.TOKEN_EXPIRED.code, + -> { + tokenManager.expireToken() + val token = tokenManager.loadSync() + tokenManager.updateToken(token) + } + else -> {} + } + when (error.serverErrorCode) { VideoErrorCode.UNDEFINED_TOKEN.code, VideoErrorCode.INVALID_TOKEN.code, VideoErrorCode.API_KEY_NOT_FOUND.code, - VideoErrorCode.VALIDATION_ERROR.code, -> { logger.d { "One unrecoverable error happened. Error: $error. Error code: ${error.serverErrorCode}" @@ -290,7 +299,9 @@ internal open class CoordinatorSocket( coordinatorSocketStateService.onUnrecoverableError(error) } - else -> coordinatorSocketStateService.onNetworkError(error) + else -> { + coordinatorSocketStateService.onNetworkError(error) + } } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocketConnection.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocketConnection.kt index a81c9ae669d..93b48979551 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocketConnection.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/coordinator/CoordinatorSocketConnection.kt @@ -35,9 +35,10 @@ import io.getstream.video.android.core.socket.common.VideoParser import io.getstream.video.android.core.socket.common.parser2.MoshiVideoParser import io.getstream.video.android.core.socket.common.scope.ClientScope import io.getstream.video.android.core.socket.common.scope.UserScope -import io.getstream.video.android.core.socket.common.token.CacheableTokenProvider +import io.getstream.video.android.core.socket.common.token.PersistingTokenProvider import io.getstream.video.android.core.socket.common.token.TokenManagerImpl import io.getstream.video.android.core.socket.common.token.TokenProvider +import io.getstream.video.android.core.socket.common.token.TokenRepository import io.getstream.video.android.core.socket.coordinator.state.VideoSocketState import io.getstream.video.android.core.utils.isWhitespaceOnly import io.getstream.video.android.core.utils.mapState @@ -63,6 +64,8 @@ import okhttp3.OkHttpClient * - Raises the error if there is a permanent failure * - Flow to avoid concurrency related bugs * - Ability to wait till the socket is connected (important to prevent race conditions) + * + * This should be internal */ public open class CoordinatorSocketConnection( private val apiKey: ApiKey, @@ -71,6 +74,10 @@ public open class CoordinatorSocketConnection( /** The user to connect. */ private val user: User, /** The initial token. */ + @Deprecated( + "token is not used", + ReplaceWith("Use tokenManager or tokenRepository.getToken() instead"), + ) private val token: String, /** Inject your http client */ private val httpClient: OkHttpClient, @@ -82,12 +89,13 @@ public open class CoordinatorSocketConnection( private val lifecycle: Lifecycle, /** Token provider */ private val tokenProvider: TokenProvider, + private val tokenRepository: TokenRepository, ) : SocketListener(), SocketActions { // Private state private val parser: VideoParser = MoshiVideoParser() - private val tokenManager = TokenManagerImpl() + private val tokenManager = TokenManagerImpl(tokenRepository) // Internal state private val logger by taggedLogger("Video:Socket") @@ -121,7 +129,7 @@ public open class CoordinatorSocketConnection( // Init init { - tokenManager.setTokenProvider(CacheableTokenProvider(tokenProvider)) + tokenManager.setTokenProvider(PersistingTokenProvider(tokenProvider, tokenRepository)) } // Extension opportunity for subclasses @@ -130,12 +138,12 @@ public open class CoordinatorSocketConnection( logger.d { "[onCreated] Socket is created" } scope.launch { logger.d { "[onConnected] Video socket created, user: $user" } - if (token.isEmpty()) { + if (tokenManager.getToken().isEmpty()) { logger.e { "[onConnected] Token is empty. Disconnecting." } disconnect() } else { val authRequest = WSAuthMessageRequest( - token = token, + token = tokenManager.getToken().ifEmpty { tokenRepository.getToken() }, userDetails = ConnectUserDetailsRequest( id = user.id, name = user.name.takeUnless { it.isWhitespaceOnly() }, @@ -189,7 +197,9 @@ public open class CoordinatorSocketConnection( override fun onDisconnected(cause: DisconnectCause) { super.onDisconnected(cause) connectionId.value = null - logger.d { "[onDisconnected] Socket disconnected. Cause: $cause" } + logger.d { + "[onDisconnected] Socket disconnected. Cause: ${(cause as? DisconnectCause.Error)?.error}" + } } // API diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocketConnection.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocketConnection.kt index 07693ed8019..73fef0b76d5 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocketConnection.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocketConnection.kt @@ -31,9 +31,10 @@ import io.getstream.video.android.core.socket.common.SocketListener import io.getstream.video.android.core.socket.common.StreamWebSocketEvent import io.getstream.video.android.core.socket.common.scope.ClientScope import io.getstream.video.android.core.socket.common.scope.UserScope -import io.getstream.video.android.core.socket.common.token.CacheableTokenProvider +import io.getstream.video.android.core.socket.common.token.PersistingTokenProvider import io.getstream.video.android.core.socket.common.token.TokenManagerImpl import io.getstream.video.android.core.socket.common.token.TokenProvider +import io.getstream.video.android.core.socket.common.token.TokenRepository import io.getstream.video.android.core.socket.sfu.state.SfuSocketState import io.getstream.video.android.core.utils.mapState import io.getstream.video.android.model.ApiKey @@ -62,6 +63,7 @@ class SfuSocketConnection( private val lifecycle: Lifecycle, /** Token provider */ private val tokenProvider: TokenProvider, + private val tokenRepository: TokenRepository, ) : SocketListener(), SocketActions { @@ -70,7 +72,7 @@ class SfuSocketConnection( } private val logger by taggedLogger("Video:SfuSocket") - private val tokenManager = TokenManagerImpl() + private val tokenManager = TokenManagerImpl(tokenRepository) private val internalSocket: SfuSocket = SfuSocket( wssUrl = url, apiKey = apiKey, @@ -100,7 +102,7 @@ class SfuSocketConnection( // Initialization init { - tokenManager.setTokenProvider(CacheableTokenProvider(tokenProvider)) + tokenManager.setTokenProvider(PersistingTokenProvider(tokenProvider, tokenRepository)) } override fun onCreated() { diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoBuilderTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoBuilderTest.kt index 6af2d2de684..df766d31c88 100644 --- a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoBuilderTest.kt +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoBuilderTest.kt @@ -25,6 +25,7 @@ import io.mockk.every import io.mockk.mockk import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -32,6 +33,9 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.concurrent.thread +@Ignore( + "Because it installs a anonymous user StreamVideo Client. However we will run it via isolatedTest (check build.gradle.kts)", +) @RunWith(RobolectricTestRunner::class) class StreamVideoBuilderTest : TestBase() { @@ -109,6 +113,7 @@ class StreamVideoBuilderTest : TestBase() { assertEquals(client.coordinatorConnectionModule.apiUrl, customApiUrl) assertEquals(client.coordinatorConnectionModule.wssUrl, customWssUrl) + StreamVideo.removeClient() } @Test diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/RepositoryTokenProviderTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/RepositoryTokenProviderTest.kt new file mode 100644 index 00000000000..ec790a6926d --- /dev/null +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/RepositoryTokenProviderTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.socket.common + +import io.getstream.video.android.core.socket.common.token.RepositoryTokenProvider +import io.getstream.video.android.core.socket.common.token.TokenRepository +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RepositoryTokenProviderTest { + private val tokenRepository: TokenRepository = mockk() + private val provider = RepositoryTokenProvider(tokenRepository) + + @Test + fun `loadToken should return token from repository`() = runTest { + val expectedToken = "jwt_123" + every { tokenRepository.getToken() } returns expectedToken + val result = provider.loadToken() + assertEquals(expectedToken, result) + } +} diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/token/PersistingTokenProviderTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/token/PersistingTokenProviderTest.kt new file mode 100644 index 00000000000..8b29f7ee5e3 --- /dev/null +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/token/PersistingTokenProviderTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.socket.common.token + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +class PersistingTokenProviderTest { + private val tokenProvider: TokenProvider = mockk() + private val tokenRepository: TokenRepository = mockk(relaxed = true) + + private val provider = PersistingTokenProvider(tokenProvider, tokenRepository) + + @Test + fun `loadToken should return token from provider and persist it`() = runTest { + // Given + val expectedToken = "jwt_123" + coEvery { tokenProvider.loadToken() } returns expectedToken + + // When + val result = provider.loadToken() + + // Then + assertEquals(expectedToken, result) + + // Verify internal behavior: + coVerify(exactly = 1) { tokenProvider.loadToken() } + coVerify(exactly = 1) { tokenRepository.updateToken(expectedToken) } + } +} diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/token/TokenManagerImplTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/token/TokenManagerImplTest.kt new file mode 100644 index 00000000000..8bdb5dffc29 --- /dev/null +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/socket/common/token/TokenManagerImplTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.socket.common.token + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class TokenManagerImplTest { + private lateinit var tokenRepository: TokenRepository + private lateinit var tokenManager: TokenManagerImpl + private lateinit var provider: PersistingTokenProvider + + @Before + fun setup() { + tokenRepository = mockk(relaxed = true) + provider = mockk() + tokenManager = TokenManagerImpl(tokenRepository) + } + + @After + fun tearDown() { + unmockkAll() + } + + // --------------------------------------------------------- + // updateToken + // --------------------------------------------------------- + @Test + fun `updateToken should update repository`() { + val token = "ABC" + + tokenManager.updateToken(token) + + verify { tokenRepository.updateToken(token) } + } + + // --------------------------------------------------------- + // getToken + hasToken + // --------------------------------------------------------- + @Test + fun `getToken should return token from repository`() { + every { tokenRepository.getToken() } returns "JWT123" + + val result = tokenManager.getToken() + + assertEquals("JWT123", result) + } + + @Test + fun `hasToken should return true when token is not EMPTY_TOKEN`() { + every { tokenRepository.getToken() } returns "VALID" + + assertTrue(tokenManager.hasToken()) + } + + @Test + fun `hasToken should return false when token is EMPTY_TOKEN`() { + every { tokenRepository.getToken() } returns TokenManagerImpl.EMPTY_TOKEN + + assertFalse(tokenManager.hasToken()) + } + + // --------------------------------------------------------- + // expireToken + // --------------------------------------------------------- + @Test + fun `expireToken should update repository with EMPTY_TOKEN`() { + tokenManager.expireToken() + + verify { tokenRepository.updateToken(TokenManagerImpl.EMPTY_TOKEN) } + } + + // --------------------------------------------------------- + // setTokenProvider + hasTokenProvider + // --------------------------------------------------------- + @Test + fun `hasTokenProvider returns false before provider is set`() { + assertFalse(tokenManager.hasTokenProvider()) + } + + @Test + fun `hasTokenProvider returns true after provider is set`() { + tokenManager.setTokenProvider(mockk()) + + assertTrue(tokenManager.hasTokenProvider()) + } + + // --------------------------------------------------------- + // loadSync + // --------------------------------------------------------- + @Test + fun `loadSync should load token from provider and persist it`() = runTest { + val expectedToken = "fresh_token" + coEvery { provider.loadToken() } returns expectedToken + tokenManager.setTokenProvider(provider) + + val result = tokenManager.loadSync() + + assertEquals(expectedToken, result) + coVerify { provider.loadToken() } + verify { tokenRepository.updateToken(expectedToken) } + } + + // --------------------------------------------------------- + // ensureTokenLoaded + // --------------------------------------------------------- + @Test + fun `ensureTokenLoaded loads token when no token exists`() = runTest { + // No token present + every { tokenRepository.getToken() } returns TokenManagerImpl.EMPTY_TOKEN + + coEvery { provider.loadToken() } returns "new_token" + tokenManager.setTokenProvider(provider) + + tokenManager.ensureTokenLoaded() + + coVerify { provider.loadToken() } + verify { tokenRepository.updateToken("new_token") } + } + + @Test + fun `ensureTokenLoaded does nothing when token already exists`() = runTest { + every { tokenRepository.getToken() } returns "already_present" + + tokenManager.setTokenProvider(provider) + + tokenManager.ensureTokenLoaded() + + coVerify(exactly = 0) { provider.loadToken() } + } +} diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/token/FakeTokenManager.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/token/FakeTokenManager.kt index 3b8b62cdf0c..b5ddc139e82 100644 --- a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/token/FakeTokenManager.kt +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/token/FakeTokenManager.kt @@ -17,6 +17,7 @@ package io.getstream.video.android.core.token import io.getstream.video.android.core.socket.common.token.CacheableTokenProvider +import io.getstream.video.android.core.socket.common.token.PersistingTokenProvider import io.getstream.video.android.core.socket.common.token.TokenManager internal class FakeTokenManager( @@ -34,6 +35,10 @@ internal class FakeTokenManager( // empty } + override fun setTokenProvider(provider: PersistingTokenProvider) { + // empty + } + override fun hasTokenProvider(): Boolean { return true }