From 8063dbfa8cba235220fd39cda1c9d0aa4dc26710 Mon Sep 17 00:00:00 2001 From: Themis wang Date: Tue, 15 Apr 2025 14:32:43 -0400 Subject: [PATCH] add unit tests for session Repo --- .../sessions/SharedSessionRepository.kt | 23 +- .../sessions/SharedSessionRepositoryTest.kt | 232 ++++++++++++++++++ 2 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt index 88189c5fe4a..7c25ab7f8c0 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt @@ -29,7 +29,6 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch -import org.jetbrains.annotations.VisibleForTesting /** Repository to persist session data to be shared between all app processes. */ internal interface SharedSessionRepository { @@ -50,16 +49,17 @@ constructor( @Background private val backgroundDispatcher: CoroutineContext, ) : SharedSessionRepository { /** Local copy of the session data. Can get out of sync, must be double-checked in datastore. */ - @VisibleForTesting lateinit var localSessionData: SessionData + internal lateinit var localSessionData: SessionData /** * Either notify the subscribers with general multi-process supported session or fallback local * session */ - private enum class NotificationType { + internal enum class NotificationType { GENERAL, FALLBACK } + internal var previousNotificationType: NotificationType = NotificationType.GENERAL init { println("session repo init") @@ -142,18 +142,19 @@ constructor( } private suspend fun notifySubscribers(sessionId: String, type: NotificationType) { + previousNotificationType = type FirebaseSessionsDependencies.getRegisteredSubscribers().values.forEach { subscriber -> // Notify subscribers, regardless of sampling and data collection state subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionId)) - when (type) { - NotificationType.GENERAL -> - Log.d(TAG, "Notified ${subscriber.sessionSubscriberName} of new session $sessionId") - NotificationType.FALLBACK -> - Log.d( - TAG, + Log.d( + TAG, + when (type) { + NotificationType.GENERAL -> + "Notified ${subscriber.sessionSubscriberName} of new session $sessionId" + NotificationType.FALLBACK -> "Notified ${subscriber.sessionSubscriberName} of new fallback session $sessionId" - ) - } + } + ) } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt new file mode 100644 index 00000000000..301f748f05a --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.google.firebase.sessions + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.FirebaseApp +import com.google.firebase.concurrent.TestOnlyExecutors +import com.google.firebase.sessions.SessionGeneratorTest.Companion.SESSION_ID_1 +import com.google.firebase.sessions.SessionGeneratorTest.Companion.SESSION_ID_2 +import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.testing.FakeDataStore +import com.google.firebase.sessions.testing.FakeEventGDTLogger +import com.google.firebase.sessions.testing.FakeFirebaseApp +import com.google.firebase.sessions.testing.FakeFirebaseInstallations +import com.google.firebase.sessions.testing.FakeSettingsProvider +import com.google.firebase.sessions.testing.FakeTimeProvider +import com.google.firebase.sessions.testing.FakeUuidGenerator +import kotlin.time.Duration.Companion.hours +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class SharedSessionRepositoryTest { + private val fakeFirebaseApp = FakeFirebaseApp() + private val fakeEventGDTLogger = FakeEventGDTLogger() + private val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD", "FakeAuthToken") + private var fakeTimeProvider = FakeTimeProvider() + private val sessionGenerator = SessionGenerator(fakeTimeProvider, FakeUuidGenerator()) + private var localSettingsProvider = FakeSettingsProvider(true, null, 100.0) + private var remoteSettingsProvider = FakeSettingsProvider(true, null, 100.0) + private var sessionsSettings = SessionsSettings(localSettingsProvider, remoteSettingsProvider) + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } + + @Test + fun initSharedSessionRepo_readFromDatastore() = runTest { + val publisher = + SessionFirelogPublisherImpl( + fakeFirebaseApp.firebaseApp, + firebaseInstallations, + sessionsSettings, + eventGDTLogger = fakeEventGDTLogger, + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + ) + val fakeDataStore = + FakeDataStore( + SessionData( + SessionDetails( + SESSION_ID_INIT, + SESSION_ID_INIT, + 0, + fakeTimeProvider.currentTime().ms, + ), + fakeTimeProvider.currentTime(), + ) + ) + val sharedSessionRepository = + SharedSessionRepositoryImpl( + sessionsSettings = sessionsSettings, + sessionGenerator = sessionGenerator, + sessionFirelogPublisher = publisher, + timeProvider = fakeTimeProvider, + sessionDataStore = fakeDataStore, + backgroundDispatcher = + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + ) + runCurrent() + fakeDataStore.close() + assertThat(sharedSessionRepository.localSessionData.sessionDetails.sessionId) + .isEqualTo(SESSION_ID_INIT) + } + + @Test + fun initSharedSessionRepo_initException() = runTest { + val sessionFirelogPublisher = + SessionFirelogPublisherImpl( + fakeFirebaseApp.firebaseApp, + firebaseInstallations, + sessionsSettings, + eventGDTLogger = fakeEventGDTLogger, + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + ) + val fakeDataStore = + FakeDataStore( + SessionData( + SessionDetails( + SESSION_ID_INIT, + SESSION_ID_INIT, + 0, + fakeTimeProvider.currentTime().ms, + ), + fakeTimeProvider.currentTime(), + ), + IllegalArgumentException("Datastore init failed") + ) + val sharedSessionRepository = + SharedSessionRepositoryImpl( + sessionsSettings, + sessionGenerator, + sessionFirelogPublisher, + fakeTimeProvider, + fakeDataStore, + backgroundDispatcher = + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + ) + runCurrent() + fakeDataStore.close() + assertThat(sharedSessionRepository.localSessionData.sessionDetails.sessionId) + .isEqualTo(SESSION_ID_1) + } + + @Test + fun appForegroundSharedSessionRepo_updateSuccess() = runTest { + val sessionFirelogPublisher = + SessionFirelogPublisherImpl( + fakeFirebaseApp.firebaseApp, + firebaseInstallations, + sessionsSettings, + eventGDTLogger = fakeEventGDTLogger, + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + ) + val fakeDataStore = + FakeDataStore( + SessionData( + SessionDetails( + SESSION_ID_INIT, + SESSION_ID_INIT, + 0, + fakeTimeProvider.currentTime().ms, + ), + fakeTimeProvider.currentTime(), + ) + ) + val sharedSessionRepository = + SharedSessionRepositoryImpl( + sessionsSettings, + sessionGenerator, + sessionFirelogPublisher, + fakeTimeProvider, + fakeDataStore, + backgroundDispatcher = + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + ) + backgroundScope.launch { + fakeTimeProvider.addInterval(20.hours) + sharedSessionRepository.appForeground() + } + runCurrent() + assertThat(sharedSessionRepository.localSessionData.sessionDetails.sessionId) + .isEqualTo(SESSION_ID_1) + assertThat(sharedSessionRepository.previousNotificationType) + .isEqualTo(SharedSessionRepositoryImpl.NotificationType.GENERAL) + fakeDataStore.close() + } + + @Test + fun appForegroundSharedSessionRepo_updateFail() = runTest { + val sessionFirelogPublisher = + SessionFirelogPublisherImpl( + fakeFirebaseApp.firebaseApp, + firebaseInstallations, + sessionsSettings, + eventGDTLogger = fakeEventGDTLogger, + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + ) + val fakeDataStore = + FakeDataStore( + SessionData( + SessionDetails( + SESSION_ID_INIT, + SESSION_ID_INIT, + 0, + fakeTimeProvider.currentTime().ms, + ), + fakeTimeProvider.currentTime(), + ), + IllegalArgumentException("Datastore init failed") + ) + fakeDataStore.throwOnNextUpdateData(IllegalArgumentException("Datastore update failed")) + val sharedSessionRepository = + SharedSessionRepositoryImpl( + sessionsSettings, + sessionGenerator, + sessionFirelogPublisher, + fakeTimeProvider, + fakeDataStore, + backgroundDispatcher = + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + ) + + backgroundScope.launch { + fakeTimeProvider.addInterval(20.hours) + sharedSessionRepository.appForeground() + } + runCurrent() + // session_2 here because session_1 is failed when try to init datastore + assertThat(sharedSessionRepository.localSessionData.sessionDetails.sessionId) + .isEqualTo(SESSION_ID_2) + assertThat(sharedSessionRepository.previousNotificationType) + .isEqualTo(SharedSessionRepositoryImpl.NotificationType.FALLBACK) + fakeDataStore.close() + } + + companion object { + const val SESSION_ID_INIT = "12345678901234546677960" + } +}