Benoit Marty
4 months ago
committed by
GitHub
10 changed files with 427 additions and 24 deletions
@ -0,0 +1,286 @@
@@ -0,0 +1,286 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* 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. |
||||
*/ |
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class) |
||||
|
||||
package io.element.android.services.analytics.impl |
||||
|
||||
import com.google.common.truth.Truth.assertThat |
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent |
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen |
||||
import im.vector.app.features.analytics.plan.MobileScreen |
||||
import im.vector.app.features.analytics.plan.PollEnd |
||||
import im.vector.app.features.analytics.plan.UserProperties |
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver |
||||
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver |
||||
import io.element.android.services.analytics.impl.store.AnalyticsStore |
||||
import io.element.android.services.analytics.impl.store.FakeAnalyticsStore |
||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider |
||||
import io.element.android.services.analyticsproviders.test.FakeAnalyticsProvider |
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder |
||||
import io.element.android.tests.testutils.lambda.value |
||||
import io.element.android.tests.testutils.runCancellableScopeTest |
||||
import kotlinx.coroutines.CompletableDeferred |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.cancel |
||||
import kotlinx.coroutines.delay |
||||
import kotlinx.coroutines.flow.first |
||||
import kotlinx.coroutines.launch |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
class DefaultAnalyticsServiceTest { |
||||
@Test |
||||
fun `getAvailableAnalyticsProviders return the set of provider`() = runCancellableScopeTest { |
||||
val providers = setOf( |
||||
FakeAnalyticsProvider(name = "provider1", stopLambda = { }), |
||||
FakeAnalyticsProvider(name = "provider2", stopLambda = { }), |
||||
) |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsProviders = providers |
||||
) |
||||
val result = sut.getAvailableAnalyticsProviders() |
||||
assertThat(result).isEqualTo(providers) |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is not provided, capture is no op`() = runCancellableScopeTest { |
||||
val sut = createDefaultAnalyticsService(it) |
||||
sut.capture(anEvent) |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is provided, capture is sent to the AnalyticsProvider`() = runCancellableScopeTest { |
||||
val initLambda = lambdaRecorder<Unit> { } |
||||
val captureLambda = lambdaRecorder<VectorAnalyticsEvent, Unit> { _ -> } |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = FakeAnalyticsStore(defaultUserConsent = true), |
||||
analyticsProviders = setOf( |
||||
FakeAnalyticsProvider( |
||||
initLambda = initLambda, |
||||
captureLambda = captureLambda, |
||||
) |
||||
) |
||||
) |
||||
initLambda.assertions().isCalledOnce() |
||||
sut.capture(anEvent) |
||||
captureLambda.assertions().isCalledOnce().with(value(anEvent)) |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is not provided, screen is no op`() = runCancellableScopeTest { |
||||
val sut = createDefaultAnalyticsService(it) |
||||
sut.screen(aScreen) |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is provided, screen is sent to the AnalyticsProvider`() = runCancellableScopeTest { |
||||
val initLambda = lambdaRecorder<Unit> { } |
||||
val screenLambda = lambdaRecorder<VectorAnalyticsScreen, Unit> { _ -> } |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = FakeAnalyticsStore(defaultUserConsent = true), |
||||
analyticsProviders = setOf( |
||||
FakeAnalyticsProvider( |
||||
initLambda = initLambda, |
||||
screenLambda = screenLambda, |
||||
) |
||||
) |
||||
) |
||||
initLambda.assertions().isCalledOnce() |
||||
sut.screen(aScreen) |
||||
screenLambda.assertions().isCalledOnce().with(value(aScreen)) |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is not provided, trackError is no op`() = runCancellableScopeTest { |
||||
val sut = createDefaultAnalyticsService(it) |
||||
sut.trackError(anError) |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is provided, trackError is sent to the AnalyticsProvider`() = runCancellableScopeTest { |
||||
val initLambda = lambdaRecorder<Unit> { } |
||||
val trackErrorLambda = lambdaRecorder<Throwable, Unit> { _ -> } |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = FakeAnalyticsStore(defaultUserConsent = true), |
||||
analyticsProviders = setOf( |
||||
FakeAnalyticsProvider( |
||||
initLambda = initLambda, |
||||
trackErrorLambda = trackErrorLambda, |
||||
) |
||||
) |
||||
) |
||||
initLambda.assertions().isCalledOnce() |
||||
sut.trackError(anError) |
||||
trackErrorLambda.assertions().isCalledOnce().with(value(anError)) |
||||
} |
||||
|
||||
@Test |
||||
fun `setUserConsent is sent to the store`() = runCancellableScopeTest { |
||||
val store = FakeAnalyticsStore() |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = store, |
||||
) |
||||
assertThat(store.userConsentFlow.first()).isFalse() |
||||
assertThat(sut.getUserConsent().first()).isFalse() |
||||
sut.setUserConsent(true) |
||||
assertThat(store.userConsentFlow.first()).isTrue() |
||||
assertThat(sut.getUserConsent().first()).isTrue() |
||||
} |
||||
|
||||
@Test |
||||
fun `setAnalyticsId is sent to the store`() = runCancellableScopeTest { |
||||
val store = FakeAnalyticsStore() |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = store, |
||||
) |
||||
assertThat(store.analyticsIdFlow.first()).isEqualTo("") |
||||
assertThat(sut.getAnalyticsId().first()).isEqualTo("") |
||||
sut.setAnalyticsId(AN_ID) |
||||
assertThat(store.analyticsIdFlow.first()).isEqualTo(AN_ID) |
||||
assertThat(sut.getAnalyticsId().first()).isEqualTo(AN_ID) |
||||
} |
||||
|
||||
@Test |
||||
fun `setDidAskUserConsent is sent to the store`() = runCancellableScopeTest { |
||||
val store = FakeAnalyticsStore() |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = store, |
||||
) |
||||
assertThat(store.didAskUserConsentFlow.first()).isFalse() |
||||
assertThat(sut.didAskUserConsent().first()).isFalse() |
||||
sut.setDidAskUserConsent() |
||||
assertThat(store.didAskUserConsentFlow.first()).isTrue() |
||||
assertThat(sut.didAskUserConsent().first()).isTrue() |
||||
} |
||||
|
||||
@Test |
||||
fun `when a session is deleted, the store is reset`() = runCancellableScopeTest { |
||||
val resetLambda = lambdaRecorder<Unit> { } |
||||
val store = FakeAnalyticsStore( |
||||
resetLambda = resetLambda, |
||||
) |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = store, |
||||
) |
||||
sut.onSessionDeleted("userId") |
||||
resetLambda.assertions().isCalledOnce() |
||||
} |
||||
|
||||
@Test |
||||
fun `when reset is invoked, the user consent is reset`() = runCancellableScopeTest { |
||||
val store = FakeAnalyticsStore( |
||||
defaultDidAskUserConsent = true, |
||||
) |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsStore = store, |
||||
) |
||||
assertThat(store.didAskUserConsentFlow.first()).isTrue() |
||||
sut.reset() |
||||
assertThat(store.didAskUserConsentFlow.first()).isFalse() |
||||
} |
||||
|
||||
@Test |
||||
fun `when a session is added, nothing happen`() = runCancellableScopeTest { |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
) |
||||
sut.onSessionCreated("userId") |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is not provided, updateUserProperties is stored for future use`() = runTest { |
||||
val completable = CompletableDeferred<Unit>() |
||||
val updateUserPropertiesLambda = lambdaRecorder<UserProperties, Unit> { _ -> |
||||
completable.complete(Unit) |
||||
} |
||||
launch { |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = this, |
||||
analyticsProviders = setOf( |
||||
FakeAnalyticsProvider( |
||||
initLambda = { }, |
||||
stopLambda = { }, |
||||
updateUserPropertiesLambda = updateUserPropertiesLambda, |
||||
) |
||||
) |
||||
) |
||||
sut.updateUserProperties(aUserProperty) |
||||
updateUserPropertiesLambda.assertions().isNeverCalled() |
||||
// Give user consent |
||||
sut.setUserConsent(true) |
||||
completable.await() |
||||
updateUserPropertiesLambda.assertions().isCalledOnce().with(value(aUserProperty)) |
||||
cancel() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `when consent is provided, updateUserProperties is sent to the provider`() = runCancellableScopeTest { |
||||
val updateUserPropertiesLambda = lambdaRecorder<UserProperties, Unit> { _ -> } |
||||
val sut = createDefaultAnalyticsService( |
||||
coroutineScope = it, |
||||
analyticsProviders = setOf( |
||||
FakeAnalyticsProvider( |
||||
initLambda = { }, |
||||
updateUserPropertiesLambda = updateUserPropertiesLambda, |
||||
) |
||||
), |
||||
analyticsStore = FakeAnalyticsStore(defaultUserConsent = true), |
||||
) |
||||
sut.updateUserProperties(aUserProperty) |
||||
updateUserPropertiesLambda.assertions().isCalledOnce().with(value(aUserProperty)) |
||||
} |
||||
|
||||
private suspend fun createDefaultAnalyticsService( |
||||
coroutineScope: CoroutineScope, |
||||
analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider> = setOf( |
||||
FakeAnalyticsProvider( |
||||
stopLambda = { }, |
||||
) |
||||
), |
||||
analyticsStore: AnalyticsStore = FakeAnalyticsStore(), |
||||
sessionObserver: SessionObserver = NoOpSessionObserver(), |
||||
) = DefaultAnalyticsService( |
||||
analyticsProviders = analyticsProviders, |
||||
analyticsStore = analyticsStore, |
||||
coroutineScope = coroutineScope, |
||||
sessionObserver = sessionObserver, |
||||
).also { |
||||
// Wait for the service to be ready |
||||
delay(1) |
||||
} |
||||
|
||||
private companion object { |
||||
private val anEvent = PollEnd() |
||||
private val aScreen = MobileScreen(screenName = MobileScreen.ScreenName.User) |
||||
private val aUserProperty = UserProperties( |
||||
ftueUseCaseSelection = UserProperties.FtueUseCaseSelection.WorkMessaging, |
||||
) |
||||
private val anError = Exception("a reason") |
||||
private const val AN_ID = "anId" |
||||
} |
||||
} |
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* 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 io.element.android.services.analytics.impl.store |
||||
|
||||
import io.element.android.tests.testutils.lambda.lambdaError |
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
|
||||
class FakeAnalyticsStore( |
||||
defaultUserConsent: Boolean = false, |
||||
defaultDidAskUserConsent: Boolean = false, |
||||
defaultAnalyticsId: String = "", |
||||
private val resetLambda: () -> Unit = { lambdaError() }, |
||||
) : AnalyticsStore { |
||||
override val userConsentFlow = MutableStateFlow(defaultUserConsent) |
||||
override val didAskUserConsentFlow = MutableStateFlow(defaultDidAskUserConsent) |
||||
override val analyticsIdFlow = MutableStateFlow(defaultAnalyticsId) |
||||
|
||||
override suspend fun setUserConsent(newUserConsent: Boolean) { |
||||
userConsentFlow.emit(newUserConsent) |
||||
} |
||||
|
||||
override suspend fun setDidAskUserConsent(newValue: Boolean) { |
||||
didAskUserConsentFlow.emit(newValue) |
||||
} |
||||
|
||||
override suspend fun setAnalyticsId(newAnalyticsId: String) { |
||||
analyticsIdFlow.emit(newAnalyticsId) |
||||
} |
||||
|
||||
override suspend fun reset() { |
||||
resetLambda() |
||||
} |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* 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. |
||||
*/ |
||||
plugins { |
||||
id("io.element.android-library") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.services.analyticsproviders.test" |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(projects.services.analyticsproviders.api) |
||||
implementation(projects.tests.testutils) |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* 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 io.element.android.services.analyticsproviders.test |
||||
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent |
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen |
||||
import im.vector.app.features.analytics.plan.UserProperties |
||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider |
||||
import io.element.android.tests.testutils.lambda.lambdaError |
||||
|
||||
class FakeAnalyticsProvider( |
||||
override val name: String = "FakeAnalyticsProvider", |
||||
private val initLambda: () -> Unit = { lambdaError() }, |
||||
private val stopLambda: () -> Unit = { lambdaError() }, |
||||
private val captureLambda: (VectorAnalyticsEvent) -> Unit = { lambdaError() }, |
||||
private val screenLambda: (VectorAnalyticsScreen) -> Unit = { lambdaError() }, |
||||
private val updateUserPropertiesLambda: (UserProperties) -> Unit = { lambdaError() }, |
||||
private val trackErrorLambda: (Throwable) -> Unit = { lambdaError() } |
||||
) : AnalyticsProvider { |
||||
override fun init() = initLambda() |
||||
override fun stop() = stopLambda() |
||||
override fun capture(event: VectorAnalyticsEvent) = captureLambda(event) |
||||
override fun screen(screen: VectorAnalyticsScreen) = screenLambda(screen) |
||||
override fun updateUserProperties(userProperties: UserProperties) = updateUserPropertiesLambda(userProperties) |
||||
override fun trackError(throwable: Throwable) = trackErrorLambda(throwable) |
||||
} |
Loading…
Reference in new issue