chore(feature-flag): Group all call to WorkManager in FeatureFlagWorkerManager.

Extract interface from FeatureFlagWorkerManager.
This commit is contained in:
Nicolas Mouchel 2024-03-26 09:25:58 +01:00 committed by MargeBot
parent 884cfa467f
commit 49a40b1f15
17 changed files with 169 additions and 212 deletions

View File

@ -14,6 +14,7 @@ public abstract interface class me/proton/core/featureflag/dagger/CoreFeatureFla
public abstract fun bindFeatureFlagRemoteDataSource (Lme/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl;)Lme/proton/core/featureflag/domain/repository/FeatureFlagRemoteDataSource;
public abstract fun bindManager (Lme/proton/core/featureflag/data/FeatureFlagManagerImpl;)Lme/proton/core/featureflag/domain/FeatureFlagManager;
public abstract fun bindRepository (Lme/proton/core/featureflag/data/repository/FeatureFlagRepositoryImpl;)Lme/proton/core/featureflag/domain/repository/FeatureFlagRepository;
public abstract fun bindWorkerManager (Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManagerImpl;)Lme/proton/core/featureflag/domain/FeatureFlagWorkerManager;
public abstract fun optionalFeatureFlagContextProvider ()Lme/proton/core/featureflag/domain/repository/FeatureFlagContextProvider;
}

View File

@ -26,8 +26,10 @@ import dagger.hilt.components.SingletonComponent
import me.proton.core.featureflag.data.FeatureFlagManagerImpl
import me.proton.core.featureflag.data.local.FeatureFlagLocalDataSourceImpl
import me.proton.core.featureflag.data.remote.FeatureFlagRemoteDataSourceImpl
import me.proton.core.featureflag.data.remote.worker.FeatureFlagWorkerManagerImpl
import me.proton.core.featureflag.data.repository.FeatureFlagRepositoryImpl
import me.proton.core.featureflag.domain.FeatureFlagManager
import me.proton.core.featureflag.domain.FeatureFlagWorkerManager
import me.proton.core.featureflag.domain.repository.FeatureFlagContextProvider
import me.proton.core.featureflag.domain.repository.FeatureFlagLocalDataSource
import me.proton.core.featureflag.domain.repository.FeatureFlagRemoteDataSource
@ -54,6 +56,10 @@ public interface CoreFeatureFlagModule {
@Singleton
public fun bindManager(featureFlagManagerImpl: FeatureFlagManagerImpl): FeatureFlagManager
@Binds
@Singleton
public fun bindWorkerManager(featureFlagWorkerManagerImpl: FeatureFlagWorkerManagerImpl): FeatureFlagWorkerManager
@BindsOptionalOf
public fun optionalFeatureFlagContextProvider(): FeatureFlagContextProvider
}

View File

@ -39,7 +39,7 @@ public final class me/proton/core/featureflag/data/FeatureFlagManagerImpl_Factor
}
public final class me/proton/core/featureflag/data/FeatureFlagRefreshStarter {
public fun <init> (Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager;Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)V
public fun <init> (Lme/proton/core/featureflag/domain/FeatureFlagWorkerManager;Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)V
public final fun start (Z)V
public static synthetic fun start$default (Lme/proton/core/featureflag/data/FeatureFlagRefreshStarter;ZILjava/lang/Object;)V
}
@ -49,7 +49,7 @@ public final class me/proton/core/featureflag/data/FeatureFlagRefreshStarter_Fac
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/featureflag/data/FeatureFlagRefreshStarter_Factory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/featureflag/data/FeatureFlagRefreshStarter;
public static fun newInstance (Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager;Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)Lme/proton/core/featureflag/data/FeatureFlagRefreshStarter;
public static fun newInstance (Lme/proton/core/featureflag/domain/FeatureFlagWorkerManager;Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)Lme/proton/core/featureflag/data/FeatureFlagRefreshStarter;
}
public abstract class me/proton/core/featureflag/data/IsFeatureFlagEnabledImpl : me/proton/core/featureflag/domain/IsFeatureFlagEnabled {
@ -114,19 +114,18 @@ public final class me/proton/core/featureflag/data/local/FeatureFlagLocalDataSou
}
public final class me/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl : me/proton/core/featureflag/domain/repository/FeatureFlagRemoteDataSource {
public fun <init> (Lme/proton/core/network/data/ApiProvider;Landroidx/work/WorkManager;Ljava/util/Optional;)V
public fun <init> (Lme/proton/core/network/data/ApiProvider;Ljava/util/Optional;)V
public fun get (Lme/proton/core/domain/entity/UserId;Ljava/util/Set;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getAll (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun prefetch (Lme/proton/core/domain/entity/UserId;Ljava/util/Set;)V
public fun update (Lme/proton/core/featureflag/domain/entity/FeatureFlag;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun update (Lme/proton/core/domain/entity/UserId;Lme/proton/core/featureflag/domain/entity/FeatureId;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class me/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl_Factory : dagger/internal/Factory {
public fun <init> (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl_Factory;
public fun <init> (Ljavax/inject/Provider;Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl_Factory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl;
public static fun newInstance (Lme/proton/core/network/data/ApiProvider;Landroidx/work/WorkManager;Ljava/util/Optional;)Lme/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl;
public static fun newInstance (Lme/proton/core/network/data/ApiProvider;Ljava/util/Optional;)Lme/proton/core/featureflag/data/remote/FeatureFlagRemoteDataSourceImpl;
}
public final class me/proton/core/featureflag/data/remote/request/PutFeatureFlagBody {
@ -275,19 +274,21 @@ public final class me/proton/core/featureflag/data/remote/response/GetUnleashTog
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager {
public final class me/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManagerImpl : me/proton/core/featureflag/domain/FeatureFlagWorkerManager {
public fun <init> (Landroid/content/Context;Landroidx/work/WorkManager;)V
public final fun cancel (Lme/proton/core/domain/entity/UserId;)V
public final fun enqueueOneTime (Lme/proton/core/domain/entity/UserId;)V
public final fun enqueuePeriodic (Lme/proton/core/domain/entity/UserId;Z)V
public fun cancel (Lme/proton/core/domain/entity/UserId;)V
public fun enqueueOneTime (Lme/proton/core/domain/entity/UserId;)V
public fun enqueuePeriodic (Lme/proton/core/domain/entity/UserId;Z)V
public fun prefetch (Lme/proton/core/domain/entity/UserId;Ljava/util/Set;)V
public fun update (Lme/proton/core/featureflag/domain/entity/FeatureFlag;)V
}
public final class me/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager_Factory : dagger/internal/Factory {
public final class me/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManagerImpl_Factory : dagger/internal/Factory {
public fun <init> (Ljavax/inject/Provider;Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager_Factory;
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManagerImpl_Factory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager;
public static fun newInstance (Landroid/content/Context;Landroidx/work/WorkManager;)Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager;
public fun get ()Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManagerImpl;
public static fun newInstance (Landroid/content/Context;Landroidx/work/WorkManager;)Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManagerImpl;
}
public abstract interface class me/proton/core/featureflag/data/remote/worker/FetchFeatureIdsWorker_AssistedFactory : androidx/hilt/work/WorkerAssistedFactory {
@ -343,7 +344,7 @@ public final class me/proton/core/featureflag/data/remote/worker/UpdateFeatureFl
public fun <init> (Ljavax/inject/Provider;Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/featureflag/data/remote/worker/UpdateFeatureFlagWorker_Factory;
public fun get (Landroid/content/Context;Landroidx/work/WorkerParameters;)Lme/proton/core/featureflag/data/remote/worker/UpdateFeatureFlagWorker;
public static fun newInstance (Landroid/content/Context;Landroidx/work/WorkerParameters;Lme/proton/core/network/data/ApiProvider;Lme/proton/core/featureflag/domain/repository/FeatureFlagLocalDataSource;)Lme/proton/core/featureflag/data/remote/worker/UpdateFeatureFlagWorker;
public static fun newInstance (Landroid/content/Context;Landroidx/work/WorkerParameters;Lme/proton/core/featureflag/domain/repository/FeatureFlagRemoteDataSource;Lme/proton/core/featureflag/domain/repository/FeatureFlagLocalDataSource;)Lme/proton/core/featureflag/data/remote/worker/UpdateFeatureFlagWorker;
}
public abstract interface class me/proton/core/featureflag/data/remote/worker/UpdateFeatureFlagWorker_HiltModule {
@ -374,6 +375,6 @@ public final class me/proton/core/featureflag/data/repository/FeatureFlagReposit
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/featureflag/data/repository/FeatureFlagRepositoryImpl_Factory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/featureflag/data/repository/FeatureFlagRepositoryImpl;
public static fun newInstance (Lme/proton/core/featureflag/domain/repository/FeatureFlagLocalDataSource;Lme/proton/core/featureflag/domain/repository/FeatureFlagRemoteDataSource;Lme/proton/core/featureflag/data/remote/worker/FeatureFlagWorkerManager;Lme/proton/core/observability/domain/ObservabilityManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)Lme/proton/core/featureflag/data/repository/FeatureFlagRepositoryImpl;
public static fun newInstance (Lme/proton/core/featureflag/domain/repository/FeatureFlagLocalDataSource;Lme/proton/core/featureflag/domain/repository/FeatureFlagRemoteDataSource;Lme/proton/core/featureflag/domain/FeatureFlagWorkerManager;Lme/proton/core/observability/domain/ObservabilityManager;Lme/proton/core/util/kotlin/CoroutineScopeProvider;)Lme/proton/core/featureflag/data/repository/FeatureFlagRepositoryImpl;
}

View File

@ -26,7 +26,7 @@ plugins {
protonCoverage {
branchCoveragePercentage.set(50)
lineCoveragePercentage.set(83)
lineCoveragePercentage.set(82)
}
protonDagger {

View File

@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.core.account.domain.entity.AccountState
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.featureflag.data.remote.worker.FeatureFlagWorkerManager
import me.proton.core.featureflag.domain.FeatureFlagWorkerManager
import me.proton.core.util.kotlin.CoroutineScopeProvider
import javax.inject.Inject
import javax.inject.Singleton

View File

@ -18,13 +18,10 @@
package me.proton.core.featureflag.data.remote
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkManager
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.data.remote.request.PutFeatureFlagBody
import me.proton.core.featureflag.data.remote.response.toFeatureFlag
import me.proton.core.featureflag.domain.LogTag
import me.proton.core.featureflag.data.remote.worker.FetchFeatureIdsWorker
import me.proton.core.featureflag.data.remote.worker.UpdateFeatureFlagWorker
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.featureflag.domain.entity.FeatureId
import me.proton.core.featureflag.domain.repository.FeatureFlagContextProvider
@ -37,7 +34,6 @@ import kotlin.jvm.optionals.getOrNull
public class FeatureFlagRemoteDataSourceImpl @Inject constructor(
private val apiProvider: ApiProvider,
private val workManager: WorkManager,
private val featureFlagContextProvider: Optional<FeatureFlagContextProvider>,
) : FeatureFlagRemoteDataSource {
@ -62,20 +58,9 @@ public class FeatureFlagRemoteDataSourceImpl @Inject constructor(
}
}.valueOrThrow
override suspend fun update(featureFlag: FeatureFlag) {
val request = UpdateFeatureFlagWorker.getRequest(
featureFlag.userId,
featureFlag.featureId,
featureFlag.value
)
workManager.enqueue(request)
}
override fun prefetch(userId: UserId?, featureIds: Set<FeatureId>) {
workManager.enqueueUniqueWork(
FetchFeatureIdsWorker.getUniqueWorkName(userId),
ExistingWorkPolicy.REPLACE,
FetchFeatureIdsWorker.getRequest(userId, featureIds),
)
override suspend fun update(userId: UserId?, featureId: FeatureId, enabled: Boolean) {
apiProvider.get<FeaturesApi>(userId).invoke {
putFeatureFlag(featureId.id, PutFeatureFlagBody(enabled))
}.valueOrThrow
}
}

View File

@ -7,18 +7,21 @@ import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.data.R
import me.proton.core.featureflag.domain.FeatureFlagWorkerManager
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.featureflag.domain.entity.FeatureId
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
public class FeatureFlagWorkerManager @Inject constructor(
public class FeatureFlagWorkerManagerImpl @Inject constructor(
@ApplicationContext
private val context: Context,
private val workManager: WorkManager
) {
) : FeatureFlagWorkerManager {
public fun enqueueOneTime(userId: UserId?) {
override fun enqueueOneTime(userId: UserId?) {
workManager.enqueueUniqueWork(
FetchUnleashTogglesWorker.getOneTimeUniqueWorkName(userId),
ExistingWorkPolicy.REPLACE,
@ -26,7 +29,7 @@ public class FeatureFlagWorkerManager @Inject constructor(
)
}
public fun enqueuePeriodic(userId: UserId?, immediately: Boolean) {
override fun enqueuePeriodic(userId: UserId?, immediately: Boolean) {
val repeatInterval = when (userId) {
null -> getRepeatIntervalBackgroundUnauth()
else -> getRepeatIntervalBackgroundAuth()
@ -38,11 +41,29 @@ public class FeatureFlagWorkerManager @Inject constructor(
)
}
public fun cancel(userId: UserId?) {
override fun cancel(userId: UserId?) {
workManager.cancelUniqueWork(FetchUnleashTogglesWorker.getPeriodicUniqueWorkName(userId))
workManager.cancelUniqueWork(FetchUnleashTogglesWorker.getOneTimeUniqueWorkName(userId))
}
override fun update(featureFlag: FeatureFlag) {
val request = UpdateFeatureFlagWorker.getRequest(
featureFlag.userId,
featureFlag.featureId,
featureFlag.value
)
workManager.enqueue(request)
}
override fun prefetch(userId: UserId?, featureIds: Set<FeatureId>) {
workManager.enqueueUniqueWork(
FetchFeatureIdsWorker.getUniqueWorkName(userId),
ExistingWorkPolicy.REPLACE,
FetchFeatureIdsWorker.getRequest(userId, featureIds),
)
}
private fun getRepeatIntervalBackgroundAuth(): Duration = context.resources.getInteger(
R.integer.core_feature_feature_flag_worker_repeat_interval_auth_seconds
).toDuration(DurationUnit.SECONDS)

View File

@ -32,38 +32,34 @@ import androidx.work.workDataOf
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.data.remote.FeaturesApi
import me.proton.core.featureflag.data.remote.request.PutFeatureFlagBody
import me.proton.core.featureflag.domain.entity.FeatureId
import me.proton.core.featureflag.domain.repository.FeatureFlagLocalDataSource
import me.proton.core.network.data.ApiProvider
import me.proton.core.network.domain.ApiResult
import me.proton.core.featureflag.domain.repository.FeatureFlagRemoteDataSource
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.isRetryable
@HiltWorker
internal class UpdateFeatureFlagWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val apiProvider: ApiProvider,
private val remoteDataSource: FeatureFlagRemoteDataSource,
private val localDataSource: FeatureFlagLocalDataSource
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val userId = inputData.getString(INPUT_USER_ID)?.let { UserId(it) }
val featureId = inputData.getString(INPUT_FEATURE_ID) ?: return Result.failure()
val featureId = inputData.getString(INPUT_FEATURE_ID)?.let(::FeatureId) ?: return Result.failure()
val isEnabled = inputData.getBoolean(INPUT_FEATURE_VALUE, false)
val apiManager = apiProvider.get<FeaturesApi>(userId)
val body = PutFeatureFlagBody(isEnabled)
return when (val result = apiManager { putFeatureFlag(featureId, body) }) {
is ApiResult.Success -> Result.success()
is ApiResult.Error -> {
if (result.isRetryable()) {
Result.retry()
} else {
rollbackLocalFeatureFlag(userId, FeatureId(featureId), isEnabled)
Result.failure()
}
return runCatching {
remoteDataSource.update(userId, featureId, isEnabled)
Result.success()
}.getOrElse { error ->
if (error is ApiException && error.isRetryable()) {
Result.retry()
} else {
rollbackLocalFeatureFlag(userId, featureId, isEnabled)
Result.failure()
}
}
}

View File

@ -32,7 +32,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import me.proton.core.data.arch.buildProtonStore
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.data.remote.worker.FeatureFlagWorkerManager
import me.proton.core.featureflag.domain.FeatureFlagWorkerManager
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.featureflag.domain.entity.FeatureId
import me.proton.core.featureflag.domain.entity.Scope
@ -154,11 +154,11 @@ public class FeatureFlagRepositoryImpl @Inject internal constructor(
get(userId, setOf(featureId), refresh).firstOrNull()
override fun prefetch(userId: UserId?, featureIds: Set<FeatureId>) {
remoteDataSource.prefetch(userId, featureIds)
workerManager.prefetch(userId, featureIds)
}
override suspend fun update(featureFlag: FeatureFlag) {
localDataSource.upsert(listOf(featureFlag))
remoteDataSource.update(featureFlag)
workerManager.update(featureFlag)
}
}

View File

@ -27,8 +27,7 @@ import me.proton.core.account.domain.entity.Account
import me.proton.core.account.domain.entity.AccountState
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.data.remote.worker.FeatureFlagWorkerManager
import me.proton.core.featureflag.domain.ExperimentalProtonFeatureFlag
import me.proton.core.featureflag.domain.FeatureFlagWorkerManager
import me.proton.core.test.kotlin.UnconfinedTestCoroutineScopeProvider
import org.junit.Before
import org.junit.Test

View File

@ -1,82 +0,0 @@
/*
* Copyright (c) 2022 Proton AG
*
* This file is part of Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see https://www.gnu.org/licenses/.
*/
package me.proton.core.featureflag.data.remote
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import io.mockk.coEvery
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import me.proton.core.featureflag.data.remote.worker.FetchFeatureIdsWorker
import me.proton.core.featureflag.data.remote.worker.UpdateFeatureFlagWorker
import me.proton.core.featureflag.data.repository.TestFeatureFlagContextProvider
import me.proton.core.featureflag.data.testdata.FeatureFlagTestData
import me.proton.core.featureflag.data.testdata.UserIdTestData
import me.proton.core.featureflag.domain.repository.FeatureFlagContextProvider
import me.proton.core.network.data.ApiProvider
import org.junit.Test
import java.util.Optional
import kotlin.test.assertEquals
class FeatureFlagRemoteDataSourceImplTest {
private val workManager: WorkManager = mockk {
coEvery { enqueue(any<OneTimeWorkRequest>()) } returns mockk()
coEvery { enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) } returns mockk()
}
private val apiProvider: ApiProvider = mockk()
private val remoteDataSource = FeatureFlagRemoteDataSourceImpl(apiProvider, workManager, Optional.empty())
@Test
fun `update enqueues worker to update on remote`() = runTest {
// given
val featureFlag = FeatureFlagTestData.disabledFeature
// when
remoteDataSource.update(featureFlag)
// then
val requestSlot = slot<OneTimeWorkRequest>()
verify { workManager.enqueue(capture(requestSlot)) }
val workSpec = requestSlot.captured.workSpec
assertEquals(UpdateFeatureFlagWorker::class.qualifiedName, workSpec.workerClassName)
}
@Test
fun `prefetch enqueues worker to prefetch on remote`() = runTest {
// given
val featureIds = setOf(FeatureFlagTestData.featureId, FeatureFlagTestData.featureId1)
val userId = UserIdTestData.userId
// when
remoteDataSource.prefetch(userId, featureIds)
// then
val requestSlot = slot<OneTimeWorkRequest>()
val expectedName = FetchFeatureIdsWorker.getUniqueWorkName(userId)
verify { workManager.enqueueUniqueWork(expectedName, ExistingWorkPolicy.REPLACE, capture(requestSlot)) }
val workSpec = requestSlot.captured.workSpec
assertEquals(FetchFeatureIdsWorker::class.qualifiedName, workSpec.workerClassName)
}
}

View File

@ -26,12 +26,15 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import me.proton.core.featureflag.data.testdata.FeatureFlagTestData
import me.proton.core.featureflag.data.testdata.UserIdTestData
import org.junit.Test
import kotlin.test.assertEquals
class FeatureFlagWorkerManagerTest {
class FeatureFlagWorkerManagerImplTest {
private val userId = UserIdTestData.userId
@ -42,7 +45,7 @@ class FeatureFlagWorkerManagerTest {
}
private val workManager = mockk<WorkManager>(relaxed = true)
private fun mockManager() = FeatureFlagWorkerManager(context, workManager)
private fun mockManager() = FeatureFlagWorkerManagerImpl(context, workManager)
@Test
fun enqueueOneTime() = runTest {
@ -88,4 +91,36 @@ class FeatureFlagWorkerManagerTest {
)
}
}
@Test
fun `update enqueues worker to update on remote`() = runTest {
// given
val featureFlag = FeatureFlagTestData.disabledFeature
// when
mockManager().update(featureFlag)
// then
val requestSlot = slot<OneTimeWorkRequest>()
verify { workManager.enqueue(capture(requestSlot)) }
val workSpec = requestSlot.captured.workSpec
assertEquals(UpdateFeatureFlagWorker::class.qualifiedName, workSpec.workerClassName)
}
@Test
fun `prefetch enqueues worker to prefetch on remote`() = runTest {
// given
val featureIds = setOf(FeatureFlagTestData.featureId, FeatureFlagTestData.featureId1)
val userId = UserIdTestData.userId
// when
mockManager().prefetch(userId, featureIds)
// then
val requestSlot = slot<OneTimeWorkRequest>()
val expectedName = FetchFeatureIdsWorker.getUniqueWorkName(userId)
verify { workManager.enqueueUniqueWork(expectedName, ExistingWorkPolicy.REPLACE, capture(requestSlot)) }
val workSpec = requestSlot.captured.workSpec
assertEquals(FetchFeatureIdsWorker::class.qualifiedName, workSpec.workerClassName)
}
}

View File

@ -27,27 +27,22 @@ import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.SerializationException
import me.proton.core.featureflag.data.remote.FeaturesApi
import me.proton.core.featureflag.data.remote.request.PutFeatureFlagBody
import me.proton.core.featureflag.data.remote.response.PutFeatureResponse
import me.proton.core.featureflag.data.testdata.UserIdTestData
import me.proton.core.featureflag.domain.entity.FeatureId
import me.proton.core.featureflag.domain.repository.FeatureFlagLocalDataSource
import me.proton.core.network.data.ApiProvider
import me.proton.core.featureflag.domain.repository.FeatureFlagRemoteDataSource
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.ApiResult
import me.proton.core.test.kotlin.CoroutinesTest
import me.proton.core.test.kotlin.UnconfinedCoroutinesTest
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.UnknownHostException
class UpdateFeatureFlagWorkerTest : CoroutinesTest by UnconfinedCoroutinesTest() {
private val userId = UserIdTestData.userId
private val featureId = FeatureId("feature-flag-mail")
private val featureFlagValue = true
private val nonRetryableException = SerializationException()
private val retryableException = UnknownHostException()
private val context = mockk<Context>()
private val parameters = mockk<WorkerParameters> {
@ -56,27 +51,13 @@ class UpdateFeatureFlagWorkerTest : CoroutinesTest by UnconfinedCoroutinesTest()
every { inputData.getBoolean(UpdateFeatureFlagWorker.INPUT_FEATURE_VALUE, false) } returns featureFlagValue
every { this@mockk.taskExecutor } returns mockk(relaxed = true)
}
private val featuresApi: FeaturesApi = mockk()
private val apiProvider: ApiProvider = mockk {
coEvery { get<FeaturesApi>(userId).invoke<PutFeatureResponse>(block = any()) } coAnswers {
val block = firstArg<suspend FeaturesApi.() -> PutFeatureResponse>()
try {
ApiResult.Success(block(featuresApi))
} catch (e: Exception) {
when (e) {
nonRetryableException -> ApiResult.Error.Parse(e)
retryableException -> ApiResult.Error.Connection()
else -> throw e
}
}
}
}
private val remoteDataSource: FeatureFlagRemoteDataSource = mockk()
private val localDataSource: FeatureFlagLocalDataSource = mockk(relaxUnitFun = true)
private val worker = UpdateFeatureFlagWorker(
context,
parameters,
apiProvider,
remoteDataSource,
localDataSource
)
@ -113,7 +94,7 @@ class UpdateFeatureFlagWorkerTest : CoroutinesTest by UnconfinedCoroutinesTest()
@Test
fun `worker returns success when API calls succeeds`() = runTest {
// given
coEvery { featuresApi.putFeatureFlag(featureId.id, PutFeatureFlagBody(featureFlagValue)) } returns mockk()
coEvery { remoteDataSource.update(userId, featureId, featureFlagValue) } returns mockk()
// when
val actual = worker.doWork()
@ -126,11 +107,8 @@ class UpdateFeatureFlagWorkerTest : CoroutinesTest by UnconfinedCoroutinesTest()
fun `worker returns retry when API calls fails with retryable exception`() = runTest {
// given
coEvery {
featuresApi.putFeatureFlag(
featureId.id,
PutFeatureFlagBody(featureFlagValue)
)
} throws retryableException
remoteDataSource.update(userId, featureId, featureFlagValue)
} throws ApiException(ApiResult.Error.NoInternet())
// when
val actual = worker.doWork()
@ -143,33 +121,14 @@ class UpdateFeatureFlagWorkerTest : CoroutinesTest by UnconfinedCoroutinesTest()
fun `worker returns failure when API calls fails with non retryable exception`() = runTest {
// given
coEvery {
featuresApi.putFeatureFlag(
featureId.id,
PutFeatureFlagBody(featureFlagValue)
)
} throws nonRetryableException
remoteDataSource.update(userId, featureId, featureFlagValue)
} throws ApiException(ApiResult.Error.Parse(SerializationException()))
// when
val actual = worker.doWork()
// then
coVerify { localDataSource.updateValue(userId, featureId, featureFlagValue.not()) }
assertEquals(androidx.work.ListenableWorker.Result.failure(), actual)
}
@Test
fun `worker rollbacks local feature flag value when API calls fails with non retryable exception`() = runTest {
// given
coEvery {
featuresApi.putFeatureFlag(
featureId.id,
PutFeatureFlagBody(featureFlagValue)
)
} throws nonRetryableException
// when
worker.doWork()
// then
coVerify { localDataSource.updateValue(userId, featureId, featureFlagValue.not()) }
}
}
}

View File

@ -47,7 +47,6 @@ import me.proton.core.featureflag.data.remote.FeatureFlagRemoteDataSourceImpl
import me.proton.core.featureflag.data.remote.FeaturesApi
import me.proton.core.featureflag.data.remote.response.GetFeaturesResponse
import me.proton.core.featureflag.data.remote.response.GetUnleashTogglesResponse
import me.proton.core.featureflag.data.remote.worker.FeatureFlagWorkerManager
import me.proton.core.featureflag.data.testdata.FeatureFlagTestData
import me.proton.core.featureflag.data.testdata.FeatureFlagTestData.disabledFeature
import me.proton.core.featureflag.data.testdata.FeatureFlagTestData.disabledFeatureApiResponse
@ -59,6 +58,7 @@ import me.proton.core.featureflag.data.testdata.FeatureFlagTestData.featureId
import me.proton.core.featureflag.data.testdata.FeatureFlagTestData.featureId1
import me.proton.core.featureflag.data.testdata.SessionIdTestData
import me.proton.core.featureflag.data.testdata.UserIdTestData.userId
import me.proton.core.featureflag.domain.FeatureFlagWorkerManager
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.featureflag.domain.entity.FeatureId
import me.proton.core.featureflag.domain.entity.Scope
@ -115,7 +115,6 @@ class FeatureFlagRepositoryImplTest : CoroutinesTest by UnconfinedCoroutinesTest
)
} returns TestApiManager(featuresApi)
}
private val workManager = mockk<WorkManager>(relaxed = true)
private val workerManager = mockk<FeatureFlagWorkerManager>(relaxed = true)
private val observabilityManager = mockk<ObservabilityManager>(relaxed = true)
@ -131,7 +130,7 @@ class FeatureFlagRepositoryImplTest : CoroutinesTest by UnconfinedCoroutinesTest
apiProvider = ApiProvider(apiManagerFactory, sessionProvider, coroutinesRule.dispatchers)
featureFlagContextProvider = TestFeatureFlagContextProvider()
local = spyk(FeatureFlagLocalDataSourceImpl(database))
remote = spyk(FeatureFlagRemoteDataSourceImpl(apiProvider, workManager, Optional.of(featureFlagContextProvider)))
remote = spyk(FeatureFlagRemoteDataSourceImpl(apiProvider, Optional.of(featureFlagContextProvider)))
repository = FeatureFlagRepositoryImpl(
localDataSource = local,
remoteDataSource = remote,
@ -372,7 +371,7 @@ class FeatureFlagRepositoryImplTest : CoroutinesTest by UnconfinedCoroutinesTest
val featureIds = setOf(featureId, featureId1)
repository.prefetch(userId, featureIds)
coVerify { remote.prefetch(userId, featureIds) }
coVerify { workerManager.prefetch(userId, featureIds) }
}
@Test
@ -385,7 +384,7 @@ class FeatureFlagRepositoryImplTest : CoroutinesTest by UnconfinedCoroutinesTest
// Then
coVerify { local.upsert(listOf(featureFlag)) }
coVerify { remote.update(featureFlag) }
coVerify { workerManager.update(featureFlag) }
}
@Test

View File

@ -25,6 +25,14 @@ public final class me/proton/core/featureflag/domain/FeatureFlagManager$DefaultI
public static synthetic fun refreshAll$default (Lme/proton/core/featureflag/domain/FeatureFlagManager;Lme/proton/core/domain/entity/UserId;ILjava/lang/Object;)V
}
public abstract interface class me/proton/core/featureflag/domain/FeatureFlagWorkerManager {
public abstract fun cancel (Lme/proton/core/domain/entity/UserId;)V
public abstract fun enqueueOneTime (Lme/proton/core/domain/entity/UserId;)V
public abstract fun enqueuePeriodic (Lme/proton/core/domain/entity/UserId;Z)V
public abstract fun prefetch (Lme/proton/core/domain/entity/UserId;Ljava/util/Set;)V
public abstract fun update (Lme/proton/core/featureflag/domain/entity/FeatureFlag;)V
}
public abstract interface class me/proton/core/featureflag/domain/IsFeatureFlagEnabled {
public abstract fun invoke (Lme/proton/core/domain/entity/UserId;)Z
public abstract fun isLocalEnabled ()Z
@ -99,8 +107,7 @@ public abstract interface class me/proton/core/featureflag/domain/repository/Fea
public abstract interface class me/proton/core/featureflag/domain/repository/FeatureFlagRemoteDataSource {
public abstract fun get (Lme/proton/core/domain/entity/UserId;Ljava/util/Set;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getAll (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun prefetch (Lme/proton/core/domain/entity/UserId;Ljava/util/Set;)V
public abstract fun update (Lme/proton/core/featureflag/domain/entity/FeatureFlag;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun update (Lme/proton/core/domain/entity/UserId;Lme/proton/core/featureflag/domain/entity/FeatureId;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class me/proton/core/featureflag/domain/repository/FeatureFlagRepository {

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.featureflag.domain
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.domain.entity.FeatureFlag
import me.proton.core.featureflag.domain.entity.FeatureId
public interface FeatureFlagWorkerManager {
public fun enqueueOneTime(userId: UserId?)
public fun enqueuePeriodic(userId: UserId?, immediately: Boolean)
public fun cancel(userId: UserId?)
public fun update(featureFlag: FeatureFlag)
public fun prefetch(userId: UserId?, featureIds: Set<FeatureId>)
}

View File

@ -25,6 +25,5 @@ import me.proton.core.featureflag.domain.entity.FeatureId
public interface FeatureFlagRemoteDataSource {
public suspend fun getAll(userId: UserId?): List<FeatureFlag>
public suspend fun get(userId: UserId?, ids: Set<FeatureId>): List<FeatureFlag>
public suspend fun update(featureFlag: FeatureFlag)
public fun prefetch(userId: UserId?, featureIds: Set<FeatureId>)
public suspend fun update(userId: UserId?, featureId: FeatureId, enabled: Boolean)
}