Loading feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt +3 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package app.k9mail.feature.account.setup import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.service.RealAutoDiscoveryService import app.k9mail.core.common.coreCommonModule import app.k9mail.feature.account.setup.domain.DomainContract import app.k9mail.feature.account.setup.domain.usecase.CheckIncomingServerConfig import app.k9mail.feature.account.setup.domain.usecase.CheckOutgoingServerConfig Loading Loading @@ -29,6 +30,7 @@ import org.koin.core.module.Module import org.koin.dsl.module val featureAccountSetupModule: Module = module { includes(coreCommonModule) single<OkHttpClient> { OkHttpClient() Loading @@ -43,6 +45,7 @@ val featureAccountSetupModule: Module = module { single<DomainContract.UseCase.GetAutoDiscovery> { GetAutoDiscovery( service = get(), oauthProvider = get(), ) } Loading feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/data/FakeAutoDiscoveryService.kt 0 → 100644 +82 −0 Original line number Diff line number Diff line package app.k9mail.feature.account.setup.data import app.k9mail.autodiscovery.api.AuthenticationType import app.k9mail.autodiscovery.api.AutoDiscoveryResult import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.core.common.mail.EmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import java.io.IOException import java.lang.UnsupportedOperationException import kotlin.random.Random import kotlinx.coroutines.delay class FakeAutoDiscoveryService : AutoDiscoveryService { override suspend fun discover(email: EmailAddress): AutoDiscoveryResult { val result: AutoDiscoveryResult? = handleFakeResponse(email) return if (result != null) { provideWithDelay(result) } else { AutoDiscoveryResult.UnexpectedException( UnsupportedOperationException("No fake response for $email"), ) } } @Suppress("MagicNumber") private suspend fun provideWithDelay(autoDiscoveryResult: AutoDiscoveryResult): AutoDiscoveryResult { delay(Random(0).nextLong(500, 2000)) return autoDiscoveryResult } private fun handleFakeResponse(emailAddress: EmailAddress): AutoDiscoveryResult? { return if (emailAddress.localPart.contains("empty")) { AutoDiscoveryResult.NoUsableSettingsFound } else if (emailAddress.localPart.contains("test")) { getFakeAutoDiscovery(emailAddress) } else if (emailAddress.localPart.contains("error")) { AutoDiscoveryResult.NetworkError(IOException("Failed to load config")) } else if (emailAddress.localPart.contains("unexpected")) { AutoDiscoveryResult.UnexpectedException(Exception("Unexpected exception")) } else { null } } @Suppress("MagicNumber") private fun getFakeAutoDiscovery(emailAddress: EmailAddress): AutoDiscoveryResult.Settings { val hasIncomingOauth = emailAddress.localPart.contains("in") val hasOutgoingOauth = emailAddress.localPart.contains("out") val isTrusted = emailAddress.localPart.contains("trust") return AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.${emailAddress.domain}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasIncomingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.${emailAddress.domain}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasOutgoingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", ), isTrusted = isTrusted, source = "fake", ) } } feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscovery.kt +50 −54 Original line number Diff line number Diff line Loading @@ -3,79 +3,75 @@ package app.k9mail.feature.account.setup.domain.usecase import app.k9mail.autodiscovery.api.AuthenticationType import app.k9mail.autodiscovery.api.AutoDiscoveryResult import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import app.k9mail.core.common.oauth.OAuthConfigurationProvider import app.k9mail.feature.account.setup.data.FakeAutoDiscoveryService import app.k9mail.feature.account.setup.domain.DomainContract import java.io.IOException import kotlin.random.Random import kotlinx.coroutines.delay internal class GetAutoDiscovery( private val service: AutoDiscoveryService, private val oauthProvider: OAuthConfigurationProvider, private val fakeService: FakeAutoDiscoveryService = FakeAutoDiscoveryService(), ) : DomainContract.UseCase.GetAutoDiscovery { override suspend fun execute(emailAddress: String): AutoDiscoveryResult { val fakeResult: AutoDiscoveryResult? = if (emailAddress.contains("empty")) { AutoDiscoveryResult.NoUsableSettingsFound } else if (emailAddress.contains("test")) { getFakeAutoDiscovery(emailAddress) } else if (emailAddress.contains("error")) { AutoDiscoveryResult.NetworkError(IOException("Failed to load config")) } else if (emailAddress.contains("unexpected")) { AutoDiscoveryResult.UnexpectedException(Exception("Unexpected exception")) } else { null val email = emailAddress.toUserEmailAddress() val fakeResult = fakeService.discover(email) if (fakeResult !is AutoDiscoveryResult.UnexpectedException) { return fakeResult } if (fakeResult != null) { return provideWithDelay(fakeResult) } val result = service.discover(email) return service.discover(emailAddress.toUserEmailAddress()) return if (result is AutoDiscoveryResult.Settings) { validateOAuthSupport(result) } else { result } } @Suppress("MagicNumber") private suspend fun provideWithDelay(autoDiscoveryResult: AutoDiscoveryResult): AutoDiscoveryResult { delay(Random(0).nextLong(500, 2000)) return autoDiscoveryResult private fun validateOAuthSupport(settings: AutoDiscoveryResult.Settings): AutoDiscoveryResult { if (settings.incomingServerSettings !is ImapServerSettings) { return AutoDiscoveryResult.NoUsableSettingsFound } @Suppress("MagicNumber") private fun getFakeAutoDiscovery(emailAddress: String): AutoDiscoveryResult.Settings { val hasIncomingOauth = emailAddress.contains("in") val hasOutgoingOauth = emailAddress.contains("out") val isTrusted = emailAddress.contains("trust") val incomingServerSettings = settings.incomingServerSettings as ImapServerSettings val outgoingServerSettings = settings.outgoingServerSettings as SmtpServerSettings return AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.${getHost(emailAddress)}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasIncomingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", val incomingAuthenticationType = updateAuthenticationType( authenticationType = incomingServerSettings.authenticationType, hostname = incomingServerSettings.hostname.value, ) val outgoingAuthenticationType = updateAuthenticationType( authenticationType = outgoingServerSettings.authenticationType, hostname = outgoingServerSettings.hostname.value, ) return settings.copy( incomingServerSettings = incomingServerSettings.copy( authenticationType = incomingAuthenticationType, ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.${getHost(emailAddress)}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasOutgoingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", outgoingServerSettings = outgoingServerSettings.copy( authenticationType = outgoingAuthenticationType, ), isTrusted = isTrusted, source = "fake", ) } private fun getHost(emailAddress: String) = emailAddress.split("@").last() private fun updateAuthenticationType( authenticationType: AuthenticationType, hostname: String, ): AuthenticationType { return if (authenticationType == AuthenticationType.OAuth2 && !isOAuthSupportedFor(hostname)) { // OAuth2 is not supported for this hostname, downgrade to password cleartext // TODO replace with next supported authentication type, once populated by autodiscovery AuthenticationType.PasswordCleartext } else { authenticationType } } private fun isOAuthSupportedFor(hostname: String): Boolean { return oauthProvider.getConfiguration(hostname) != null } } feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt +2 −0 Original line number Diff line number Diff line package app.k9mail.feature.account.setup import app.k9mail.core.common.oauth.OAuthConfigurationFactory import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult import app.k9mail.feature.account.setup.ui.AccountSetupContract Loading Loading @@ -34,6 +35,7 @@ class AccountSetupModuleKtTest : KoinTest { single<AccountCreator> { AccountCreator { _ -> AccountCreatorResult.Success("accountUuid") } } single<OAuthConfigurationFactory> { OAuthConfigurationFactory { emptyMap() } } } @Test Loading feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscoveryTest.kt 0 → 100644 +189 −0 Original line number Diff line number Diff line package app.k9mail.feature.account.setup.domain.usecase import app.k9mail.autodiscovery.api.AuthenticationType import app.k9mail.autodiscovery.api.AutoDiscoveryResult import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.IncomingServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.core.common.mail.EmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import app.k9mail.core.common.oauth.OAuthConfiguration import app.k9mail.core.common.oauth.OAuthConfigurationProvider import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import kotlinx.coroutines.test.runTest import org.junit.Test class GetAutoDiscoveryTest { @Test fun `should return a valid result`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_PASSWORD), oauthProvider = FakeOAuthConfigurationProvider(OAUTH_CONFIGURATION), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.Settings::class) .isEqualTo(SETTINGS_WITH_PASSWORD) } @Test fun `should return NoUsableSettingsFound result`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(AutoDiscoveryResult.NoUsableSettingsFound), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.NoUsableSettingsFound::class) } @Test fun `should return NoUsableSettingsFound result when server settings not supported`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_UNSUPPORTED_SERVER), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.NoUsableSettingsFound::class) } @Test fun `should return UnexpectedException result`() = runTest { val autoDiscoveryResult = AutoDiscoveryResult.UnexpectedException(Exception("unexpected exception")) val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(autoDiscoveryResult), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.UnexpectedException::class) .isEqualTo(autoDiscoveryResult) } @Test fun `should check for oauth support and return when supported`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_OAUTH), oauthProvider = FakeOAuthConfigurationProvider(OAUTH_CONFIGURATION), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.Settings::class) .isEqualTo(SETTINGS_WITH_OAUTH) } @Test fun `should check for oauth support and change auth type to password when not supported`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_OAUTH), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.Settings::class) .isEqualTo( SETTINGS_WITH_OAUTH.copy( incomingServerSettings = (SETTINGS_WITH_OAUTH.incomingServerSettings as ImapServerSettings).copy( authenticationType = AuthenticationType.PasswordCleartext, ), outgoingServerSettings = (SETTINGS_WITH_OAUTH.outgoingServerSettings as SmtpServerSettings).copy( authenticationType = AuthenticationType.PasswordCleartext, ), ), ) } private class FakeAutoDiscoveryService( private val answer: AutoDiscoveryResult = AutoDiscoveryResult.NoUsableSettingsFound, ) : AutoDiscoveryService { override suspend fun discover(email: EmailAddress): AutoDiscoveryResult = answer } private class FakeOAuthConfigurationProvider( private val answer: OAuthConfiguration? = null, ) : OAuthConfigurationProvider { override fun getConfiguration(hostname: String): OAuthConfiguration? = answer } private class UnsupportedServerSettings : IncomingServerSettings private companion object { private val SETTINGS_WITH_OAUTH = AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.example.com".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.OAuth2, username = "user", ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.example.com".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.OAuth2, username = "user", ), isTrusted = true, source = "source", ) private val SETTINGS_WITH_UNSUPPORTED_SERVER = AutoDiscoveryResult.Settings( incomingServerSettings = UnsupportedServerSettings(), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.example.com".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.OAuth2, username = "user", ), isTrusted = true, source = "source", ) private val SETTINGS_WITH_PASSWORD = AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.example.com".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.PasswordCleartext, username = "user", ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.example.com".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.PasswordCleartext, username = "user", ), isTrusted = true, source = "source", ) private val OAUTH_CONFIGURATION = OAuthConfiguration( clientId = "clientId", scopes = listOf("scopes"), authorizationEndpoint = "authorizationEndpoint", tokenEndpoint = "tokenEndpoint", redirectUri = "redirectUri", ) } } Loading
feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt +3 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package app.k9mail.feature.account.setup import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.service.RealAutoDiscoveryService import app.k9mail.core.common.coreCommonModule import app.k9mail.feature.account.setup.domain.DomainContract import app.k9mail.feature.account.setup.domain.usecase.CheckIncomingServerConfig import app.k9mail.feature.account.setup.domain.usecase.CheckOutgoingServerConfig Loading Loading @@ -29,6 +30,7 @@ import org.koin.core.module.Module import org.koin.dsl.module val featureAccountSetupModule: Module = module { includes(coreCommonModule) single<OkHttpClient> { OkHttpClient() Loading @@ -43,6 +45,7 @@ val featureAccountSetupModule: Module = module { single<DomainContract.UseCase.GetAutoDiscovery> { GetAutoDiscovery( service = get(), oauthProvider = get(), ) } Loading
feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/data/FakeAutoDiscoveryService.kt 0 → 100644 +82 −0 Original line number Diff line number Diff line package app.k9mail.feature.account.setup.data import app.k9mail.autodiscovery.api.AuthenticationType import app.k9mail.autodiscovery.api.AutoDiscoveryResult import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.core.common.mail.EmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import java.io.IOException import java.lang.UnsupportedOperationException import kotlin.random.Random import kotlinx.coroutines.delay class FakeAutoDiscoveryService : AutoDiscoveryService { override suspend fun discover(email: EmailAddress): AutoDiscoveryResult { val result: AutoDiscoveryResult? = handleFakeResponse(email) return if (result != null) { provideWithDelay(result) } else { AutoDiscoveryResult.UnexpectedException( UnsupportedOperationException("No fake response for $email"), ) } } @Suppress("MagicNumber") private suspend fun provideWithDelay(autoDiscoveryResult: AutoDiscoveryResult): AutoDiscoveryResult { delay(Random(0).nextLong(500, 2000)) return autoDiscoveryResult } private fun handleFakeResponse(emailAddress: EmailAddress): AutoDiscoveryResult? { return if (emailAddress.localPart.contains("empty")) { AutoDiscoveryResult.NoUsableSettingsFound } else if (emailAddress.localPart.contains("test")) { getFakeAutoDiscovery(emailAddress) } else if (emailAddress.localPart.contains("error")) { AutoDiscoveryResult.NetworkError(IOException("Failed to load config")) } else if (emailAddress.localPart.contains("unexpected")) { AutoDiscoveryResult.UnexpectedException(Exception("Unexpected exception")) } else { null } } @Suppress("MagicNumber") private fun getFakeAutoDiscovery(emailAddress: EmailAddress): AutoDiscoveryResult.Settings { val hasIncomingOauth = emailAddress.localPart.contains("in") val hasOutgoingOauth = emailAddress.localPart.contains("out") val isTrusted = emailAddress.localPart.contains("trust") return AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.${emailAddress.domain}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasIncomingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.${emailAddress.domain}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasOutgoingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", ), isTrusted = isTrusted, source = "fake", ) } }
feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscovery.kt +50 −54 Original line number Diff line number Diff line Loading @@ -3,79 +3,75 @@ package app.k9mail.feature.account.setup.domain.usecase import app.k9mail.autodiscovery.api.AuthenticationType import app.k9mail.autodiscovery.api.AutoDiscoveryResult import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import app.k9mail.core.common.oauth.OAuthConfigurationProvider import app.k9mail.feature.account.setup.data.FakeAutoDiscoveryService import app.k9mail.feature.account.setup.domain.DomainContract import java.io.IOException import kotlin.random.Random import kotlinx.coroutines.delay internal class GetAutoDiscovery( private val service: AutoDiscoveryService, private val oauthProvider: OAuthConfigurationProvider, private val fakeService: FakeAutoDiscoveryService = FakeAutoDiscoveryService(), ) : DomainContract.UseCase.GetAutoDiscovery { override suspend fun execute(emailAddress: String): AutoDiscoveryResult { val fakeResult: AutoDiscoveryResult? = if (emailAddress.contains("empty")) { AutoDiscoveryResult.NoUsableSettingsFound } else if (emailAddress.contains("test")) { getFakeAutoDiscovery(emailAddress) } else if (emailAddress.contains("error")) { AutoDiscoveryResult.NetworkError(IOException("Failed to load config")) } else if (emailAddress.contains("unexpected")) { AutoDiscoveryResult.UnexpectedException(Exception("Unexpected exception")) } else { null val email = emailAddress.toUserEmailAddress() val fakeResult = fakeService.discover(email) if (fakeResult !is AutoDiscoveryResult.UnexpectedException) { return fakeResult } if (fakeResult != null) { return provideWithDelay(fakeResult) } val result = service.discover(email) return service.discover(emailAddress.toUserEmailAddress()) return if (result is AutoDiscoveryResult.Settings) { validateOAuthSupport(result) } else { result } } @Suppress("MagicNumber") private suspend fun provideWithDelay(autoDiscoveryResult: AutoDiscoveryResult): AutoDiscoveryResult { delay(Random(0).nextLong(500, 2000)) return autoDiscoveryResult private fun validateOAuthSupport(settings: AutoDiscoveryResult.Settings): AutoDiscoveryResult { if (settings.incomingServerSettings !is ImapServerSettings) { return AutoDiscoveryResult.NoUsableSettingsFound } @Suppress("MagicNumber") private fun getFakeAutoDiscovery(emailAddress: String): AutoDiscoveryResult.Settings { val hasIncomingOauth = emailAddress.contains("in") val hasOutgoingOauth = emailAddress.contains("out") val isTrusted = emailAddress.contains("trust") val incomingServerSettings = settings.incomingServerSettings as ImapServerSettings val outgoingServerSettings = settings.outgoingServerSettings as SmtpServerSettings return AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.${getHost(emailAddress)}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasIncomingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", val incomingAuthenticationType = updateAuthenticationType( authenticationType = incomingServerSettings.authenticationType, hostname = incomingServerSettings.hostname.value, ) val outgoingAuthenticationType = updateAuthenticationType( authenticationType = outgoingServerSettings.authenticationType, hostname = outgoingServerSettings.hostname.value, ) return settings.copy( incomingServerSettings = incomingServerSettings.copy( authenticationType = incomingAuthenticationType, ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.${getHost(emailAddress)}".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = if (hasOutgoingOauth) { AuthenticationType.OAuth2 } else { AuthenticationType.PasswordEncrypted }, username = "username", outgoingServerSettings = outgoingServerSettings.copy( authenticationType = outgoingAuthenticationType, ), isTrusted = isTrusted, source = "fake", ) } private fun getHost(emailAddress: String) = emailAddress.split("@").last() private fun updateAuthenticationType( authenticationType: AuthenticationType, hostname: String, ): AuthenticationType { return if (authenticationType == AuthenticationType.OAuth2 && !isOAuthSupportedFor(hostname)) { // OAuth2 is not supported for this hostname, downgrade to password cleartext // TODO replace with next supported authentication type, once populated by autodiscovery AuthenticationType.PasswordCleartext } else { authenticationType } } private fun isOAuthSupportedFor(hostname: String): Boolean { return oauthProvider.getConfiguration(hostname) != null } }
feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt +2 −0 Original line number Diff line number Diff line package app.k9mail.feature.account.setup import app.k9mail.core.common.oauth.OAuthConfigurationFactory import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult import app.k9mail.feature.account.setup.ui.AccountSetupContract Loading Loading @@ -34,6 +35,7 @@ class AccountSetupModuleKtTest : KoinTest { single<AccountCreator> { AccountCreator { _ -> AccountCreatorResult.Success("accountUuid") } } single<OAuthConfigurationFactory> { OAuthConfigurationFactory { emptyMap() } } } @Test Loading
feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/GetAutoDiscoveryTest.kt 0 → 100644 +189 −0 Original line number Diff line number Diff line package app.k9mail.feature.account.setup.domain.usecase import app.k9mail.autodiscovery.api.AuthenticationType import app.k9mail.autodiscovery.api.AutoDiscoveryResult import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.api.ConnectionSecurity import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.autodiscovery.api.IncomingServerSettings import app.k9mail.autodiscovery.api.SmtpServerSettings import app.k9mail.core.common.mail.EmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import app.k9mail.core.common.oauth.OAuthConfiguration import app.k9mail.core.common.oauth.OAuthConfigurationProvider import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import kotlinx.coroutines.test.runTest import org.junit.Test class GetAutoDiscoveryTest { @Test fun `should return a valid result`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_PASSWORD), oauthProvider = FakeOAuthConfigurationProvider(OAUTH_CONFIGURATION), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.Settings::class) .isEqualTo(SETTINGS_WITH_PASSWORD) } @Test fun `should return NoUsableSettingsFound result`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(AutoDiscoveryResult.NoUsableSettingsFound), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.NoUsableSettingsFound::class) } @Test fun `should return NoUsableSettingsFound result when server settings not supported`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_UNSUPPORTED_SERVER), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.NoUsableSettingsFound::class) } @Test fun `should return UnexpectedException result`() = runTest { val autoDiscoveryResult = AutoDiscoveryResult.UnexpectedException(Exception("unexpected exception")) val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(autoDiscoveryResult), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.UnexpectedException::class) .isEqualTo(autoDiscoveryResult) } @Test fun `should check for oauth support and return when supported`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_OAUTH), oauthProvider = FakeOAuthConfigurationProvider(OAUTH_CONFIGURATION), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.Settings::class) .isEqualTo(SETTINGS_WITH_OAUTH) } @Test fun `should check for oauth support and change auth type to password when not supported`() = runTest { val useCase = GetAutoDiscovery( service = FakeAutoDiscoveryService(SETTINGS_WITH_OAUTH), oauthProvider = FakeOAuthConfigurationProvider(), ) val result = useCase.execute("user@example.com") assertThat(result) .isInstanceOf(AutoDiscoveryResult.Settings::class) .isEqualTo( SETTINGS_WITH_OAUTH.copy( incomingServerSettings = (SETTINGS_WITH_OAUTH.incomingServerSettings as ImapServerSettings).copy( authenticationType = AuthenticationType.PasswordCleartext, ), outgoingServerSettings = (SETTINGS_WITH_OAUTH.outgoingServerSettings as SmtpServerSettings).copy( authenticationType = AuthenticationType.PasswordCleartext, ), ), ) } private class FakeAutoDiscoveryService( private val answer: AutoDiscoveryResult = AutoDiscoveryResult.NoUsableSettingsFound, ) : AutoDiscoveryService { override suspend fun discover(email: EmailAddress): AutoDiscoveryResult = answer } private class FakeOAuthConfigurationProvider( private val answer: OAuthConfiguration? = null, ) : OAuthConfigurationProvider { override fun getConfiguration(hostname: String): OAuthConfiguration? = answer } private class UnsupportedServerSettings : IncomingServerSettings private companion object { private val SETTINGS_WITH_OAUTH = AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.example.com".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.OAuth2, username = "user", ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.example.com".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.OAuth2, username = "user", ), isTrusted = true, source = "source", ) private val SETTINGS_WITH_UNSUPPORTED_SERVER = AutoDiscoveryResult.Settings( incomingServerSettings = UnsupportedServerSettings(), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.example.com".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.OAuth2, username = "user", ), isTrusted = true, source = "source", ) private val SETTINGS_WITH_PASSWORD = AutoDiscoveryResult.Settings( incomingServerSettings = ImapServerSettings( hostname = "imap.example.com".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.PasswordCleartext, username = "user", ), outgoingServerSettings = SmtpServerSettings( hostname = "smtp.example.com".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.TLS, authenticationType = AuthenticationType.PasswordCleartext, username = "user", ), isTrusted = true, source = "source", ) private val OAUTH_CONFIGURATION = OAuthConfiguration( clientId = "clientId", scopes = listOf("scopes"), authorizationEndpoint = "authorizationEndpoint", tokenEndpoint = "tokenEndpoint", redirectUri = "redirectUri", ) } }