mirror of
https://github.com/bitwarden/android.git
synced 2024-11-28 22:08:49 +03:00
BIT-2418: Add the OrganizationEventManager (#3330)
This commit is contained in:
parent
170db5077d
commit
16fce43739
8 changed files with 393 additions and 0 deletions
|
@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -24,4 +25,7 @@ class BitwardenApplication : Application() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var authRequestNotificationManager: AuthRequestNotificationManager
|
lateinit var authRequestNotificationManager: AuthRequestNotificationManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var organizationEventManager: OrganizationEventManager
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.AuthenticatorProvider
|
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.AuthenticatorProvider
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
@ -123,6 +124,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||||
*/
|
*/
|
||||||
val passwordResetReason: ForcePasswordResetReason?
|
val passwordResetReason: ForcePasswordResetReason?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organization for the active user.
|
||||||
|
*/
|
||||||
|
val organizations: List<SyncResponseJson.Profile.Organization>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -312,6 +312,9 @@ class AuthRepositoryImpl(
|
||||||
?.profile
|
?.profile
|
||||||
?.forcePasswordResetReason
|
?.forcePasswordResetReason
|
||||||
|
|
||||||
|
override val organizations: List<SyncResponseJson.Profile.Organization>
|
||||||
|
get() = activeUserId?.let { authDiskSource.getOrganizations(it) }.orEmpty()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
pushManager
|
pushManager
|
||||||
.syncOrgKeysFlow
|
.syncOrgKeysFlow
|
||||||
|
|
|
@ -4,12 +4,14 @@ import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
||||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl
|
||||||
|
@ -35,6 +37,8 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManagerImpl
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
|
@ -62,6 +66,24 @@ object PlatformManagerModule {
|
||||||
fun provideAppForegroundManager(): AppForegroundManager =
|
fun provideAppForegroundManager(): AppForegroundManager =
|
||||||
AppForegroundManagerImpl()
|
AppForegroundManagerImpl()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOrganizationEventManager(
|
||||||
|
authRepository: AuthRepository,
|
||||||
|
vaultRepository: VaultRepository,
|
||||||
|
clock: Clock,
|
||||||
|
dispatcherManager: DispatcherManager,
|
||||||
|
eventDiskSource: EventDiskSource,
|
||||||
|
eventService: EventService,
|
||||||
|
): OrganizationEventManager = OrganizationEventManagerImpl(
|
||||||
|
authRepository = authRepository,
|
||||||
|
vaultRepository = vaultRepository,
|
||||||
|
clock = clock,
|
||||||
|
dispatcherManager = dispatcherManager,
|
||||||
|
eventDiskSource = eventDiskSource,
|
||||||
|
eventService = eventService,
|
||||||
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesCipherMatchingManager(
|
fun providesCipherMatchingManager(
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.manager.event
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager for tracking events.
|
||||||
|
*/
|
||||||
|
interface OrganizationEventManager {
|
||||||
|
/**
|
||||||
|
* Tracks a specific event to be uploaded at a different time.
|
||||||
|
*/
|
||||||
|
suspend fun trackEvent(eventType: OrganizationEventType, cipherId: String? = null)
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.manager.event
|
||||||
|
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of time to delay before attempting the first upload events after the app is
|
||||||
|
* foregrounded.
|
||||||
|
*/
|
||||||
|
private const val UPLOAD_DELAY_INITIAL_MS: Long = 120_000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of time to delay before a subsequent attempts to upload events after the first one.
|
||||||
|
*/
|
||||||
|
private const val UPLOAD_DELAY_INTERVAL_MS: Long = 300_000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [OrganizationEventManager].
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
class OrganizationEventManagerImpl(
|
||||||
|
private val clock: Clock,
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
|
private val vaultRepository: VaultRepository,
|
||||||
|
private val eventDiskSource: EventDiskSource,
|
||||||
|
private val eventService: EventService,
|
||||||
|
dispatcherManager: DispatcherManager,
|
||||||
|
processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
|
||||||
|
) : OrganizationEventManager {
|
||||||
|
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||||
|
private var job: Job = Job().apply { complete() }
|
||||||
|
|
||||||
|
init {
|
||||||
|
processLifecycleOwner.lifecycle.addObserver(
|
||||||
|
object : DefaultLifecycleObserver {
|
||||||
|
override fun onStart(owner: LifecycleOwner) = start()
|
||||||
|
|
||||||
|
override fun onStop(owner: LifecycleOwner) = stop()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
override suspend fun trackEvent(eventType: OrganizationEventType, cipherId: String?) {
|
||||||
|
val userId = authRepository.activeUserId ?: return
|
||||||
|
if (authRepository.authStateFlow.value !is AuthState.Authenticated) return
|
||||||
|
val organizations = authRepository.organizations.filter { it.shouldUseEvents }
|
||||||
|
if (organizations.none()) return
|
||||||
|
cipherId?.let { id ->
|
||||||
|
val cipherOrganizationId = vaultRepository
|
||||||
|
.getVaultItemStateFlow(itemId = id)
|
||||||
|
.first { it.data != null }
|
||||||
|
.data
|
||||||
|
?.organizationId
|
||||||
|
?: return
|
||||||
|
if (organizations.none { it.id == cipherOrganizationId }) return
|
||||||
|
}
|
||||||
|
eventDiskSource.addOrganizationEvent(
|
||||||
|
userId = userId,
|
||||||
|
event = OrganizationEvent(
|
||||||
|
type = eventType,
|
||||||
|
cipherId = cipherId,
|
||||||
|
date = ZonedDateTime.now(clock),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun uploadEvents() {
|
||||||
|
val userId = authRepository.activeUserId ?: return
|
||||||
|
val events = eventDiskSource
|
||||||
|
.getOrganizationEvents(userId = userId)
|
||||||
|
.takeUnless { it.isEmpty() }
|
||||||
|
?: return
|
||||||
|
eventService
|
||||||
|
.sendOrganizationEvents(events = events)
|
||||||
|
.onSuccess { eventDiskSource.deleteOrganizationEvents(userId = userId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start() {
|
||||||
|
job.cancel()
|
||||||
|
job = ioScope.launch {
|
||||||
|
delay(timeMillis = UPLOAD_DELAY_INITIAL_MS)
|
||||||
|
uploadEvents()
|
||||||
|
while (coroutineContext.isActive) {
|
||||||
|
delay(timeMillis = UPLOAD_DELAY_INTERVAL_MS)
|
||||||
|
uploadEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stop() {
|
||||||
|
job.cancel()
|
||||||
|
ioScope.launch { uploadEvents() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -571,6 +571,21 @@ class AuthRepositoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `organizations should return an empty list when there is no active user`() = runTest {
|
||||||
|
assertEquals(emptyList<SyncResponseJson.Profile.Organization>(), repository.organizations)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `organizations should pull from the organizations in the AuthDiskSource`() = runTest {
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
fakeAuthDiskSource.storeOrganizations(
|
||||||
|
userId = USER_ID_1,
|
||||||
|
organizations = ORGANIZATIONS,
|
||||||
|
)
|
||||||
|
assertEquals(ORGANIZATIONS, repository.organizations)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clear Pending Account Deletion should unblock userState updates`() = runTest {
|
fun `clear Pending Account Deletion should unblock userState updates`() = runTest {
|
||||||
val masterPassword = "hello world"
|
val masterPassword = "hello world"
|
||||||
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.manager.event
|
||||||
|
|
||||||
|
import com.bitwarden.vault.CipherView
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||||
|
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
|
||||||
|
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||||
|
import com.x8bit.bitwarden.data.util.FakeLifecycleOwner
|
||||||
|
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
class OrganizationEventManagerTest {
|
||||||
|
|
||||||
|
private val fakeLifecycleOwner = FakeLifecycleOwner()
|
||||||
|
private val fixedClock: Clock = Clock.fixed(
|
||||||
|
Instant.parse("2023-10-27T12:00:00Z"),
|
||||||
|
ZoneOffset.UTC,
|
||||||
|
)
|
||||||
|
private val dispatcher = StandardTestDispatcher()
|
||||||
|
private val fakeDispatcherManager = FakeDispatcherManager(io = dispatcher)
|
||||||
|
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(value = AuthState.Uninitialized)
|
||||||
|
private val authRepository = mockk<AuthRepository> {
|
||||||
|
every { activeUserId } returns USER_ID
|
||||||
|
every { authStateFlow } returns mutableAuthStateFlow
|
||||||
|
every { organizations } returns emptyList()
|
||||||
|
}
|
||||||
|
private val mutableVaultItemStateFlow = MutableStateFlow<DataState<CipherView?>>(
|
||||||
|
value = DataState.Loading
|
||||||
|
)
|
||||||
|
private val vaultRepository = mockk<VaultRepository> {
|
||||||
|
every { getVaultItemStateFlow(itemId = any()) } returns mutableVaultItemStateFlow
|
||||||
|
}
|
||||||
|
private val eventService = mockk<EventService>()
|
||||||
|
private val eventDiskSource = mockk<EventDiskSource> {
|
||||||
|
coEvery { addOrganizationEvent(userId = any(), event = any()) } just runs
|
||||||
|
}
|
||||||
|
|
||||||
|
private val organizationEventManager: OrganizationEventManager = OrganizationEventManagerImpl(
|
||||||
|
processLifecycleOwner = fakeLifecycleOwner,
|
||||||
|
clock = fixedClock,
|
||||||
|
dispatcherManager = fakeDispatcherManager,
|
||||||
|
authRepository = authRepository,
|
||||||
|
vaultRepository = vaultRepository,
|
||||||
|
eventService = eventService,
|
||||||
|
eventDiskSource = eventDiskSource,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onLifecycleStart should upload events after 2 minutes and again after 5 more minutes`() =
|
||||||
|
runTest {
|
||||||
|
val organizationEvent = OrganizationEvent(
|
||||||
|
type = OrganizationEventType.CIPHER_UPDATED,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
date = ZonedDateTime.now(fixedClock),
|
||||||
|
)
|
||||||
|
val events = listOf(organizationEvent)
|
||||||
|
coEvery { eventDiskSource.getOrganizationEvents(userId = USER_ID) } returns events
|
||||||
|
coEvery {
|
||||||
|
eventService.sendOrganizationEvents(events = events)
|
||||||
|
} returns Unit.asSuccess()
|
||||||
|
coEvery { eventDiskSource.deleteOrganizationEvents(userId = USER_ID) } just runs
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
|
||||||
|
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 120_000L)
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
eventDiskSource.getOrganizationEvents(userId = USER_ID)
|
||||||
|
eventService.sendOrganizationEvents(events = events)
|
||||||
|
eventDiskSource.deleteOrganizationEvents(userId = USER_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 300_000L)
|
||||||
|
coVerify(exactly = 2) {
|
||||||
|
eventDiskSource.getOrganizationEvents(userId = USER_ID)
|
||||||
|
eventService.sendOrganizationEvents(events = events)
|
||||||
|
eventDiskSource.deleteOrganizationEvents(userId = USER_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onLifecycleStop should upload events immediately`() = runTest {
|
||||||
|
val organizationEvent = OrganizationEvent(
|
||||||
|
type = OrganizationEventType.CIPHER_UPDATED,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
date = ZonedDateTime.now(fixedClock),
|
||||||
|
)
|
||||||
|
val events = listOf(organizationEvent)
|
||||||
|
coEvery { eventDiskSource.getOrganizationEvents(userId = USER_ID) } returns events
|
||||||
|
coEvery { eventService.sendOrganizationEvents(events = events) } returns Unit.asSuccess()
|
||||||
|
coEvery { eventDiskSource.deleteOrganizationEvents(userId = USER_ID) } just runs
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStop()
|
||||||
|
|
||||||
|
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 120_000L)
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
eventDiskSource.getOrganizationEvents(userId = USER_ID)
|
||||||
|
eventService.sendOrganizationEvents(events = events)
|
||||||
|
eventDiskSource.deleteOrganizationEvents(userId = USER_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trackEvent should do nothing if there is no active user`() = runTest {
|
||||||
|
every { authRepository.activeUserId } returns null
|
||||||
|
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
eventType = OrganizationEventType.CIPHER_UPDATED,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
eventDiskSource.addOrganizationEvent(userId = any(), event = any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trackEvent should do nothing if the active user is not authenticated`() = runTest {
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
eventType = OrganizationEventType.CIPHER_UPDATED,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
eventDiskSource.addOrganizationEvent(userId = any(), event = any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trackEvent should do nothing if the active user has no organizations that use events`() =
|
||||||
|
runTest {
|
||||||
|
mutableAuthStateFlow.value = AuthState.Authenticated(accessToken = "access-token")
|
||||||
|
val organization = createMockOrganization(number = 1)
|
||||||
|
every { authRepository.organizations } returns listOf(organization)
|
||||||
|
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
eventType = OrganizationEventType.CIPHER_UPDATED,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
eventDiskSource.addOrganizationEvent(userId = any(), event = any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `trackEvent should do nothing if the cipher does not belong to an organization that uses events`() =
|
||||||
|
runTest {
|
||||||
|
mutableAuthStateFlow.value = AuthState.Authenticated(accessToken = "access-token")
|
||||||
|
val organization = createMockOrganization(number = 1).copy(shouldUseEvents = true)
|
||||||
|
every { authRepository.organizations } returns listOf(organization)
|
||||||
|
val cipherView = createMockCipherView(number = 1)
|
||||||
|
mutableVaultItemStateFlow.value = DataState.Loaded(data = cipherView)
|
||||||
|
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
eventType = OrganizationEventType.CIPHER_UPDATED,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
eventDiskSource.addOrganizationEvent(userId = any(), event = any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trackEvent should add the event to disk if the ciphers organization allows it`() =
|
||||||
|
runTest {
|
||||||
|
mutableAuthStateFlow.value = AuthState.Authenticated(accessToken = "access-token")
|
||||||
|
val organization = createMockOrganization(number = 1).copy(
|
||||||
|
id = "mockOrganizationId-1",
|
||||||
|
shouldUseEvents = true,
|
||||||
|
)
|
||||||
|
every { authRepository.organizations } returns listOf(organization)
|
||||||
|
val cipherView = createMockCipherView(number = 1)
|
||||||
|
mutableVaultItemStateFlow.value = DataState.Loaded(data = cipherView)
|
||||||
|
val eventType = OrganizationEventType.CIPHER_UPDATED
|
||||||
|
|
||||||
|
organizationEventManager.trackEvent(
|
||||||
|
eventType = eventType,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
eventDiskSource.addOrganizationEvent(
|
||||||
|
userId = USER_ID,
|
||||||
|
event = OrganizationEvent(
|
||||||
|
type = eventType,
|
||||||
|
cipherId = CIPHER_ID,
|
||||||
|
date = ZonedDateTime.now(fixedClock),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val CIPHER_ID: String = "mockId-1"
|
||||||
|
private const val USER_ID: String = "user-id"
|
Loading…
Reference in a new issue