diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt index 2d124aa6..2b42d07e 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt @@ -10,8 +10,9 @@ import dev.hotwire.turbo.demo.util.SIGN_IN_URL import dev.hotwire.turbo.fragments.TurboWebFragment import dev.hotwire.turbo.nav.TurboNavGraphDestination import dev.hotwire.turbo.views.TurboWebView +import dev.hotwire.turbo.errors.HttpError import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE -import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.errors.TurboVisitError import dev.hotwire.turbo.visit.TurboVisitOptions @TurboNavGraphDestination(uri = "turbo://fragment/web") @@ -60,9 +61,10 @@ open class WebFragment : TurboWebFragment(), NavDestination { } override fun onVisitErrorReceived(location: String, error: TurboVisitError) { - when (error.code) { - 401 -> navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE)) - else -> super.onVisitErrorReceived(location, error) + if (error is HttpError.ClientError.Unauthorized) { + navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE)) + } else { + super.onVisitErrorReceived(location, error) } } diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt index b40d577f..bee4a2db 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt @@ -7,7 +7,7 @@ import android.view.View import android.view.ViewGroup import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.nav.TurboNavGraphDestination -import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.errors.TurboVisitError @TurboNavGraphDestination(uri = "turbo://fragment/web/home") class WebHomeFragment : WebFragment() { diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt index 9591f25f..f7e4e3bb 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt @@ -21,7 +21,7 @@ import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit import dev.hotwire.turbo.visit.TurboVisitAction -import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.errors.TurboVisitError import dev.hotwire.turbo.visit.TurboVisitOptions import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/errors/HttpError.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/HttpError.kt new file mode 100644 index 00000000..5fdf8956 --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/HttpError.kt @@ -0,0 +1,145 @@ +package dev.hotwire.turbo.errors + +import android.webkit.WebResourceResponse + +sealed interface HttpError : TurboVisitError { + val statusCode: Int + val reasonPhrase: String? + + sealed interface ClientError : HttpError { + data object BadRequest : ClientError { + override val statusCode = 400 + override val reasonPhrase = "Bad Request" + } + + data object Unauthorized : ClientError { + override val statusCode = 401 + override val reasonPhrase = "Unauthorized" + } + + data object Forbidden : ClientError { + override val statusCode = 403 + override val reasonPhrase = "Forbidden" + } + + data object NotFound : ClientError { + override val statusCode = 404 + override val reasonPhrase = "Not Found" + } + + data object MethodNotAllowed : ClientError { + override val statusCode = 405 + override val reasonPhrase = "Method Not Allowed" + } + + data object NotAccessible : ClientError { + override val statusCode = 406 + override val reasonPhrase = "Not Accessible" + } + + data object ProxyAuthenticationRequired : ClientError { + override val statusCode = 407 + override val reasonPhrase = "Proxy Authentication Required" + } + + data object RequestTimeout : ClientError { + override val statusCode = 408 + override val reasonPhrase = "Request Timeout" + } + + data object Conflict : ClientError { + override val statusCode = 409 + override val reasonPhrase = "Conflict" + } + + data object MisdirectedRequest : ClientError { + override val statusCode = 421 + override val reasonPhrase = "Misdirected Request" + } + + data object UnprocessableEntity : ClientError { + override val statusCode = 422 + override val reasonPhrase = "Unprocessable Entity" + } + + data object PreconditionRequired : ClientError { + override val statusCode = 428 + override val reasonPhrase = "Precondition Required" + } + + data object TooManyRequests : ClientError { + override val statusCode = 429 + override val reasonPhrase = "Too Many Requests" + } + + data class Other( + override val statusCode: Int, + override val reasonPhrase: String? + ) : ClientError + } + + sealed interface ServerError : HttpError { + data object InternalServerError : ServerError { + override val statusCode = 500 + override val reasonPhrase = "Internal Server Error" + } + + data object NotImplemented : ServerError { + override val statusCode = 501 + override val reasonPhrase = "Not Implemented" + } + + data object BadGateway : ServerError { + override val statusCode = 502 + override val reasonPhrase = "Bad Gateway" + } + + data object ServiceUnavailable : ServerError { + override val statusCode = 503 + override val reasonPhrase = "Service Unavailable" + } + + data object GatewayTimeout : ServerError { + override val statusCode = 504 + override val reasonPhrase = "Gateway Timeout" + } + + data object HttpVersionNotSupported : ServerError { + override val statusCode = 505 + override val reasonPhrase = "Http Version Not Supported" + } + + data class Other( + override val statusCode: Int, + override val reasonPhrase: String? + ) : ServerError + } + + companion object { + fun from(errorResponse: WebResourceResponse): HttpError { + return getError(errorResponse.statusCode, errorResponse.reasonPhrase) + } + + fun from(statusCode: Int): HttpError { + return getError(statusCode, null) + } + + private fun getError(statusCode: Int, reasonPhrase: String?): HttpError { + if (statusCode in 400..499) { + return ClientError::class.sealedSubclasses + .mapNotNull { it.objectInstance } + .firstOrNull { it.statusCode == statusCode } + ?: ClientError.Other(statusCode, reasonPhrase) + } + + if (statusCode in 500..599) { + return ServerError::class.sealedSubclasses + .map { it.objectInstance } + .firstOrNull { it?.statusCode == statusCode } + ?: ServerError.Other(statusCode, reasonPhrase) + } + + throw IllegalArgumentException("Invalid HTTP error status code: $statusCode") + } + } +} diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/errors/LoadError.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/LoadError.kt new file mode 100644 index 00000000..73fc829c --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/LoadError.kt @@ -0,0 +1,6 @@ +package dev.hotwire.turbo.errors + +sealed interface LoadError : TurboVisitError { + data object NotPresent : LoadError + data object NotReady : LoadError +} diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/errors/TurboVisitError.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/TurboVisitError.kt new file mode 100644 index 00000000..c84ee42b --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/TurboVisitError.kt @@ -0,0 +1,3 @@ +package dev.hotwire.turbo.errors + +sealed interface TurboVisitError diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebError.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebError.kt new file mode 100644 index 00000000..dc8ba5e8 --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebError.kt @@ -0,0 +1,125 @@ +package dev.hotwire.turbo.errors + +import androidx.webkit.WebResourceErrorCompat +import androidx.webkit.WebViewClientCompat +import androidx.webkit.WebViewFeature +import androidx.webkit.WebViewFeature.isFeatureSupported + +sealed interface WebError : TurboVisitError { + val errorCode: Int + val description: String? + + data object Unknown : WebError { + override val errorCode = WebViewClientCompat.ERROR_UNKNOWN + override val description = "Unknown" + } + + data object HostLookup : WebError { + override val errorCode = WebViewClientCompat.ERROR_HOST_LOOKUP + override val description = "Host Lookup" + } + + data object UnsupportedAuthScheme : WebError { + override val errorCode = WebViewClientCompat.ERROR_UNSUPPORTED_AUTH_SCHEME + override val description = "Unsupported Auth Scheme" + } + + data object Authentication : WebError { + override val errorCode = WebViewClientCompat.ERROR_AUTHENTICATION + override val description = "Authentication" + } + + data object ProxyAuthentication : WebError { + override val errorCode = WebViewClientCompat.ERROR_PROXY_AUTHENTICATION + override val description = "Proxy Authentication" + } + + data object Connect : WebError { + override val errorCode = WebViewClientCompat.ERROR_CONNECT + override val description = "Connect" + } + + data object IO : WebError { + override val errorCode = WebViewClientCompat.ERROR_IO + override val description = "IO" + } + + data object Timeout : WebError { + override val errorCode = WebViewClientCompat.ERROR_TIMEOUT + override val description = "Timeout" + } + + data object RedirectLoop : WebError { + override val errorCode = WebViewClientCompat.ERROR_REDIRECT_LOOP + override val description = "Redirect Loop" + } + + data object UnsupportedScheme : WebError { + override val errorCode = WebViewClientCompat.ERROR_UNSUPPORTED_SCHEME + override val description = "Unsupported Scheme" + } + + data object FailedSslHandshake : WebError { + override val errorCode = WebViewClientCompat.ERROR_FAILED_SSL_HANDSHAKE + override val description = "Failed SSL Handshake" + } + + data object BadUrl : WebError { + override val errorCode = WebViewClientCompat.ERROR_BAD_URL + override val description = "Bad URL" + } + + data object File : WebError { + override val errorCode = WebViewClientCompat.ERROR_FILE + override val description = "File" + } + + data object FileNotFound : WebError { + override val errorCode = WebViewClientCompat.ERROR_FILE_NOT_FOUND + override val description = "File Not Found" + } + + data object TooManyRequests : WebError { + override val errorCode = WebViewClientCompat.ERROR_TOO_MANY_REQUESTS + override val description = "Too Many Requests" + } + + data object UnsafeResource : WebError { + override val errorCode = WebViewClientCompat.ERROR_UNSAFE_RESOURCE + override val description = "Unsafe Resource" + } + + data class Other( + override val errorCode: Int, + override val description: String? + ) : WebError + + companion object { + fun from(error: WebResourceErrorCompat): WebError { + val errorCode = if (isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { + error.errorCode + } else { + 0 + } + + val description = if (isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) { + error.description.toString() + } else { + null + } + + return getError(errorCode, description) + } + + fun from(errorCode: Int): WebError { + return getError(errorCode, null) + } + + private fun getError(errorCode: Int, description: String?): WebError { + return WebError::class.sealedSubclasses + .mapNotNull { it.objectInstance } + .firstOrNull { it.errorCode == errorCode } + ?: Other(errorCode, description) + } + } +} diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebSslError.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebSslError.kt new file mode 100644 index 00000000..28d1d113 --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebSslError.kt @@ -0,0 +1,59 @@ +package dev.hotwire.turbo.errors + +import android.net.http.SslError + +sealed interface WebSslError : TurboVisitError { + val errorCode: Int + val description: String? + + data object NotYetValid : WebSslError { + override val errorCode = SslError.SSL_NOTYETVALID + override val description = "Not Yet Valid" + } + + data object Expired : WebSslError { + override val errorCode = SslError.SSL_EXPIRED + override val description = "Expired" + } + + data object IdMismatch : WebSslError { + override val errorCode = SslError.SSL_IDMISMATCH + override val description = "ID Mismatch" + } + + data object Untrusted : WebSslError { + override val errorCode = SslError.SSL_UNTRUSTED + override val description = "Untrusted" + } + + data object DateInvalid : WebSslError { + override val errorCode = SslError.SSL_DATE_INVALID + override val description = "Date Invalid" + } + + data object Invalid : WebSslError { + override val errorCode = SslError.SSL_INVALID + override val description = "Invalid" + } + + data class Other(override val errorCode: Int) : WebSslError { + override val description = null + } + + companion object { + fun from(error: SslError): WebSslError { + return getError(error.primaryError) + } + + fun from(errorCode: Int): WebSslError { + return getError(errorCode) + } + + private fun getError(errorCode: Int): WebSslError { + return WebSslError::class.sealedSubclasses + .mapNotNull { it.objectInstance } + .firstOrNull { it.errorCode == errorCode } + ?: Other(errorCode) + } + } +} diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt index d2f35af1..d7c37811 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt @@ -13,7 +13,7 @@ import dev.hotwire.turbo.delegates.TurboWebFragmentDelegate import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebChromeClient -import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.errors.TurboVisitError /** * The base class from which all bottom sheet web fragments in a diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt index 7b36d7cd..28403616 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt @@ -13,7 +13,7 @@ import dev.hotwire.turbo.session.TurboSessionModalResult import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebChromeClient -import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.errors.TurboVisitError /** * The base class from which all web "standard" fragments (non-dialogs) in a diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt index 64458dd8..26ca5058 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt @@ -5,7 +5,7 @@ import android.webkit.HttpAuthHandler import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebChromeClient import dev.hotwire.turbo.views.TurboWebView -import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.errors.TurboVisitError /** * Callback interface to be implemented by a [TurboWebFragment], diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt index b4df2c6b..04af0d21 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt @@ -1,11 +1,9 @@ package dev.hotwire.turbo.session import android.annotation.SuppressLint -import android.annotation.TargetApi import android.content.Context import android.graphics.Bitmap import android.net.http.SslError -import android.os.Build import android.util.SparseArray import android.webkit.* import androidx.appcompat.app.AppCompatActivity @@ -17,14 +15,16 @@ import androidx.webkit.WebViewFeature.* import dev.hotwire.turbo.config.TurboPathConfiguration import dev.hotwire.turbo.config.screenshotsEnabled import dev.hotwire.turbo.delegates.TurboFileChooserDelegate +import dev.hotwire.turbo.errors.HttpError +import dev.hotwire.turbo.errors.LoadError +import dev.hotwire.turbo.errors.WebError +import dev.hotwire.turbo.errors.WebSslError import dev.hotwire.turbo.http.* import dev.hotwire.turbo.nav.TurboNavDestination import dev.hotwire.turbo.util.* import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit import dev.hotwire.turbo.visit.TurboVisitAction -import dev.hotwire.turbo.visit.TurboVisitError -import dev.hotwire.turbo.visit.TurboVisitErrorType import dev.hotwire.turbo.visit.TurboVisitOptions import kotlinx.coroutines.* import java.util.* @@ -285,11 +285,7 @@ class TurboSession internal constructor( */ @JavascriptInterface fun visitRequestFailedWithStatusCode(visitIdentifier: String, visitHasCachedSnapshot: Boolean, statusCode: Int) { - val visitError = TurboVisitError( - type = TurboVisitErrorType.HTTP_ERROR, - code = statusCode, - description = "Request failed" - ) + val visitError = HttpError.from(statusCode) logEvent( "visitRequestFailedWithStatusCode", @@ -464,7 +460,11 @@ class TurboSession internal constructor( if (!isReady) { reset() - visitRequestFailedWithStatusCode(visit.identifier, false, 0) + + val visitError = LoadError.NotReady + logEvent("turboIsNotReady", "error" to visitError) + + callback { it.requestFailedWithError(false, visitError) } return } @@ -485,11 +485,7 @@ class TurboSession internal constructor( */ @JavascriptInterface fun turboFailedToLoad() { - val visitError = TurboVisitError( - type = TurboVisitErrorType.LOAD_ERROR, - code = -1, - description = "Turbo failed to load" - ) + val visitError = LoadError.NotPresent logEvent("turboFailedToLoad", "error" to visitError) reset() @@ -761,16 +757,8 @@ class TurboSession internal constructor( override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceErrorCompat) { super.onReceivedError(view, request, error) - if (request.isForMainFrame && isFeatureSupported(WEB_RESOURCE_ERROR_GET_CODE)) { - val visitError = TurboVisitError( - type = TurboVisitErrorType.WEB_RESOURCE_ERROR, - code = error.errorCode, - description = if (isFeatureSupported(WEB_RESOURCE_ERROR_GET_DESCRIPTION)) { - error.description.toString() - } else { - null - } - ) + if (request.isForMainFrame) { + val visitError = WebError.from(error) logEvent("onReceivedError", "error" to visitError) reset() @@ -782,11 +770,7 @@ class TurboSession internal constructor( super.onReceivedHttpError(view, request, errorResponse) if (request.isForMainFrame) { - val visitError = TurboVisitError( - type = TurboVisitErrorType.HTTP_ERROR, - code = errorResponse.statusCode, - description = errorResponse.reasonPhrase - ) + val visitError = HttpError.from(errorResponse) logEvent("onReceivedHttpError", "error" to visitError) reset() @@ -798,10 +782,7 @@ class TurboSession internal constructor( super.onReceivedSslError(view, handler, error) handler.cancel() - val visitError = TurboVisitError( - type = TurboVisitErrorType.WEB_SSL_ERROR, - code = error.primaryError - ) + val visitError = WebSslError.from(error) logEvent("onReceivedSslError", "error" to visitError) reset() diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt index 5039600e..0bbaa01e 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt @@ -2,8 +2,7 @@ package dev.hotwire.turbo.session import android.webkit.HttpAuthHandler import dev.hotwire.turbo.nav.TurboNavDestination -import dev.hotwire.turbo.visit.TurboVisitError -import dev.hotwire.turbo.visit.TurboVisitErrorType +import dev.hotwire.turbo.errors.TurboVisitError import dev.hotwire.turbo.visit.TurboVisitOptions internal interface TurboSessionCallback { diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitError.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitError.kt deleted file mode 100644 index c0b44ce5..00000000 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitError.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.hotwire.turbo.visit - -data class TurboVisitError( - /** - * The [TurboVisitErrorType] type of error received. - */ - val type: TurboVisitErrorType, - - /** - * The error code associated with the [TurboVisitErrorType] type. - */ - val code: Int, - - /** - * The (optional) description of the error. - */ - val description: String? = null -) diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/errors/HttpErrorTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/errors/HttpErrorTest.kt new file mode 100644 index 00000000..7741a647 --- /dev/null +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/errors/HttpErrorTest.kt @@ -0,0 +1,62 @@ +package dev.hotwire.turbo.errors + +import android.os.Build +import dev.hotwire.turbo.BaseUnitTest +import dev.hotwire.turbo.errors.HttpError.ClientError +import dev.hotwire.turbo.errors.HttpError.ServerError +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O]) +class HttpErrorTest : BaseUnitTest() { + @Test + fun clientErrors() { + val errors = listOf( + 400 to ClientError.BadRequest, + 401 to ClientError.Unauthorized, + 403 to ClientError.Forbidden, + 404 to ClientError.NotFound, + 405 to ClientError.MethodNotAllowed, + 406 to ClientError.NotAccessible, + 407 to ClientError.ProxyAuthenticationRequired, + 408 to ClientError.RequestTimeout, + 409 to ClientError.Conflict, + 421 to ClientError.MisdirectedRequest, + 422 to ClientError.UnprocessableEntity, + 428 to ClientError.PreconditionRequired, + 429 to ClientError.TooManyRequests, + 430 to ClientError.Other(430, null), + 499 to ClientError.Other(499, null) + ) + + errors.forEach { + val error = HttpError.from(it.first) + assertThat(error).isEqualTo(it.second) + assertThat(error.statusCode).isEqualTo(it.first) + } + } + + @Test + fun serverErrors() { + val errors = listOf( + 500 to ServerError.InternalServerError, + 501 to ServerError.NotImplemented, + 502 to ServerError.BadGateway, + 503 to ServerError.ServiceUnavailable, + 504 to ServerError.GatewayTimeout, + 505 to ServerError.HttpVersionNotSupported, + 506 to ServerError.Other(506, null), + 599 to ServerError.Other(599, null) + ) + + errors.forEach { + val error = HttpError.from(it.first) + assertThat(error).isEqualTo(it.second) + assertThat(error.statusCode).isEqualTo(it.first) + } + } +} diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/errors/WebErrorTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/errors/WebErrorTest.kt new file mode 100644 index 00000000..984daa72 --- /dev/null +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/errors/WebErrorTest.kt @@ -0,0 +1,44 @@ +package dev.hotwire.turbo.errors + +import android.os.Build +import androidx.webkit.WebViewClientCompat +import dev.hotwire.turbo.BaseUnitTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O]) +class WebErrorTest : BaseUnitTest() { + @Test + fun webErrors() { + val errors = listOf( + WebViewClientCompat.ERROR_UNKNOWN to WebError.Unknown, + WebViewClientCompat.ERROR_HOST_LOOKUP to WebError.HostLookup, + WebViewClientCompat.ERROR_UNSUPPORTED_AUTH_SCHEME to WebError.UnsupportedAuthScheme, + WebViewClientCompat.ERROR_AUTHENTICATION to WebError.Authentication, + WebViewClientCompat.ERROR_PROXY_AUTHENTICATION to WebError.ProxyAuthentication, + WebViewClientCompat.ERROR_CONNECT to WebError.Connect, + WebViewClientCompat.ERROR_IO to WebError.IO, + WebViewClientCompat.ERROR_TIMEOUT to WebError.Timeout, + WebViewClientCompat.ERROR_REDIRECT_LOOP to WebError.RedirectLoop, + WebViewClientCompat.ERROR_UNSUPPORTED_SCHEME to WebError.UnsupportedScheme, + WebViewClientCompat.ERROR_FAILED_SSL_HANDSHAKE to WebError.FailedSslHandshake, + WebViewClientCompat.ERROR_BAD_URL to WebError.BadUrl, + WebViewClientCompat.ERROR_FILE to WebError.File, + WebViewClientCompat.ERROR_FILE_NOT_FOUND to WebError.FileNotFound, + WebViewClientCompat.ERROR_TOO_MANY_REQUESTS to WebError.TooManyRequests, + WebViewClientCompat.ERROR_UNSAFE_RESOURCE to WebError.UnsafeResource, + -17 to WebError.Other(-17, null), + 1 to WebError.Other(1, null), + ) + + errors.forEach { + val error = WebError.from(it.first) + assertThat(error).isEqualTo(it.second) + assertThat(error.errorCode).isEqualTo(it.first) + } + } +} diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/errors/WebSslErrorTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/errors/WebSslErrorTest.kt new file mode 100644 index 00000000..72010fe8 --- /dev/null +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/errors/WebSslErrorTest.kt @@ -0,0 +1,34 @@ +package dev.hotwire.turbo.errors + +import android.net.http.SslError +import android.os.Build +import dev.hotwire.turbo.BaseUnitTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O]) +class WebSslErrorTest : BaseUnitTest() { + @Test + fun sslErrors() { + val errors = listOf( + SslError.SSL_NOTYETVALID to WebSslError.NotYetValid, + SslError.SSL_EXPIRED to WebSslError.Expired, + SslError.SSL_IDMISMATCH to WebSslError.IdMismatch, + SslError.SSL_UNTRUSTED to WebSslError.Untrusted, + SslError.SSL_DATE_INVALID to WebSslError.DateInvalid, + SslError.SSL_INVALID to WebSslError.Invalid, + -1 to WebSslError.Other(-1), + 6 to WebSslError.Other(6), + ) + + errors.forEach { + val error = WebSslError.from(it.first) + assertThat(error).isEqualTo(it.second) + assertThat(error.errorCode).isEqualTo(it.first) + } + } +} diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt index 3ef66738..86ce02df 100644 --- a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt @@ -5,12 +5,12 @@ import androidx.appcompat.app.AppCompatActivity import com.nhaarman.mockito_kotlin.never import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.whenever +import dev.hotwire.turbo.errors.HttpError.ServerError +import dev.hotwire.turbo.errors.LoadError import dev.hotwire.turbo.nav.TurboNavDestination import dev.hotwire.turbo.util.toJson import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit -import dev.hotwire.turbo.visit.TurboVisitError -import dev.hotwire.turbo.visit.TurboVisitErrorType import dev.hotwire.turbo.visit.TurboVisitOptions import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -97,11 +97,7 @@ class TurboSessionTest { session.currentVisit = visit.copy(identifier = visitIdentifier) session.turboFailedToLoad() - verify(callback).onReceivedError(TurboVisitError( - type = TurboVisitErrorType.LOAD_ERROR, - code = -1, - description = "Turbo failed to load" - )) + verify(callback).onReceivedError(LoadError.NotPresent) } @Test @@ -111,11 +107,10 @@ class TurboSessionTest { session.currentVisit = visit.copy(identifier = visitIdentifier) session.visitRequestFailedWithStatusCode(visitIdentifier, true, 500) - verify(callback).requestFailedWithError(true, TurboVisitError( - type = TurboVisitErrorType.HTTP_ERROR, - code = 500, - description = "Request failed" - )) + verify(callback).requestFailedWithError( + visitHasCachedSnapshot = true, + error = ServerError.InternalServerError + ) } @Test @@ -234,6 +229,7 @@ class TurboSessionTest { assertThat(session.restoreCurrentVisit(callback)).isFalse() verify(callback, never()).visitCompleted(false) + verify(callback).requestFailedWithError(false, LoadError.NotReady) } @Test