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 @@ -28,7 +28,7 @@ val featureMessageModule = module { } factory<DomainContract.UseCase.SetArchiveFolder> { SetArchiveFolder( baseAccountManager = get<AccountManager<BaseAccount>>(), accountManager = get<AccountManager<BaseAccount>>(), backendStorageFactory = get<BackendStorageFactory<BaseAccount>>(), specialFolderUpdaterFactory = get<SpecialFolderUpdater.Factory<BaseAccount>>(), ) Loading feature/mail/message/list/src/main/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/SetArchiveFolder.kt +11 −11 Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ import net.thunderbird.feature.mail.message.list.domain.SetAccountFolderOutcome import com.fsck.k9.mail.FolderType as LegacyFolderType internal class SetArchiveFolder( private val baseAccountManager: AccountManager<BaseAccount>, private val accountManager: AccountManager<BaseAccount>, private val backendStorageFactory: BackendStorageFactory<BaseAccount>, private val specialFolderUpdaterFactory: SpecialFolderUpdater.Factory<BaseAccount>, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, Loading @@ -27,16 +27,16 @@ internal class SetArchiveFolder( folder: RemoteFolder, ): Outcome<SetAccountFolderOutcome.Success, SetAccountFolderOutcome.Error> { val account = withContext(ioDispatcher) { baseAccountManager.getAccount(accountUuid) accountManager.getAccount(accountUuid) } ?: return Outcome.Failure(SetAccountFolderOutcome.Error.AccountNotFound) val backend = backendStorageFactory.createBackendStorage(account) val specialFolderUpdater = specialFolderUpdaterFactory.create(account) return withContext(ioDispatcher) { return try { withContext(ioDispatcher) { backend .createFolderUpdater() .use { updater -> try { updater.changeFolder( folderServerId = folder.serverId, name = folder.name, Loading @@ -48,13 +48,13 @@ internal class SetArchiveFolder( selection = SpecialFolderSelection.MANUAL, ) specialFolderUpdater.updateSpecialFolders() baseAccountManager.saveAccount(account) accountManager.saveAccount(account) Outcome.success(SetAccountFolderOutcome.Success) } catch (e: MessagingException) { Outcome.Failure(SetAccountFolderOutcome.Error.UnhandledError(throwable = e)) } } } catch (e: MessagingException) { Outcome.Failure(SetAccountFolderOutcome.Error.UnhandledError(throwable = e)) } } } feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/SetArchiveFolderTest.kt 0 → 100644 +186 −0 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.domain.usecase import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.prop import com.fsck.k9.mail.MessagingException import dev.mokkery.matcher.any import dev.mokkery.matcher.matching import dev.mokkery.spy import dev.mokkery.verify import dev.mokkery.verify.VerifyMode.Companion.exactly import kotlin.random.Random import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid import kotlinx.coroutines.test.runTest 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.RemoteFolder import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection import net.thunderbird.feature.mail.message.list.domain.SetAccountFolderOutcome 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 org.junit.Test import com.fsck.k9.mail.FolderType as LegacyFolderType @OptIn(ExperimentalUuidApi::class) @Suppress("MaxLineLength") class SetArchiveFolderTest { @Test fun `invoke should successfully create folder and update account when given valid input`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = listOf(FakeAccount(uuid = accountUuid)) val fakeBackendStorageFactory = FakeBackendStorageFactory() val fakeAccountManager = spy(FakeAccountManager(accounts)) val fakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory() val testSubject = createTestSubject(fakeAccountManager, fakeBackendStorageFactory, fakeSpecialFolderUpdaterFactory) val folder = createRemoteFolder() // Act val outcome = testSubject(accountUuid, folder) // Assert assertThat(outcome) .isInstanceOf<Outcome.Success<SetAccountFolderOutcome.Success>>() .prop(name = "data") { it.data } .isEqualTo(SetAccountFolderOutcome.Success) verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.changeFolder( folderServerId = folder.serverId, name = folder.name, type = LegacyFolderType.ARCHIVE, ) } verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.close() } verify(exactly(1)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.setSpecialFolder( type = FolderType.ARCHIVE, folderId = folder.id, selection = SpecialFolderSelection.MANUAL, ) } verify(exactly(1)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.updateSpecialFolders() } verify(exactly(1)) { fakeAccountManager.saveAccount( account = matching { it.uuid == accountUuid }, ) } } @Test fun `invoke should return AccountNotFound when account is not found`() = runTest { // Arrange val accounts = listOf<FakeAccount>() val testSubject = createTestSubject(accounts) val accountUuid = Uuid.random().toHexString() val folder = createRemoteFolder() // Act val outcome = testSubject(accountUuid, folder) // Assert assertThat(outcome) .isInstanceOf<Outcome.Failure<SetAccountFolderOutcome.Error>>() .prop(name = "error") { it.error } .isEqualTo(SetAccountFolderOutcome.Error.AccountNotFound) } @Test fun `invoke should return UnhandledError when changeFolder throws MessagingException`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = listOf(FakeAccount(uuid = accountUuid)) val exception = MessagingException("this is an error") val fakeBackendStorageFactory = FakeBackendStorageFactory( backendFolderUpdater = FakeBackendFolderUpdater(exception = exception), ) val fakeAccountManager = spy(FakeAccountManager(accounts)) val fakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory() val testSubject = createTestSubject(fakeAccountManager, fakeBackendStorageFactory, fakeSpecialFolderUpdaterFactory) val folder = createRemoteFolder() // Act val outcome = testSubject(accountUuid, folder) // Assert assertThat(outcome) .isInstanceOf<Outcome.Failure<SetAccountFolderOutcome.Error>>() .prop(name = "error") { it.error } .isInstanceOf<SetAccountFolderOutcome.Error.UnhandledError>() .prop("throwable") { it.throwable } .hasMessage(exception.message) verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.changeFolder( folderServerId = folder.serverId, name = folder.name, type = LegacyFolderType.ARCHIVE, ) } verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.close() } verify(exactly(0)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.setSpecialFolder( type = any(), folderId = any(), selection = any(), ) } verify(exactly(0)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.updateSpecialFolders() } verify(exactly(0)) { fakeAccountManager.saveAccount(account = any()) } } private fun createTestSubject( accounts: List<BaseAccount>, backendStorageFactory: FakeBackendStorageFactory = FakeBackendStorageFactory(), specialFolderUpdaterFactory: FakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory(), ): SetArchiveFolder = createTestSubject( accountManager = FakeAccountManager(accounts), backendStorageFactory = backendStorageFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ) private fun createTestSubject( accountManager: FakeAccountManager, backendStorageFactory: FakeBackendStorageFactory = FakeBackendStorageFactory(), specialFolderUpdaterFactory: FakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory(), ): SetArchiveFolder { return SetArchiveFolder( accountManager = accountManager, backendStorageFactory = backendStorageFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ) } private fun createRemoteFolder( id: Long = Random.nextLong(), serverId: String = "remote_folder_$id", name: String = serverId, ): RemoteFolder = RemoteFolder( id = id, serverId = serverId, name = name, type = FolderType.ARCHIVE, ) } feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/fakes/FakeAccountManager.kt +1 −1 Original line number Diff line number Diff line Loading @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.Flow import net.thunderbird.feature.mail.account.api.AccountManager import net.thunderbird.feature.mail.account.api.BaseAccount internal class FakeAccountManager( internal open class FakeAccountManager( private val accounts: List<BaseAccount>, ) : AccountManager<BaseAccount> { override fun getAccounts(): List<BaseAccount> = error("not implemented.") Loading feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/fakes/FakeBackendFolderUpdater.kt 0 → 100644 +32 −0 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.fakes import com.fsck.k9.backend.api.BackendFolderUpdater import com.fsck.k9.backend.api.FolderInfo import com.fsck.k9.mail.FolderType internal open class FakeBackendFolderUpdater( private val exception: Exception? = null, ) : BackendFolderUpdater { private val ids = mutableSetOf<Long>() override fun createFolders(folders: List<FolderInfo>): Set<Long> { if (exception != null) throw exception var last = ids.last() ids.addAll(folders.map { ++last }) return ids } override fun deleteFolders(folderServerIds: List<String>) { if (exception != null) throw exception } override fun changeFolder(folderServerId: String, name: String, type: FolderType) { if (exception != null) throw exception } override fun close() { if (exception != null) throw exception } } 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 @@ -28,7 +28,7 @@ val featureMessageModule = module { } factory<DomainContract.UseCase.SetArchiveFolder> { SetArchiveFolder( baseAccountManager = get<AccountManager<BaseAccount>>(), accountManager = get<AccountManager<BaseAccount>>(), backendStorageFactory = get<BackendStorageFactory<BaseAccount>>(), specialFolderUpdaterFactory = get<SpecialFolderUpdater.Factory<BaseAccount>>(), ) Loading
feature/mail/message/list/src/main/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/SetArchiveFolder.kt +11 −11 Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ import net.thunderbird.feature.mail.message.list.domain.SetAccountFolderOutcome import com.fsck.k9.mail.FolderType as LegacyFolderType internal class SetArchiveFolder( private val baseAccountManager: AccountManager<BaseAccount>, private val accountManager: AccountManager<BaseAccount>, private val backendStorageFactory: BackendStorageFactory<BaseAccount>, private val specialFolderUpdaterFactory: SpecialFolderUpdater.Factory<BaseAccount>, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, Loading @@ -27,16 +27,16 @@ internal class SetArchiveFolder( folder: RemoteFolder, ): Outcome<SetAccountFolderOutcome.Success, SetAccountFolderOutcome.Error> { val account = withContext(ioDispatcher) { baseAccountManager.getAccount(accountUuid) accountManager.getAccount(accountUuid) } ?: return Outcome.Failure(SetAccountFolderOutcome.Error.AccountNotFound) val backend = backendStorageFactory.createBackendStorage(account) val specialFolderUpdater = specialFolderUpdaterFactory.create(account) return withContext(ioDispatcher) { return try { withContext(ioDispatcher) { backend .createFolderUpdater() .use { updater -> try { updater.changeFolder( folderServerId = folder.serverId, name = folder.name, Loading @@ -48,13 +48,13 @@ internal class SetArchiveFolder( selection = SpecialFolderSelection.MANUAL, ) specialFolderUpdater.updateSpecialFolders() baseAccountManager.saveAccount(account) accountManager.saveAccount(account) Outcome.success(SetAccountFolderOutcome.Success) } catch (e: MessagingException) { Outcome.Failure(SetAccountFolderOutcome.Error.UnhandledError(throwable = e)) } } } catch (e: MessagingException) { Outcome.Failure(SetAccountFolderOutcome.Error.UnhandledError(throwable = e)) } } }
feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/domain/usecase/SetArchiveFolderTest.kt 0 → 100644 +186 −0 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.domain.usecase import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.prop import com.fsck.k9.mail.MessagingException import dev.mokkery.matcher.any import dev.mokkery.matcher.matching import dev.mokkery.spy import dev.mokkery.verify import dev.mokkery.verify.VerifyMode.Companion.exactly import kotlin.random.Random import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid import kotlinx.coroutines.test.runTest 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.RemoteFolder import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection import net.thunderbird.feature.mail.message.list.domain.SetAccountFolderOutcome 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 org.junit.Test import com.fsck.k9.mail.FolderType as LegacyFolderType @OptIn(ExperimentalUuidApi::class) @Suppress("MaxLineLength") class SetArchiveFolderTest { @Test fun `invoke should successfully create folder and update account when given valid input`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = listOf(FakeAccount(uuid = accountUuid)) val fakeBackendStorageFactory = FakeBackendStorageFactory() val fakeAccountManager = spy(FakeAccountManager(accounts)) val fakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory() val testSubject = createTestSubject(fakeAccountManager, fakeBackendStorageFactory, fakeSpecialFolderUpdaterFactory) val folder = createRemoteFolder() // Act val outcome = testSubject(accountUuid, folder) // Assert assertThat(outcome) .isInstanceOf<Outcome.Success<SetAccountFolderOutcome.Success>>() .prop(name = "data") { it.data } .isEqualTo(SetAccountFolderOutcome.Success) verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.changeFolder( folderServerId = folder.serverId, name = folder.name, type = LegacyFolderType.ARCHIVE, ) } verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.close() } verify(exactly(1)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.setSpecialFolder( type = FolderType.ARCHIVE, folderId = folder.id, selection = SpecialFolderSelection.MANUAL, ) } verify(exactly(1)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.updateSpecialFolders() } verify(exactly(1)) { fakeAccountManager.saveAccount( account = matching { it.uuid == accountUuid }, ) } } @Test fun `invoke should return AccountNotFound when account is not found`() = runTest { // Arrange val accounts = listOf<FakeAccount>() val testSubject = createTestSubject(accounts) val accountUuid = Uuid.random().toHexString() val folder = createRemoteFolder() // Act val outcome = testSubject(accountUuid, folder) // Assert assertThat(outcome) .isInstanceOf<Outcome.Failure<SetAccountFolderOutcome.Error>>() .prop(name = "error") { it.error } .isEqualTo(SetAccountFolderOutcome.Error.AccountNotFound) } @Test fun `invoke should return UnhandledError when changeFolder throws MessagingException`() = runTest { // Arrange val accountUuid = Uuid.random().toHexString() val accounts = listOf(FakeAccount(uuid = accountUuid)) val exception = MessagingException("this is an error") val fakeBackendStorageFactory = FakeBackendStorageFactory( backendFolderUpdater = FakeBackendFolderUpdater(exception = exception), ) val fakeAccountManager = spy(FakeAccountManager(accounts)) val fakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory() val testSubject = createTestSubject(fakeAccountManager, fakeBackendStorageFactory, fakeSpecialFolderUpdaterFactory) val folder = createRemoteFolder() // Act val outcome = testSubject(accountUuid, folder) // Assert assertThat(outcome) .isInstanceOf<Outcome.Failure<SetAccountFolderOutcome.Error>>() .prop(name = "error") { it.error } .isInstanceOf<SetAccountFolderOutcome.Error.UnhandledError>() .prop("throwable") { it.throwable } .hasMessage(exception.message) verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.changeFolder( folderServerId = folder.serverId, name = folder.name, type = LegacyFolderType.ARCHIVE, ) } verify(exactly(1)) { fakeBackendStorageFactory.backendFolderUpdater.close() } verify(exactly(0)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.setSpecialFolder( type = any(), folderId = any(), selection = any(), ) } verify(exactly(0)) { fakeSpecialFolderUpdaterFactory.specialFolderUpdater.updateSpecialFolders() } verify(exactly(0)) { fakeAccountManager.saveAccount(account = any()) } } private fun createTestSubject( accounts: List<BaseAccount>, backendStorageFactory: FakeBackendStorageFactory = FakeBackendStorageFactory(), specialFolderUpdaterFactory: FakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory(), ): SetArchiveFolder = createTestSubject( accountManager = FakeAccountManager(accounts), backendStorageFactory = backendStorageFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ) private fun createTestSubject( accountManager: FakeAccountManager, backendStorageFactory: FakeBackendStorageFactory = FakeBackendStorageFactory(), specialFolderUpdaterFactory: FakeSpecialFolderUpdaterFactory = FakeSpecialFolderUpdaterFactory(), ): SetArchiveFolder { return SetArchiveFolder( accountManager = accountManager, backendStorageFactory = backendStorageFactory, specialFolderUpdaterFactory = specialFolderUpdaterFactory, ) } private fun createRemoteFolder( id: Long = Random.nextLong(), serverId: String = "remote_folder_$id", name: String = serverId, ): RemoteFolder = RemoteFolder( id = id, serverId = serverId, name = name, type = FolderType.ARCHIVE, ) }
feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/fakes/FakeAccountManager.kt +1 −1 Original line number Diff line number Diff line Loading @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.Flow import net.thunderbird.feature.mail.account.api.AccountManager import net.thunderbird.feature.mail.account.api.BaseAccount internal class FakeAccountManager( internal open class FakeAccountManager( private val accounts: List<BaseAccount>, ) : AccountManager<BaseAccount> { override fun getAccounts(): List<BaseAccount> = error("not implemented.") Loading
feature/mail/message/list/src/test/kotlin/net/thunderbird/feature/mail/message/list/fakes/FakeBackendFolderUpdater.kt 0 → 100644 +32 −0 Original line number Diff line number Diff line package net.thunderbird.feature.mail.message.list.fakes import com.fsck.k9.backend.api.BackendFolderUpdater import com.fsck.k9.backend.api.FolderInfo import com.fsck.k9.mail.FolderType internal open class FakeBackendFolderUpdater( private val exception: Exception? = null, ) : BackendFolderUpdater { private val ids = mutableSetOf<Long>() override fun createFolders(folders: List<FolderInfo>): Set<Long> { if (exception != null) throw exception var last = ids.last() ids.addAll(folders.map { ++last }) return ids } override fun deleteFolders(folderServerIds: List<String>) { if (exception != null) throw exception } override fun changeFolder(folderServerId: String, name: String, type: FolderType) { if (exception != null) throw exception } override fun close() { if (exception != null) throw exception } }