Loading backend/api/src/main/java/com/fsck/k9/backend/api/BackendStorage.kt +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ interface BackendStorage { } interface BackendFolderUpdater : Closeable { @Throws(MessagingException::class) fun createFolders(folders: List<FolderInfo>): Set<Long> fun deleteFolders(folderServerIds: List<String>) Loading feature/mail/message/list/src/main/kotlin/net/thunderbird/feature/mail/message/list/FeatureMessageModule.kt +1 −1 Original line number Diff line number Diff line Loading @@ -20,7 +20,7 @@ val featureMessageModule = module { factory<DomainContract.UseCase.GetAccountFolders> { GetAccountFolders(folderRepository = get()) } factory<DomainContract.UseCase.CreateArchiveFolder> { CreateArchiveFolder( baseAccountManager = get<AccountManager<BaseAccount>>(), accountManager = get<AccountManager<BaseAccount>>(), backendStorageFactory = get<BackendStorageFactory<BaseAccount>>(), specialFolderUpdaterFactory = get<SpecialFolderUpdater.Factory<BaseAccount>>(), remoteFolderCreatorFactory = get(named("imap")), Loading feature/mail/message/list/src/main/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/CreateArchiveFolder.kt +3 −3 Original line number Diff line number Diff line Loading @@ -24,7 +24,7 @@ import net.thunderbird.feature.mail.message.list.domain.DomainContract import com.fsck.k9.mail.FolderType as LegacyFolderType class CreateArchiveFolder( private val baseAccountManager: AccountManager<BaseAccount>, private val accountManager: AccountManager<BaseAccount>, private val backendStorageFactory: BackendStorageFactory<BaseAccount>, private val remoteFolderCreatorFactory: RemoteFolderCreator.Factory, private val specialFolderUpdaterFactory: SpecialFolderUpdater.Factory<BaseAccount>, Loading @@ -40,7 +40,7 @@ class CreateArchiveFolder( } val account = withContext(ioDispatcher) { baseAccountManager.getAccount(accountUuid) accountManager.getAccount(accountUuid) } ?: run { emit(Outcome.failure(CreateArchiveFolderOutcome.Error.AccountNotFound)) return@flow Loading Loading @@ -110,7 +110,7 @@ class CreateArchiveFolder( selection = SpecialFolderSelection.MANUAL, ) specialFolderUpdater.updateSpecialFolders() baseAccountManager.saveAccount(account) accountManager.saveAccount(account) } emit(Outcome.success(CreateArchiveFolderOutcome.Success.Created)) } Loading feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/CreateArchiveFolderTest.kt 0 → 100644 +446 −0 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.domain.usecase import app.cash.turbine.test import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.prop import com.fsck.k9.backend.api.FolderInfo import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.folders.FolderServerId import dev.mokkery.matcher.any import dev.mokkery.matcher.eq import dev.mokkery.spy import dev.mokkery.verify import dev.mokkery.verify.VerifyMode.Companion.exactly import dev.mokkery.verifySuspend import kotlin.test.Test import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import net.thunderbird.backend.api.folder.RemoteFolderCreationOutcome import net.thunderbird.backend.api.folder.RemoteFolderCreator import net.thunderbird.core.outcome.Outcome import net.thunderbird.feature.mail.account.api.BaseAccount import net.thunderbird.feature.mail.folder.api.FolderType import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection import net.thunderbird.feature.mail.message.list.domain.CreateArchiveFolderOutcome import net.thunderbird.feature.mail.message.list.fakes.FakeAccount import net.thunderbird.feature.mail.message.list.fakes.FakeAccountManager import net.thunderbird.feature.mail.message.list.fakes.FakeBackendFolderUpdater import net.thunderbird.feature.mail.message.list.fakes.FakeBackendStorageFactory import net.thunderbird.feature.mail.message.list.fakes.FakeSpecialFolderUpdaterFactory import com.fsck.k9.mail.FolderType as LegacyFolderType @OptIn(ExperimentalUuidApi::class) @Suppress("MaxLineLength") class CreateArchiveFolderTest { @Test fun `invoke should emit InvalidFolderName and complete flow when folderName is invalid`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid = accountUuid) val accountManager = spy(FakeAccountManager(accounts)) val testSubject = createTestSubject(accountManager = accountManager) val folderName = "" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.InvalidFolderName>() .prop("folderName") { it.folderName } .isEqualTo(folderName) verify(exactly(0)) { accountManager.getAccount(accountUuid = any()) } awaitComplete() } } @Test fun `invoke should emit AccountNotFound and complete flow when no account uuid matches with account list`() = runTest { // Arrange val accountUuid = "any-non-expected-account-uuid" val accounts = createAccountList() val accountManager = spy(FakeAccountManager(accounts)) val testSubject = createTestSubject(accountManager = accountManager) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isEqualTo(CreateArchiveFolderOutcome.Error.AccountNotFound) verify(exactly(1)) { accountManager.getAccount(accountUuid) } awaitComplete() } } @Test fun `invoke should emit UnhandledError and complete flow when BackendStorage createFolder throws MessagingException`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val exception = MessagingException("this is an error") val backendFolderUpdater = FakeBackendFolderUpdater(exception) val remoteFolderCreatorFactory = spy(FakeRemoteFolderCreatorFactory(outcome = null)) val testSubject = createTestSubject( accounts = accounts, backendStorageFactory = FakeBackendStorageFactory(backendFolderUpdater), remoteFolderCreatorFactory = remoteFolderCreatorFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.UnhandledError>() .prop("throwable") { it.throwable } .hasMessage(exception.message) verify(exactly(0)) { remoteFolderCreatorFactory.create(account = any()) } awaitComplete() } } @Test fun `invoke should emit LocalFolderCreationError and complete flow when BackendStorage createFolder returns null`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater( returnEmptySetWhenCreatingFolders = true, ), ) val remoteFolderCreatorFactory = spy(FakeRemoteFolderCreatorFactory(outcome = null)) val testSubject = createTestSubject( accounts = accounts, backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.LocalFolderCreationError>() .prop("folderName") { it.folderName } .isEqualTo(folderName) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } verify(exactly(0)) { remoteFolderCreatorFactory.create(account = any()) } awaitComplete() } } @Test fun `invoke should emit LocalFolderCreated when BackendStorage createFolder returns folderId`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid = accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val testSubject = createTestSubject( accounts = accounts, remoteFolderCreatorOutcome = Outcome.success(RemoteFolderCreationOutcome.Success.Created), backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isEqualTo(CreateArchiveFolderOutcome.Success.LocalFolderCreated) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } cancelAndIgnoreRemainingEvents() } } @Test fun `invoke should emit SyncStarted when local folder synchronization with remote starts`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val testSubject = createTestSubject( accounts = accounts, remoteFolderCreatorOutcome = Outcome.success(RemoteFolderCreationOutcome.Success.Created), backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert skipItems(count = 1) // Skip LocalFolderCreated event. val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isInstanceOf<CreateArchiveFolderOutcome.Success.SyncStarted>() .prop("serverId") { it.serverId } .isEqualTo(FolderServerId(folderName)) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } cancelAndIgnoreRemainingEvents() } } @Test fun `invoke should emit SyncError when remote folder creation fails for any reason`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val error = RemoteFolderCreationOutcome.Error.AlreadyExists val testSubject = createTestSubject( accounts = accounts, remoteFolderCreatorOutcome = Outcome.failure(error), backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert skipItems(count = 2) // Skip LocalFolderCreated and SyncStarted event. val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.SyncError.Failed>() .isEqualTo( CreateArchiveFolderOutcome.Error.SyncError.Failed( serverId = FolderServerId(folderName), message = error.toString(), exception = null, ), ) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } cancelAndIgnoreRemainingEvents() } } @Test @Suppress("LongMethod") fun `invoke should emit Success when local and remote folder creation succeed`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val accountManager = spy(FakeAccountManager(accounts)) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val specialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory() val remoteFolderCreatorFactory = FakeRemoteFolderCreatorFactory( Outcome.success(RemoteFolderCreationOutcome.Success.Created), ) val testSubject = createTestSubject( accountManager = accountManager, remoteFolderCreatorFactory = remoteFolderCreatorFactory, backendStorageFactory = backendStorageFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert skipItems(count = 2) // Skip LocalFolderCreated and SyncStarted event. var outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isEqualTo(CreateArchiveFolderOutcome.Success.UpdatingSpecialFolders) outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isEqualTo(CreateArchiveFolderOutcome.Success.Created) verify(exactly(1)) { accountManager.getAccount(accountUuid) } verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } verifySuspend(exactly(1)) { remoteFolderCreatorFactory.instance.create( folderServerId = FolderServerId(folderName), mustCreate = false, folderType = LegacyFolderType.ARCHIVE, ) } verify(exactly(1)) { specialFolderUpdaterFactory.specialFolderUpdater.setSpecialFolder( type = FolderType.ARCHIVE, folderId = any(), selection = SpecialFolderSelection.MANUAL, ) } verify(exactly(1)) { specialFolderUpdaterFactory.specialFolderUpdater.updateSpecialFolders() } verify(exactly(1)) { accountManager.saveAccount(account = any()) } awaitComplete() } } @OptIn(ExperimentalCoroutinesApi::class) private fun createTestSubject( accounts: List<BaseAccount> = emptyList(), accountManager: FakeAccountManager = FakeAccountManager(accounts), backendStorageFactory: FakeBackendStorageFactory = FakeBackendStorageFactory(), remoteFolderCreatorOutcome: Outcome< RemoteFolderCreationOutcome.Success, RemoteFolderCreationOutcome.Error, >? = null, remoteFolderCreatorFactory: FakeRemoteFolderCreatorFactory = FakeRemoteFolderCreatorFactory( outcome = remoteFolderCreatorOutcome, ), specialFolderUpdaterFactory: FakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory(), ): CreateArchiveFolder = CreateArchiveFolder( accountManager = accountManager, backendStorageFactory = backendStorageFactory, remoteFolderCreatorFactory = remoteFolderCreatorFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ioDispatcher = UnconfinedTestDispatcher(), ) private fun createAccountList( accountUuid: String = Uuid.random().toHexString(), size: Int = 10, ) = List(size = size) { FakeAccount(uuid = if (it == 0) accountUuid else Uuid.random().toHexString()) } } private open class FakeRemoteFolderCreatorFactory( protected open val outcome: Outcome<RemoteFolderCreationOutcome.Success, RemoteFolderCreationOutcome.Error>?, ) : RemoteFolderCreator.Factory { open var instance: RemoteFolderCreator = spy<RemoteFolderCreator>(FakeRemoteFolderCreator()) protected set override fun create(account: BaseAccount): RemoteFolderCreator = instance private open inner class FakeRemoteFolderCreator : RemoteFolderCreator { override suspend fun create( folderServerId: FolderServerId, mustCreate: Boolean, folderType: LegacyFolderType, ): Outcome<RemoteFolderCreationOutcome.Success, RemoteFolderCreationOutcome.Error> = outcome ?: error("Not expected to be called in this context.") } } feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/fakes/FakeAccountManager.kt +4 −3 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.fakes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import net.thunderbird.feature.mail.account.api.AccountManager import net.thunderbird.feature.mail.account.api.BaseAccount internal open class FakeAccountManager( private val accounts: List<BaseAccount>, ) : AccountManager<BaseAccount> { override fun getAccounts(): List<BaseAccount> = error("not implemented.") override fun getAccounts(): List<BaseAccount> = accounts override fun getAccountsFlow(): Flow<List<BaseAccount>> = error("not implemented.") override fun getAccountsFlow(): Flow<List<BaseAccount>> = flowOf(accounts) override fun getAccount(accountUuid: String): BaseAccount? = accounts.firstOrNull { it.uuid == accountUuid } override fun getAccountFlow(accountUuid: String): Flow<BaseAccount?> = error("not implemented.") override fun getAccountFlow(accountUuid: String): Flow<BaseAccount?> = flowOf(getAccount(accountUuid)) override fun moveAccount( account: BaseAccount, Loading Loading
backend/api/src/main/java/com/fsck/k9/backend/api/BackendStorage.kt +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ interface BackendStorage { } interface BackendFolderUpdater : Closeable { @Throws(MessagingException::class) fun createFolders(folders: List<FolderInfo>): Set<Long> fun deleteFolders(folderServerIds: List<String>) Loading
feature/mail/message/list/src/main/kotlin/net/thunderbird/feature/mail/message/list/FeatureMessageModule.kt +1 −1 Original line number Diff line number Diff line Loading @@ -20,7 +20,7 @@ val featureMessageModule = module { factory<DomainContract.UseCase.GetAccountFolders> { GetAccountFolders(folderRepository = get()) } factory<DomainContract.UseCase.CreateArchiveFolder> { CreateArchiveFolder( baseAccountManager = get<AccountManager<BaseAccount>>(), accountManager = get<AccountManager<BaseAccount>>(), backendStorageFactory = get<BackendStorageFactory<BaseAccount>>(), specialFolderUpdaterFactory = get<SpecialFolderUpdater.Factory<BaseAccount>>(), remoteFolderCreatorFactory = get(named("imap")), Loading
feature/mail/message/list/src/main/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/CreateArchiveFolder.kt +3 −3 Original line number Diff line number Diff line Loading @@ -24,7 +24,7 @@ import net.thunderbird.feature.mail.message.list.domain.DomainContract import com.fsck.k9.mail.FolderType as LegacyFolderType class CreateArchiveFolder( private val baseAccountManager: AccountManager<BaseAccount>, private val accountManager: AccountManager<BaseAccount>, private val backendStorageFactory: BackendStorageFactory<BaseAccount>, private val remoteFolderCreatorFactory: RemoteFolderCreator.Factory, private val specialFolderUpdaterFactory: SpecialFolderUpdater.Factory<BaseAccount>, Loading @@ -40,7 +40,7 @@ class CreateArchiveFolder( } val account = withContext(ioDispatcher) { baseAccountManager.getAccount(accountUuid) accountManager.getAccount(accountUuid) } ?: run { emit(Outcome.failure(CreateArchiveFolderOutcome.Error.AccountNotFound)) return@flow Loading Loading @@ -110,7 +110,7 @@ class CreateArchiveFolder( selection = SpecialFolderSelection.MANUAL, ) specialFolderUpdater.updateSpecialFolders() baseAccountManager.saveAccount(account) accountManager.saveAccount(account) } emit(Outcome.success(CreateArchiveFolderOutcome.Success.Created)) } Loading
feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/CreateArchiveFolderTest.kt 0 → 100644 +446 −0 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.domain.usecase import app.cash.turbine.test import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.prop import com.fsck.k9.backend.api.FolderInfo import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.folders.FolderServerId import dev.mokkery.matcher.any import dev.mokkery.matcher.eq import dev.mokkery.spy import dev.mokkery.verify import dev.mokkery.verify.VerifyMode.Companion.exactly import dev.mokkery.verifySuspend import kotlin.test.Test import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import net.thunderbird.backend.api.folder.RemoteFolderCreationOutcome import net.thunderbird.backend.api.folder.RemoteFolderCreator import net.thunderbird.core.outcome.Outcome import net.thunderbird.feature.mail.account.api.BaseAccount import net.thunderbird.feature.mail.folder.api.FolderType import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection import net.thunderbird.feature.mail.message.list.domain.CreateArchiveFolderOutcome import net.thunderbird.feature.mail.message.list.fakes.FakeAccount import net.thunderbird.feature.mail.message.list.fakes.FakeAccountManager import net.thunderbird.feature.mail.message.list.fakes.FakeBackendFolderUpdater import net.thunderbird.feature.mail.message.list.fakes.FakeBackendStorageFactory import net.thunderbird.feature.mail.message.list.fakes.FakeSpecialFolderUpdaterFactory import com.fsck.k9.mail.FolderType as LegacyFolderType @OptIn(ExperimentalUuidApi::class) @Suppress("MaxLineLength") class CreateArchiveFolderTest { @Test fun `invoke should emit InvalidFolderName and complete flow when folderName is invalid`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid = accountUuid) val accountManager = spy(FakeAccountManager(accounts)) val testSubject = createTestSubject(accountManager = accountManager) val folderName = "" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.InvalidFolderName>() .prop("folderName") { it.folderName } .isEqualTo(folderName) verify(exactly(0)) { accountManager.getAccount(accountUuid = any()) } awaitComplete() } } @Test fun `invoke should emit AccountNotFound and complete flow when no account uuid matches with account list`() = runTest { // Arrange val accountUuid = "any-non-expected-account-uuid" val accounts = createAccountList() val accountManager = spy(FakeAccountManager(accounts)) val testSubject = createTestSubject(accountManager = accountManager) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isEqualTo(CreateArchiveFolderOutcome.Error.AccountNotFound) verify(exactly(1)) { accountManager.getAccount(accountUuid) } awaitComplete() } } @Test fun `invoke should emit UnhandledError and complete flow when BackendStorage createFolder throws MessagingException`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val exception = MessagingException("this is an error") val backendFolderUpdater = FakeBackendFolderUpdater(exception) val remoteFolderCreatorFactory = spy(FakeRemoteFolderCreatorFactory(outcome = null)) val testSubject = createTestSubject( accounts = accounts, backendStorageFactory = FakeBackendStorageFactory(backendFolderUpdater), remoteFolderCreatorFactory = remoteFolderCreatorFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.UnhandledError>() .prop("throwable") { it.throwable } .hasMessage(exception.message) verify(exactly(0)) { remoteFolderCreatorFactory.create(account = any()) } awaitComplete() } } @Test fun `invoke should emit LocalFolderCreationError and complete flow when BackendStorage createFolder returns null`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater( returnEmptySetWhenCreatingFolders = true, ), ) val remoteFolderCreatorFactory = spy(FakeRemoteFolderCreatorFactory(outcome = null)) val testSubject = createTestSubject( accounts = accounts, backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.LocalFolderCreationError>() .prop("folderName") { it.folderName } .isEqualTo(folderName) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } verify(exactly(0)) { remoteFolderCreatorFactory.create(account = any()) } awaitComplete() } } @Test fun `invoke should emit LocalFolderCreated when BackendStorage createFolder returns folderId`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid = accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val testSubject = createTestSubject( accounts = accounts, remoteFolderCreatorOutcome = Outcome.success(RemoteFolderCreationOutcome.Success.Created), backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isEqualTo(CreateArchiveFolderOutcome.Success.LocalFolderCreated) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } cancelAndIgnoreRemainingEvents() } } @Test fun `invoke should emit SyncStarted when local folder synchronization with remote starts`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val testSubject = createTestSubject( accounts = accounts, remoteFolderCreatorOutcome = Outcome.success(RemoteFolderCreationOutcome.Success.Created), backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert skipItems(count = 1) // Skip LocalFolderCreated event. val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isInstanceOf<CreateArchiveFolderOutcome.Success.SyncStarted>() .prop("serverId") { it.serverId } .isEqualTo(FolderServerId(folderName)) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } cancelAndIgnoreRemainingEvents() } } @Test fun `invoke should emit SyncError when remote folder creation fails for any reason`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val error = RemoteFolderCreationOutcome.Error.AlreadyExists val testSubject = createTestSubject( accounts = accounts, remoteFolderCreatorOutcome = Outcome.failure(error), backendStorageFactory = backendStorageFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert skipItems(count = 2) // Skip LocalFolderCreated and SyncStarted event. val outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Failure<CreateArchiveFolderOutcome.Error>>() .prop("error") { it.error } .isInstanceOf<CreateArchiveFolderOutcome.Error.SyncError.Failed>() .isEqualTo( CreateArchiveFolderOutcome.Error.SyncError.Failed( serverId = FolderServerId(folderName), message = error.toString(), exception = null, ), ) verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } cancelAndIgnoreRemainingEvents() } } @Test @Suppress("LongMethod") fun `invoke should emit Success when local and remote folder creation succeed`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = createAccountList(accountUuid) val accountManager = spy(FakeAccountManager(accounts)) val backendStorageFactory = FakeBackendStorageFactory( FakeBackendFolderUpdater(), ) val specialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory() val remoteFolderCreatorFactory = FakeRemoteFolderCreatorFactory( Outcome.success(RemoteFolderCreationOutcome.Success.Created), ) val testSubject = createTestSubject( accountManager = accountManager, remoteFolderCreatorFactory = remoteFolderCreatorFactory, backendStorageFactory = backendStorageFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ) val folderName = "TheFolder" // Act testSubject(accountUuid, folderName).test { // Assert skipItems(count = 2) // Skip LocalFolderCreated and SyncStarted event. var outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isEqualTo(CreateArchiveFolderOutcome.Success.UpdatingSpecialFolders) outcome = awaitItem() assertThat(outcome) .isInstanceOf<Outcome.Success<CreateArchiveFolderOutcome.Success>>() .prop("data") { it.data } .isEqualTo(CreateArchiveFolderOutcome.Success.Created) verify(exactly(1)) { accountManager.getAccount(accountUuid) } verify(exactly(1)) { // verify doesn't support verifying the extension function `createFolder`, // thus we verify the call of `createFolders(list)` instead. backendStorageFactory.backendFolderUpdater.createFolders( eq( listOf( FolderInfo( serverId = folderName, name = folderName, type = LegacyFolderType.ARCHIVE, ), ), ), ) } verifySuspend(exactly(1)) { remoteFolderCreatorFactory.instance.create( folderServerId = FolderServerId(folderName), mustCreate = false, folderType = LegacyFolderType.ARCHIVE, ) } verify(exactly(1)) { specialFolderUpdaterFactory.specialFolderUpdater.setSpecialFolder( type = FolderType.ARCHIVE, folderId = any(), selection = SpecialFolderSelection.MANUAL, ) } verify(exactly(1)) { specialFolderUpdaterFactory.specialFolderUpdater.updateSpecialFolders() } verify(exactly(1)) { accountManager.saveAccount(account = any()) } awaitComplete() } } @OptIn(ExperimentalCoroutinesApi::class) private fun createTestSubject( accounts: List<BaseAccount> = emptyList(), accountManager: FakeAccountManager = FakeAccountManager(accounts), backendStorageFactory: FakeBackendStorageFactory = FakeBackendStorageFactory(), remoteFolderCreatorOutcome: Outcome< RemoteFolderCreationOutcome.Success, RemoteFolderCreationOutcome.Error, >? = null, remoteFolderCreatorFactory: FakeRemoteFolderCreatorFactory = FakeRemoteFolderCreatorFactory( outcome = remoteFolderCreatorOutcome, ), specialFolderUpdaterFactory: FakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory(), ): CreateArchiveFolder = CreateArchiveFolder( accountManager = accountManager, backendStorageFactory = backendStorageFactory, remoteFolderCreatorFactory = remoteFolderCreatorFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ioDispatcher = UnconfinedTestDispatcher(), ) private fun createAccountList( accountUuid: String = Uuid.random().toHexString(), size: Int = 10, ) = List(size = size) { FakeAccount(uuid = if (it == 0) accountUuid else Uuid.random().toHexString()) } } private open class FakeRemoteFolderCreatorFactory( protected open val outcome: Outcome<RemoteFolderCreationOutcome.Success, RemoteFolderCreationOutcome.Error>?, ) : RemoteFolderCreator.Factory { open var instance: RemoteFolderCreator = spy<RemoteFolderCreator>(FakeRemoteFolderCreator()) protected set override fun create(account: BaseAccount): RemoteFolderCreator = instance private open inner class FakeRemoteFolderCreator : RemoteFolderCreator { override suspend fun create( folderServerId: FolderServerId, mustCreate: Boolean, folderType: LegacyFolderType, ): Outcome<RemoteFolderCreationOutcome.Success, RemoteFolderCreationOutcome.Error> = outcome ?: error("Not expected to be called in this context.") } }
feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/fakes/FakeAccountManager.kt +4 −3 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.fakes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import net.thunderbird.feature.mail.account.api.AccountManager import net.thunderbird.feature.mail.account.api.BaseAccount internal open class FakeAccountManager( private val accounts: List<BaseAccount>, ) : AccountManager<BaseAccount> { override fun getAccounts(): List<BaseAccount> = error("not implemented.") override fun getAccounts(): List<BaseAccount> = accounts override fun getAccountsFlow(): Flow<List<BaseAccount>> = error("not implemented.") override fun getAccountsFlow(): Flow<List<BaseAccount>> = flowOf(accounts) override fun getAccount(accountUuid: String): BaseAccount? = accounts.firstOrNull { it.uuid == accountUuid } override fun getAccountFlow(accountUuid: String): Flow<BaseAccount?> = error("not implemented.") override fun getAccountFlow(accountUuid: String): Flow<BaseAccount?> = flowOf(getAccount(accountUuid)) override fun moveAccount( account: BaseAccount, Loading