Merge pull request #124 from kirmanak/base-url-fallback
Fallback to HTTP if automatically chosen HTTPS fails
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user