Merge pull request #124 from kirmanak/base-url-fallback

Fallback to HTTP if automatically chosen HTTPS fails
This commit is contained in:
Kirill Kamakin
2022-12-22 19:09:43 +01:00
committed by GitHub
5 changed files with 66 additions and 15 deletions

View File

@@ -16,8 +16,8 @@ plugins {
android { android {
defaultConfig { defaultConfig {
applicationId = "gq.kirmanak.mealient" applicationId = "gq.kirmanak.mealient"
versionCode = 25 versionCode = 26
versionName = "0.3.10" versionName = "0.3.11"
testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner" testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner"
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true") testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
} }

View File

@@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -27,30 +28,40 @@ class BaseURLViewModel @Inject constructor(
fun saveBaseUrl(baseURL: String) { fun saveBaseUrl(baseURL: String) {
logger.v { "saveBaseUrl() called with: baseURL = $baseURL" } logger.v { "saveBaseUrl() called with: baseURL = $baseURL" }
_uiState.value = OperationUiState.Progress() _uiState.value = OperationUiState.Progress()
val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) } viewModelScope.launch { checkBaseURL(baseURL) }
var url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
url = url.trimStart().trimEnd { it == '/' || it.isWhitespace() }
viewModelScope.launch { checkBaseURL(url) }
} }
private suspend fun checkBaseURL(baseURL: String) { private suspend fun checkBaseURL(baseURL: String) {
logger.v { "checkBaseURL() called with: baseURL = $baseURL" } logger.v { "checkBaseURL() called with: baseURL = $baseURL" }
if (baseURL == serverInfoRepo.getUrl()) {
val hasPrefix = listOf("http://", "https://").any { baseURL.startsWith(it) }
val urlWithPrefix = baseURL.takeIf { hasPrefix } ?: "https://%s".format(baseURL)
val url = urlWithPrefix.trimEnd { it == '/' }
logger.d { "checkBaseURL: Created URL = \"$url\", with prefix = \"$urlWithPrefix\"" }
if (url == serverInfoRepo.getUrl()) {
logger.d { "checkBaseURL: new URL matches current" } logger.d { "checkBaseURL: new URL matches current" }
_uiState.value = OperationUiState.fromResult(Result.success(Unit)) _uiState.value = OperationUiState.fromResult(Result.success(Unit))
return return
} }
val result = serverInfoRepo.tryBaseURL(baseURL)
val result: Result<Unit> = serverInfoRepo.tryBaseURL(url).recoverCatching {
logger.e(it) { "checkBaseURL: trying to recover, had prefix = $hasPrefix" }
if (hasPrefix || it is NetworkError.NotMealie) {
throw it
} else {
val unencryptedUrl = url.replace("https", "http")
serverInfoRepo.tryBaseURL(unencryptedUrl).getOrThrow()
}
}
if (result.isSuccess) { if (result.isSuccess) {
authRepo.logout() authRepo.logout()
recipeRepo.clearLocalData() recipeRepo.clearLocalData()
} }
logger.i { "checkBaseURL: result is $result" } logger.i { "checkBaseURL: result is $result" }
_uiState.value = OperationUiState.fromResult(result) _uiState.value = OperationUiState.fromResult(result)
} }
companion object {
private val ALLOWED_PREFIXES = listOf("http://", "https://")
private const val WITH_PREFIX_FORMAT = "https://%s"
}
} }

View File

@@ -4,17 +4,20 @@ import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.* import kotlinx.coroutines.test.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.IOException import java.io.IOException
import javax.net.ssl.SSLHandshakeException
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class BaseURLViewModelTest : BaseUnitTest() { class BaseURLViewModelTest : BaseUnitTest() {
@@ -105,4 +108,35 @@ class BaseURLViewModelTest : BaseUnitTest() {
advanceUntilIdle() advanceUntilIdle()
assertThat(subject.uiState.value).isInstanceOf(OperationUiState.Failure::class.java) assertThat(subject.uiState.value).isInstanceOf(OperationUiState.Failure::class.java)
} }
@Test
fun `when saving base url with no prefix and https throws expect http attempt`() = runTest {
coEvery { serverInfoRepo.getUrl() } returns null
val err = NetworkError.MalformedUrl(SSLHandshakeException("test"))
coEvery { serverInfoRepo.tryBaseURL("https://test") } returns Result.failure(err)
coEvery { serverInfoRepo.tryBaseURL("http://test") } returns Result.success(Unit)
subject.saveBaseUrl("test")
coVerifyOrder {
serverInfoRepo.tryBaseURL("https://test")
serverInfoRepo.tryBaseURL("http://test")
}
}
@Test
fun `when saving base url with no prefix and https throws non ssl expect no http`() = runTest {
coEvery { serverInfoRepo.getUrl() } returns null
val err = NetworkError.NotMealie(IOException())
coEvery { serverInfoRepo.tryBaseURL("https://test") } returns Result.failure(err)
subject.saveBaseUrl("test")
coVerify(inverse = true) { serverInfoRepo.tryBaseURL("http://test") }
}
@Test
fun `when saving base url with https prefix and https throws expect no http call`() = runTest {
coEvery { serverInfoRepo.getUrl() } returns null
val err = NetworkError.MalformedUrl(SSLHandshakeException("test"))
coEvery { serverInfoRepo.tryBaseURL("https://test") } returns Result.failure(err)
subject.saveBaseUrl("https://test")
coVerify(inverse = true) { serverInfoRepo.tryBaseURL("http://test") }
}
} }

View File

@@ -1,6 +1,6 @@
package gq.kirmanak.mealient.datasource package gq.kirmanak.mealient.datasource
sealed class NetworkError(cause: Throwable) : RuntimeException(cause) { sealed class NetworkError(cause: Throwable) : RuntimeException(cause.message, cause) {
class Unauthorized(cause: Throwable) : NetworkError(cause) class Unauthorized(cause: Throwable) : NetworkError(cause)
class NoServerConnection(cause: Throwable) : NetworkError(cause) class NoServerConnection(cause: Throwable) : NetworkError(cause)
class NotMealie(cause: Throwable) : NetworkError(cause) class NotMealie(cause: Throwable) : NetworkError(cause)

View File

@@ -4,7 +4,7 @@ import gq.kirmanak.mealient.datasource.LocalInterceptor
import gq.kirmanak.mealient.datasource.ServerUrlProvider import gq.kirmanak.mealient.datasource.ServerUrlProvider
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.io.IOException import java.io.IOException
@@ -37,6 +37,12 @@ class BaseUrlInterceptor @Inject constructor(
} }
private fun getBaseUrl() = runBlocking { private fun getBaseUrl() = runBlocking {
serverUrlProvider.getUrl()?.toHttpUrlOrNull() ?: throw IOException("Base URL is unknown") val url = serverUrlProvider.getUrl() ?: throw IOException("Base URL is unknown")
url.runCatching {
toHttpUrl()
}.fold(
onSuccess = { it },
onFailure = { throw IOException(it.message, it) },
)
} }
} }