Add an AuthStateStorage parameter to ServerSettingsValidator

Typically we use `Account` to hold the (OAuth 2.0) authorization state. But during account setup we don't have an `Account` instance yet. So we allow a `ServerSettingsValidator` caller to pass an `AuthStateStorage` that we then use with `OAuth2TokenProviderFactory` to create an `OAuth2TokenProvider` instance. When setting up an account we can use an `AuthStateStorage` implementation that will simply hold the state in memory.
This commit is contained in:
cketti 2023-07-29 23:15:27 +02:00
parent 3bc0bada31
commit 8e7a5f3541
14 changed files with 264 additions and 53 deletions

View file

@ -6,6 +6,7 @@ import com.fsck.k9.BuildConfig
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
import com.fsck.k9.backend.imap.SystemAlarmManager
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import org.koin.core.qualifier.named
import org.koin.dsl.module
@ -34,6 +35,7 @@ val backendsModule = module {
single<IdleRefreshManager> { BackendIdleRefreshManager(alarmManager = get()) }
single { Pop3BackendFactory(get(), get()) }
single(named("ClientIdAppName")) { BuildConfig.CLIENT_ID_APP_NAME }
single<OAuth2TokenProviderFactory> { RealOAuth2TokenProviderFactory(context = get()) }
developmentModuleAdditions()
}

View file

@ -0,0 +1,14 @@
package com.fsck.k9.backends
import android.content.Context
import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
class RealOAuth2TokenProviderFactory(
private val context: Context,
) : OAuth2TokenProviderFactory {
override fun create(authStateStorage: AuthStateStorage): OAuth2TokenProvider {
return RealOAuth2TokenProvider(context, authStateStorage)
}
}

View file

@ -55,7 +55,7 @@ val featureAccountSetupModule: Module = module {
ValidateServerSettings(
imapValidator = ImapServerSettingsValidator(
trustedSocketFactory = get(),
oAuth2TokenProvider = null, // TODO
oAuth2TokenProviderFactory = get(),
clientIdAppName = "null",
),
pop3Validator = Pop3ServerSettingsValidator(
@ -63,7 +63,7 @@ val featureAccountSetupModule: Module = module {
),
smtpValidator = SmtpServerSettingsValidator(
trustedSocketFactory = get(),
oAuth2TokenProvider = null, // TODO
oAuth2TokenProviderFactory = get(),
),
)
}

View file

@ -17,9 +17,9 @@ internal class ValidateServerSettings(
override suspend fun execute(settings: ServerSettings): ServerSettingsValidationResult =
withContext(coroutineDispatcher) {
return@withContext when (settings.type) {
"imap" -> imapValidator.checkServerSettings(settings)
"pop3" -> pop3Validator.checkServerSettings(settings)
"smtp" -> smtpValidator.checkServerSettings(settings)
"imap" -> imapValidator.checkServerSettings(settings, authStateStorage = null)
"pop3" -> pop3Validator.checkServerSettings(settings, authStateStorage = null)
"smtp" -> smtpValidator.checkServerSettings(settings, authStateStorage = null)
else -> {
throw IllegalArgumentException("Unsupported server type: ${settings.type}")
}

View file

@ -9,6 +9,8 @@ import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContrac
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import okhttp3.OkHttpClient
import org.junit.Test
@ -37,6 +39,14 @@ class AccountSetupModuleKtTest : KoinTest {
AccountCreator { _ -> AccountCreatorResult.Success("accountUuid") }
}
single<OAuthConfigurationFactory> { OAuthConfigurationFactory { emptyMap() } }
single<OAuth2TokenProviderFactory> {
OAuth2TokenProviderFactory { _ ->
object : OAuth2TokenProvider {
override fun getToken(timeoutMillis: Long) = TODO()
override fun invalidateToken() = TODO()
}
}
}
}
@Test

View file

@ -15,9 +15,9 @@ class ValidateServerSettingsTest {
@Test
fun `should check with imap validator when protocol is imap`() = runTest {
val testSubject = ValidateServerSettings(
imapValidator = { ServerSettingsValidationResult.Success },
pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
imapValidator = { _, _ -> ServerSettingsValidationResult.Success },
pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
)
val result = testSubject.execute(IMAP_SERVER_SETTINGS)
@ -29,9 +29,9 @@ class ValidateServerSettingsTest {
fun `should check with imap validator when protocol is imap and return failure`() = runTest {
val failure = ServerSettingsValidationResult.ServerError("Failed")
val testSubject = ValidateServerSettings(
imapValidator = { failure },
pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
imapValidator = { _, _ -> failure },
pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
)
val result = testSubject.execute(IMAP_SERVER_SETTINGS)
@ -42,9 +42,9 @@ class ValidateServerSettingsTest {
@Test
fun `should check with pop3 validator when protocol is pop3`() = runTest {
val testSubject = ValidateServerSettings(
imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { ServerSettingsValidationResult.Success },
smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { _, _ -> ServerSettingsValidationResult.Success },
smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
)
val result = testSubject.execute(POP3_SERVER_SETTINGS)
@ -56,9 +56,9 @@ class ValidateServerSettingsTest {
fun `should check with pop3 validator when protocol is pop3 and return failure`() = runTest {
val failure = ServerSettingsValidationResult.ServerError("Failed POP3")
val testSubject = ValidateServerSettings(
imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { failure },
smtpValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { _, _ -> failure },
smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) },
)
val result = testSubject.execute(POP3_SERVER_SETTINGS)
@ -69,9 +69,9 @@ class ValidateServerSettingsTest {
@Test
fun `should check with smtp validator when protocol is smtp`() = runTest {
val testSubject = ValidateServerSettings(
imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { ServerSettingsValidationResult.Success },
imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { _, _ -> ServerSettingsValidationResult.Success },
)
val result = testSubject.execute(SMTP_SERVER_SETTINGS)
@ -83,9 +83,9 @@ class ValidateServerSettingsTest {
fun `should check with smtp validator when protocol is smtp and return failure`() = runTest {
val failure = ServerSettingsValidationResult.ServerError("Failed SMTP")
val testSubject = ValidateServerSettings(
imapValidator = { ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { failure },
imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) },
pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) },
smtpValidator = { _, _ -> failure },
)
val result = testSubject.execute(SMTP_SERVER_SETTINGS)

View file

@ -0,0 +1,9 @@
package com.fsck.k9.mail.oauth
/**
* Creates an instance of [OAuth2TokenProvider] that uses a given [AuthStateStorage] to retrieve and store the
* (implementation-specific) authorization state.
*/
fun interface OAuth2TokenProviderFactory {
fun create(authStateStorage: AuthStateStorage): OAuth2TokenProvider
}

View file

@ -1,10 +1,14 @@
package com.fsck.k9.mail.server
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.AuthStateStorage
/**
* Validate [ServerSettings] by trying to connect to the server and log in.
*/
fun interface ServerSettingsValidator {
fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult
fun checkServerSettings(
serverSettings: ServerSettings,
authStateStorage: AuthStateStorage?,
): ServerSettingsValidationResult
}

View file

@ -4,7 +4,9 @@ import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.server.ServerSettingsValidator
import com.fsck.k9.mail.ssl.TrustedSocketFactory
@ -12,17 +14,21 @@ import java.io.IOException
class ImapServerSettingsValidator(
private val trustedSocketFactory: TrustedSocketFactory,
private val oAuth2TokenProvider: OAuth2TokenProvider?,
private val oAuth2TokenProviderFactory: OAuth2TokenProviderFactory?,
private val clientIdAppName: String,
) : ServerSettingsValidator {
@Suppress("TooGenericExceptionCaught")
override fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult {
override fun checkServerSettings(
serverSettings: ServerSettings,
authStateStorage: AuthStateStorage?,
): ServerSettingsValidationResult {
val config = object : ImapStoreConfig {
override val logLabel = "check"
override fun isSubscribedFoldersOnly() = false
override fun clientIdAppName() = clientIdAppName
}
val oAuth2TokenProvider = createOAuth2TokenProviderOrNull(authStateStorage)
val store = RealImapStore(serverSettings, config, trustedSocketFactory, oAuth2TokenProvider)
return try {
@ -48,4 +54,10 @@ class ImapServerSettingsValidator(
ServerSettingsValidationResult.UnknownError(e)
}
}
private fun createOAuth2TokenProviderOrNull(authStateStorage: AuthStateStorage?): OAuth2TokenProvider? {
return authStateStorage?.let {
oAuth2TokenProviderFactory?.create(it)
}
}
}

View file

@ -11,6 +11,8 @@ import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.helpers.FakeTrustManager
import com.fsck.k9.mail.helpers.SimpleTrustedSocketFactory
import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.store.imap.mockserver.MockImapServer
import java.net.UnknownHostException
@ -18,19 +20,22 @@ import kotlin.test.Test
private const val USERNAME = "user"
private const val PASSWORD = "password"
private const val AUTHORIZATION_STATE = "auth state"
private const val AUTHORIZATION_TOKEN = "auth-token"
private val CLIENT_CERTIFICATE_ALIAS: String? = null
private const val CLIENT_ID = "clientId"
class ImapServerSettingsValidatorTest {
private val fakeTrustManager = FakeTrustManager()
private val trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager)
private val serverSettingsValidator = ImapServerSettingsValidator(
trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager),
oAuth2TokenProvider = null,
trustedSocketFactory = trustedSocketFactory,
oAuth2TokenProviderFactory = null,
clientIdAppName = CLIENT_ID,
)
@Test
fun `valid server settings should return Success`() {
fun `valid server settings with password should return Success`() {
val server = startServer {
output("* OK IMAP4rev1 server ready")
expect("1 CAPABILITY")
@ -64,7 +69,56 @@ class ImapServerSettingsValidatorTest {
),
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.Success>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `valid server settings with OAuth should return Success`() {
val serverSettingsValidator = ImapServerSettingsValidator(
trustedSocketFactory = trustedSocketFactory,
oAuth2TokenProviderFactory = { authStateStorage ->
assertThat(authStateStorage.getAuthorizationState()).isEqualTo(AUTHORIZATION_STATE)
FakeOAuth2TokenProvider()
},
clientIdAppName = CLIENT_ID,
)
val server = startServer {
output("* OK IMAP4rev1 server ready")
expect("1 CAPABILITY")
output("* CAPABILITY IMAP4rev1 SASL-IR AUTH=PLAIN AUTH=OAUTHBEARER")
output("1 OK CAPABILITY Completed")
expect("2 AUTHENTICATE OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIGF1dGgtdG9rZW4BAQ==")
output("2 OK [CAPABILITY IMAP4rev1 SASL-IR AUTH=PLAIN AUTH=OAUTHBEARER NAMESPACE ID] LOGIN completed")
expect("3 ID (\"name\" \"$CLIENT_ID\")")
output("* ID NIL")
output("3 OK ID completed")
expect("4 NAMESPACE")
output("* NAMESPACE ((\"\" \"/\")) NIL NIL")
output("4 OK command completed")
}
val serverSettings = ServerSettings(
type = "imap",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.NONE,
authenticationType = AuthType.XOAUTH2,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
extra = ImapStoreSettings.createExtra(
autoDetectNamespace = true,
pathPrefix = null,
useCompression = false,
sendClientId = true,
),
)
val authStateStorage = FakeAuthStateStorage(authorizationState = AUTHORIZATION_STATE)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.Success>()
server.verifyConnectionClosed()
@ -93,7 +147,7 @@ class ImapServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.AuthenticationError>()
.prop(ServerSettingsValidationResult.AuthenticationError::serverMessage).isEqualTo("Authentication failed")
@ -120,7 +174,7 @@ class ImapServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.ServerError>()
server.verifyConnectionClosed()
@ -150,7 +204,7 @@ class ImapServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.CertificateError>()
.prop(ServerSettingsValidationResult.CertificateError::certificateChain).hasSize(1)
@ -171,7 +225,7 @@ class ImapServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.NetworkError>()
.prop(ServerSettingsValidationResult.NetworkError::exception)
@ -192,7 +246,7 @@ class ImapServerSettingsValidatorTest {
)
assertFailure {
serverSettingsValidator.checkServerSettings(serverSettings)
serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
}.isInstanceOf<IllegalArgumentException>()
}
@ -203,3 +257,25 @@ class ImapServerSettingsValidatorTest {
}
}
}
class FakeOAuth2TokenProvider : OAuth2TokenProvider {
override fun getToken(timeoutMillis: Long): String {
return AUTHORIZATION_TOKEN
}
override fun invalidateToken() {
throw UnsupportedOperationException("not implemented")
}
}
class FakeAuthStateStorage(
private var authorizationState: String? = null,
) : AuthStateStorage {
override fun getAuthorizationState(): String? {
return authorizationState
}
override fun updateAuthorizationState(authorizationState: String?) {
this.authorizationState = authorizationState
}
}

View file

@ -4,6 +4,7 @@ import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.server.ServerSettingsValidator
import com.fsck.k9.mail.ssl.TrustedSocketFactory
@ -14,7 +15,10 @@ class Pop3ServerSettingsValidator(
) : ServerSettingsValidator {
@Suppress("TooGenericExceptionCaught")
override fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult {
override fun checkServerSettings(
serverSettings: ServerSettings,
authStateStorage: AuthStateStorage?,
): ServerSettingsValidationResult {
val store = Pop3Store(serverSettings, trustedSocketFactory)
return try {

View file

@ -54,7 +54,7 @@ class Pop3ServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.Success>()
server.verifyConnectionClosed()
@ -90,7 +90,7 @@ class Pop3ServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.AuthenticationError>()
.prop(ServerSettingsValidationResult.AuthenticationError::serverMessage)
@ -116,7 +116,7 @@ class Pop3ServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.ServerError>()
.prop(ServerSettingsValidationResult.ServerError::serverMessage)
@ -151,7 +151,7 @@ class Pop3ServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.CertificateError>()
.prop(ServerSettingsValidationResult.CertificateError::certificateChain).hasSize(1)
@ -172,7 +172,7 @@ class Pop3ServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.NetworkError>()
.prop(ServerSettingsValidationResult.NetworkError::exception)
@ -193,7 +193,7 @@ class Pop3ServerSettingsValidatorTest {
)
assertFailure {
serverSettingsValidator.checkServerSettings(serverSettings)
serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
}.isInstanceOf<IllegalArgumentException>()
}

View file

@ -4,7 +4,9 @@ import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.server.ServerSettingsValidator
import com.fsck.k9.mail.ssl.TrustedSocketFactory
@ -12,11 +14,15 @@ import java.io.IOException
class SmtpServerSettingsValidator(
private val trustedSocketFactory: TrustedSocketFactory,
private val oAuth2TokenProvider: OAuth2TokenProvider?,
private val oAuth2TokenProviderFactory: OAuth2TokenProviderFactory?,
) : ServerSettingsValidator {
@Suppress("TooGenericExceptionCaught")
override fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult {
override fun checkServerSettings(
serverSettings: ServerSettings,
authStateStorage: AuthStateStorage?,
): ServerSettingsValidationResult {
val oAuth2TokenProvider = createOAuth2TokenProviderOrNull(authStateStorage)
val smtpTransport = SmtpTransport(serverSettings, trustedSocketFactory, oAuth2TokenProvider)
return try {
@ -42,4 +48,10 @@ class SmtpServerSettingsValidator(
ServerSettingsValidationResult.UnknownError(e)
}
}
private fun createOAuth2TokenProviderOrNull(authStateStorage: AuthStateStorage?): OAuth2TokenProvider? {
return authStateStorage?.let {
oAuth2TokenProviderFactory?.create(it)
}
}
}

View file

@ -11,6 +11,8 @@ import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.helpers.FakeTrustManager
import com.fsck.k9.mail.helpers.SimpleTrustedSocketFactory
import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.transport.mockServer.MockSmtpServer
import java.net.UnknownHostException
@ -18,17 +20,20 @@ import kotlin.test.Test
private const val USERNAME = "user"
private const val PASSWORD = "password"
private const val AUTHORIZATION_STATE = "auth state"
private const val AUTHORIZATION_TOKEN = "auth-token"
private val CLIENT_CERTIFICATE_ALIAS: String? = null
class SmtpServerSettingsValidatorTest {
private val fakeTrustManager = FakeTrustManager()
private val trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager)
private val serverSettingsValidator = SmtpServerSettingsValidator(
trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager),
oAuth2TokenProvider = null,
trustedSocketFactory = trustedSocketFactory,
oAuth2TokenProviderFactory = null,
)
@Test
fun `valid server settings should return Success`() {
fun `valid server settings with password should return Success`() {
val server = MockSmtpServer().apply {
output("220 localhost Simple Mail Transfer Service Ready")
expect("EHLO [127.0.0.1]")
@ -53,7 +58,48 @@ class SmtpServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.Success>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `valid server settings with OAuth should return Success`() {
val serverSettingsValidator = SmtpServerSettingsValidator(
trustedSocketFactory = trustedSocketFactory,
oAuth2TokenProviderFactory = { authStateStorage ->
assertThat(authStateStorage.getAuthorizationState()).isEqualTo(AUTHORIZATION_STATE)
FakeOAuth2TokenProvider()
},
)
val server = MockSmtpServer().apply {
output("220 localhost Simple Mail Transfer Service Ready")
expect("EHLO [127.0.0.1]")
output("250-localhost Hello client.localhost")
output("250-ENHANCEDSTATUSCODES")
output("250-AUTH PLAIN LOGIN OAUTHBEARER")
output("250 HELP")
expect("AUTH OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIGF1dGgtdG9rZW4BAQ==")
output("235 2.7.0 Authentication successful")
expect("QUIT")
closeConnection()
}
server.start()
val serverSettings = ServerSettings(
type = "smtp",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.NONE,
authenticationType = AuthType.XOAUTH2,
username = USERNAME,
password = null,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val authStateStorage = FakeAuthStateStorage(authorizationState = AUTHORIZATION_STATE)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.Success>()
server.verifyConnectionClosed()
@ -86,7 +132,7 @@ class SmtpServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.AuthenticationError>()
.prop(ServerSettingsValidationResult.AuthenticationError::serverMessage).isEqualTo("Authentication failed")
@ -112,7 +158,7 @@ class SmtpServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.ServerError>()
server.verifyConnectionClosed()
@ -144,7 +190,7 @@ class SmtpServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.CertificateError>()
.prop(ServerSettingsValidationResult.CertificateError::certificateChain).hasSize(1)
@ -165,7 +211,7 @@ class SmtpServerSettingsValidatorTest {
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.NetworkError>()
.prop(ServerSettingsValidationResult.NetworkError::exception)
@ -186,7 +232,29 @@ class SmtpServerSettingsValidatorTest {
)
assertFailure {
serverSettingsValidator.checkServerSettings(serverSettings)
serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
}.isInstanceOf<IllegalArgumentException>()
}
}
class FakeOAuth2TokenProvider : OAuth2TokenProvider {
override fun getToken(timeoutMillis: Long): String {
return AUTHORIZATION_TOKEN
}
override fun invalidateToken() {
throw UnsupportedOperationException("not implemented")
}
}
class FakeAuthStateStorage(
private var authorizationState: String? = null,
) : AuthStateStorage {
override fun getAuthorizationState(): String? {
return authorizationState
}
override fun updateAuthorizationState(authorizationState: String?) {
this.authorizationState = authorizationState
}
}