From be96bb358019c6cdcba68a6ab9006a5e693788b0 Mon Sep 17 00:00:00 2001 From: "Averin, Anton" Date: Sun, 26 Jun 2016 12:43:26 +0200 Subject: [PATCH] Introduce Resolution Strategy pattern implementation --- .../ui/common/resolution/Resolution.kt | 20 +++ .../ui/common/resolution/ResolutionByCase.kt | 23 +++ .../ui/common/resolution/UIResolution.kt | 36 +++++ .../ui/common/resolution/UIResolver.kt | 35 +++++ .../view/NavigationDrawerViewExtension.kt | 1 + app/src/main/res/values/strings.xml | 6 + .../common/resolution/ResolutionByCaseTest.kt | 109 +++++++++++++ .../ui/common/resolution/UIResolutionTest.kt | 145 ++++++++++++++++++ .../view/NavigationDrawerViewExtensionTest.kt | 1 + 9 files changed, 376 insertions(+) create mode 100644 app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/Resolution.kt create mode 100644 app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCase.kt create mode 100644 app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolution.kt create mode 100644 app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolver.kt create mode 100644 app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCaseTest.kt create mode 100644 app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolutionTest.kt diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/Resolution.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/Resolution.kt new file mode 100644 index 0000000..df1a79d --- /dev/null +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/Resolution.kt @@ -0,0 +1,20 @@ +package pro.averin.anton.clean.android.cookbook.ui.common.resolution + +import retrofit2.adapter.rxjava.HttpException + +interface RxHttpResolution { + fun onHttpException(httpException: HttpException) + fun onGenericRxException(t: Throwable) +} + +interface NetworkConnectivityResolution { + fun onConnectivityAvailable() + fun onConnectivityUnavailable() +} + +interface LocationRequestResolution { + fun onNetworkLocationError() +} + +interface Resolution : RxHttpResolution, NetworkConnectivityResolution, LocationRequestResolution { +} \ No newline at end of file diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCase.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCase.kt new file mode 100644 index 0000000..2fb9109 --- /dev/null +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCase.kt @@ -0,0 +1,23 @@ +package pro.averin.anton.clean.android.cookbook.ui.common.resolution + +import retrofit2.adapter.rxjava.HttpException + + +abstract class ResolutionByCase : Resolution { + + override fun onHttpException(httpException: HttpException) { + val code = httpException.code() + when (code) { + 500 -> onInternalServerError() + 503 -> onServiceUnavailable() + 404 -> onNotFound() + else -> onInternalServerError() + } + } + + abstract fun onInternalServerError() + + abstract fun onNotFound() + + abstract fun onServiceUnavailable() +} \ No newline at end of file diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolution.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolution.kt new file mode 100644 index 0000000..97dcd1d --- /dev/null +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolution.kt @@ -0,0 +1,36 @@ +package pro.averin.anton.clean.android.cookbook.ui.common.resolution + +import pro.averin.anton.clean.android.cookbook.R +import javax.inject.Inject + + +open class UIResolution @Inject constructor(val uiResolver: UIResolver) : ResolutionByCase() { + + override fun onConnectivityAvailable() { + uiResolver.hidePersistentSnackBar() + } + + override fun onConnectivityUnavailable() { + uiResolver.showPersistentSnackBar(R.string.error_no_network_connection) + } + + override fun onNotFound() { + uiResolver.showSnackBar(R.string.error_not_found) + } + + override fun onServiceUnavailable() { + uiResolver.showSnackBar(R.string.error_service_unavailable) + } + + override fun onInternalServerError() { + uiResolver.showSnackBar(R.string.error_http_exception) + } + + override fun onGenericRxException(t: Throwable) { + t.printStackTrace() + } + + override fun onNetworkLocationError() { + uiResolver.showSnackBar(R.string.error_enable_gps) + } +} \ No newline at end of file diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolver.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolver.kt new file mode 100644 index 0000000..d3e054c --- /dev/null +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolver.kt @@ -0,0 +1,35 @@ +package pro.averin.anton.clean.android.cookbook.ui.common.resolution + +import android.R +import android.support.design.widget.Snackbar +import android.view.ViewGroup +import pro.averin.anton.clean.android.cookbook.di.ActivityScope +import pro.averin.anton.clean.android.cookbook.ui.common.view.BaseActivity +import javax.inject.Inject + + +@ActivityScope +class UIResolver @Inject constructor(var baseActivity: BaseActivity) { + + private val snackbarRoot: ViewGroup + + init { + snackbarRoot = baseActivity.findViewById(R.id.content) as ViewGroup + } + + private var persistentSnackbar: Snackbar? = null + + fun showSnackBar(messageResource: Int) { + Snackbar.make(snackbarRoot, messageResource, Snackbar.LENGTH_LONG).show() + } + + fun showPersistentSnackBar(messageResource: Int) { + persistentSnackbar = Snackbar.make(snackbarRoot, messageResource, Snackbar.LENGTH_INDEFINITE) + persistentSnackbar?.show() + } + + fun hidePersistentSnackBar() { + persistentSnackbar?.dismiss() + } + +} \ No newline at end of file diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtension.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtension.kt index 66bacf6..cca3e09 100644 --- a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtension.kt +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtension.kt @@ -5,6 +5,7 @@ import android.support.design.widget.NavigationView import android.support.v4.view.GravityCompat import android.support.v4.widget.DrawerLayout import android.support.v7.widget.Toolbar +import pro.averin.anton.clean.android.cookbook.R import pro.averin.anton.clean.android.cookbook.di.ActivityScope import pro.averin.anton.clean.android.cookbook.ui.common.ExtraLifecycleDelegate import pro.averin.anton.clean.android.cookbook.ui.common.view.BaseActivity diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c5fb83..1c6a932 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,4 +3,10 @@ Open navigation drawer Close navigation drawer + + Sorry, application does not yet support offline mode. Please, enable your network connection + Not Found + Service Unavailable + Http Exception + GPS is disabled diff --git a/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCaseTest.kt b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCaseTest.kt new file mode 100644 index 0000000..c8b4694 --- /dev/null +++ b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/ResolutionByCaseTest.kt @@ -0,0 +1,109 @@ +package pro.averin.anton.clean.android.cookbook.ui.common.resolution + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.BDDMockito +import org.mockito.Mock +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner +import retrofit2.adapter.rxjava.HttpException + +@RunWith(PowerMockRunner::class) +@PrepareForTest(HttpException::class) +class ResolutionByCaseTest { + + @Mock lateinit var testStub: TestStub + lateinit var classToTest: TestResolutionByCase + + @Mock lateinit var httpException: HttpException + @Mock lateinit var genericRxException: Throwable + + val internalServerErrorMessage = "InternalServerError" + val notFoundMessage = "NotFound" + val serviceUnavailableMessage = "ServiceUnavailable" + + @Before + fun setUp() { + classToTest = TestResolutionByCase(testStub) + } + + @Test + fun httpExceptionWith500CallsOnInternalServerError() { + // given + BDDMockito.given(httpException.code()).willReturn(500) + + // when + classToTest.onHttpException(httpException) + + // then + BDDMockito.verify(testStub).callStub(internalServerErrorMessage) + } + + @Test + fun httpExceptionWith503CallsServiceUnavailable() { + // given + BDDMockito.given(httpException.code()).willReturn(503) + + // when + classToTest.onHttpException(httpException) + + // then + BDDMockito.verify(testStub).callStub(serviceUnavailableMessage) + } + + @Test + fun httpExceptionWith404CallsNotFound() { + // given + BDDMockito.given(httpException.code()).willReturn(404) + + // when + classToTest.onHttpException(httpException) + + // then + BDDMockito.verify(testStub).callStub(notFoundMessage) + } + + @Test + fun httpExceptionWithUnknownErrorCallInternalServerError() { + // given + BDDMockito.given(httpException.code()).willReturn(100500) + + // when + classToTest.onHttpException(httpException) + + // then + BDDMockito.verify(testStub).callStub(internalServerErrorMessage) + } + + + interface TestStub { + fun callStub(message: String) + } + + inner class TestResolutionByCase constructor(val testStub: TestStub) : ResolutionByCase() { + override fun onNetworkLocationError() { + } + + override fun onInternalServerError() { + testStub.callStub(internalServerErrorMessage) + } + + override fun onNotFound() { + testStub.callStub(notFoundMessage) + } + + override fun onServiceUnavailable() { + testStub.callStub(serviceUnavailableMessage) + } + + override fun onGenericRxException(t: Throwable) { + } + + override fun onConnectivityAvailable() { + } + + override fun onConnectivityUnavailable() { + } + } +} \ No newline at end of file diff --git a/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolutionTest.kt b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolutionTest.kt new file mode 100644 index 0000000..be15494 --- /dev/null +++ b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/resolution/UIResolutionTest.kt @@ -0,0 +1,145 @@ +package pro.averin.anton.clean.android.cookbook.ui.common.resolution + +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner +import pro.averin.anton.clean.android.cookbook.kotlin.test.any +import pro.averin.anton.clean.android.cookbook.kotlin.test.given +import pro.averin.anton.clean.android.cookbook.kotlin.test.mock +import pro.averin.anton.clean.android.cookbook.kotlin.test.verify +import retrofit2.adapter.rxjava.HttpException + +@RunWith(PowerMockRunner::class) +@PrepareForTest( + UIResolver::class, + HttpException::class +) +class UIResolutionTest { + + @Mock lateinit var uiResolver: UIResolver + + @InjectMocks lateinit var classToTest: UIResolution + + @Test + fun availableConnectivityHidesPersistentSnackBar() { + // when + classToTest.onConnectivityAvailable() + + // then + verify(uiResolver).hidePersistentSnackBar() + } + + @Test + fun unavailableConnectivityShowsPersistentSnackBar() { + // when + classToTest.onConnectivityUnavailable() + + // then + verify(uiResolver).showPersistentSnackBar(any()) + } + + @Test + fun internalServerErrorShowsSnackBar() { + // when + classToTest.onInternalServerError() + + // then + verify(uiResolver).showSnackBar(any()) + } + + @Test + fun serviceUnavailableShowsSnackbar() { + // when + classToTest.onServiceUnavailable() + + // then + verify(uiResolver).showSnackBar(any()) + } + + + @Test + fun notFoundShowsSnackBar() { + // when + classToTest.onNotFound() + + // then + verify(uiResolver).showSnackBar(any()) + } + + @Test + fun genericExceptionIsTracked() { + //given + val genericException = mock() + + // when + classToTest.onGenericRxException(genericException) + + // then + verify(genericException).printStackTrace() + } + + @Test + fun networkLocationErrorShowsSnackbar() { + // when + classToTest.onNetworkLocationError() + + // then + verify(uiResolver).showSnackBar(any()) + } + + @Test + fun http500ExceptionHandledBySnackbarMessage() { + // given + val httpException = mock() + given(httpException.code()).willReturn(500) + + // when + classToTest.onHttpException(httpException) + + // then + verify(uiResolver).showSnackBar(any()) + } + + @Test + fun http503ExceptionHandledBySnackbarMessage() { + // given + val httpException = mock() + given(httpException.code()).willReturn(503) + + // when + classToTest.onHttpException(httpException) + + // then + verify(uiResolver).showSnackBar(any()) + } + + @Test + fun http404ExceptionHandledBySnackbarMessage() { + // given + val httpException = mock() + given(httpException.code()).willReturn(404) + + // when + classToTest.onHttpException(httpException) + + // then + verify(uiResolver).showSnackBar(any()) + } + + @Test + fun httpUnknownExceptionHandeledAsInternalServerError() { + // given + val httpException = mock() + given(httpException.code()).willReturn(505) + + // when + classToTest.onHttpException(httpException) + + // then + verify(uiResolver).showSnackBar(any()) + } + +} \ No newline at end of file diff --git a/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtensionTest.kt b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtensionTest.kt index 3c97804..0b064f0 100644 --- a/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtensionTest.kt +++ b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/main/view/NavigationDrawerViewExtensionTest.kt @@ -15,6 +15,7 @@ import org.mockito.InjectMocks import org.mockito.Mock import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner +import pro.averin.anton.clean.android.cookbook.R import pro.averin.anton.clean.android.cookbook.kotlin.test.* import pro.averin.anton.clean.android.cookbook.ui.common.view.BaseActivity import pro.averin.anton.clean.android.cookbook.ui.main.presenter.MainPresenter