From ad22227f8e77d807530580a2bb700bacaa947694 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 12 Nov 2022 11:10:20 +0100 Subject: [PATCH 1/5] Simplify view binding in Activity --- .../java/gq/kirmanak/mealient/ui/activity/MainActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index c98aedf..ad2e837 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.isVisible import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment +import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import dagger.hilt.android.AndroidEntryPoint @@ -20,9 +21,9 @@ import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject @AndroidEntryPoint -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(R.layout.main_activity) { - private lateinit var binding: MainActivityBinding + private val binding: MainActivityBinding by viewBinding(MainActivityBinding::bind, R.id.drawer) private val viewModel by viewModels() private val title: String by lazy { getString(R.string.app_name) } private val uiState: MainActivityUiState get() = viewModel.uiState @@ -37,7 +38,6 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } splashScreen.setKeepOnScreenCondition { viewModel.startDestination.value == null } - binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) configureToolbar() configureNavGraph() From e835d5bbe5357fe8e73864a960ef3d62675c2151 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 12 Nov 2022 11:28:10 +0100 Subject: [PATCH 2/5] Replace deep links with global actions --- .../mealient/ui/activity/MainActivity.kt | 25 ++++++--------- app/src/main/res/navigation/nav_graph.xml | 32 +++++++++++-------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index ad2e837..eb826bf 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -5,15 +5,16 @@ import android.view.Menu import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.isVisible import androidx.navigation.NavController +import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import dagger.hilt.android.AndroidEntryPoint +import gq.kirmanak.mealient.NavGraphDirections import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.MainActivityBinding import gq.kirmanak.mealient.extensions.observeOnce @@ -64,12 +65,12 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { private fun onNavigationItemSelected(menuItem: MenuItem): Boolean { logger.v { "onNavigationItemSelected() called with: menuItem = $menuItem" } menuItem.isChecked = true - val deepLink = when (menuItem.itemId) { - R.id.add_recipe -> ADD_RECIPE_DEEP_LINK - R.id.recipes_list -> RECIPES_LIST_DEEP_LINK + val directions = when (menuItem.itemId) { + R.id.add_recipe -> NavGraphDirections.actionGlobalAddRecipeFragment() + R.id.recipes_list -> NavGraphDirections.actionGlobalRecipesFragment() else -> throw IllegalArgumentException("Unknown menu item id: ${menuItem.itemId}") } - navigateDeepLink(deepLink) + navigateTo(directions) binding.drawer.close() return true } @@ -109,7 +110,7 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { logger.v { "onOptionsItemSelected() called with: item = $item" } val result = when (item.itemId) { R.id.login -> { - navigateDeepLink(AUTH_DEEP_LINK) + navigateTo(NavGraphDirections.actionGlobalAuthenticationFragment()) true } R.id.logout -> { @@ -121,14 +122,8 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { return result } - private fun navigateDeepLink(deepLink: String) { - logger.v { "navigateDeepLink() called with: deepLink = $deepLink" } - navController.navigate(deepLink.toUri()) - } - - companion object { - private const val AUTH_DEEP_LINK = "mealient://authenticate" - private const val ADD_RECIPE_DEEP_LINK = "mealient://recipe/add" - private const val RECIPES_LIST_DEEP_LINK = "mealient://recipe/list" + private fun navigateTo(directions: NavDirections) { + logger.v { "navigateTo() called with: directions = $directions" } + navController.navigate(directions) } } \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 6d506bc..81784f9 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -9,11 +9,8 @@ android:id="@+id/authenticationFragment" android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment" android:label="AuthenticationFragment" - tools:layout="@layout/fragment_authentication"> - - + tools:layout="@layout/fragment_authentication" /> + - + + + + - - + tools:layout="@layout/fragment_add_recipe" /> + + + + + + \ No newline at end of file From 4e1e3f81e319e086f4c75d78619c08f61d195072 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 12 Nov 2022 12:32:44 +0100 Subject: [PATCH 3/5] Allow changing base URL from UI --- .../gq/kirmanak/mealient/ui/activity/MainActivity.kt | 1 + .../kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt | 11 +++++++++++ app/src/main/res/menu/navigation_menu.xml | 4 ++++ app/src/main/res/navigation/nav_graph.xml | 4 ++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 22 insertions(+) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index eb826bf..ef263b4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -68,6 +68,7 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { val directions = when (menuItem.itemId) { R.id.add_recipe -> NavGraphDirections.actionGlobalAddRecipeFragment() R.id.recipes_list -> NavGraphDirections.actionGlobalRecipesFragment() + R.id.change_url -> NavGraphDirections.actionGlobalBaseURLFragment() else -> throw IllegalArgumentException("Unknown menu item id: ${menuItem.itemId}") } navigateTo(directions) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt index d2a66de..f532cc7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -5,8 +5,10 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.VersionDataSource +import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState @@ -16,6 +18,8 @@ import javax.inject.Inject @HiltViewModel class BaseURLViewModel @Inject constructor( private val serverInfoRepo: ServerInfoRepo, + private val authRepo: AuthRepo, + private val recipeRepo: RecipeRepo, private val versionDataSource: VersionDataSource, private val logger: Logger, ) : ViewModel() { @@ -33,10 +37,17 @@ class BaseURLViewModel @Inject constructor( private suspend fun checkBaseURL(baseURL: String) { logger.v { "checkBaseURL() called with: baseURL = $baseURL" } + if (baseURL == serverInfoRepo.getUrl()) { + logger.d { "checkBaseURL: new URL matches current" } + _uiState.value = OperationUiState.fromResult(Result.success(Unit)) + return + } val result = runCatchingExceptCancel { // If it returns proper version info then it must be a Mealie val version = versionDataSource.getVersionInfo(baseURL).version serverInfoRepo.storeBaseURL(baseURL, version) + authRepo.logout() + recipeRepo.clearLocalData() } logger.i { "checkBaseURL: result is $result" } _uiState.value = OperationUiState.fromResult(result) diff --git a/app/src/main/res/menu/navigation_menu.xml b/app/src/main/res/menu/navigation_menu.xml index 4ac8fb7..c5b3ea1 100644 --- a/app/src/main/res/menu/navigation_menu.xml +++ b/app/src/main/res/menu/navigation_menu.xml @@ -7,4 +7,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 81784f9..b50d1a7 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -72,4 +72,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bca5f84..06e2ecc 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -47,4 +47,5 @@ неожиданный ответ нет соединения Ошибка загрузки. + Сменить URL \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7bd2a7b..13d5934 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,4 +51,5 @@ unauthorized unexpected response no connection + Change URL \ No newline at end of file From 3504eae2483ce2db2590e879069aa2dd0c7dfb26 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 12 Nov 2022 12:48:46 +0100 Subject: [PATCH 4/5] Add base URL tests --- .../ui/baseurl/BaseURLViewModelTest.kt | 78 +++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt index 73048b0..688c497 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt @@ -1,8 +1,10 @@ package gq.kirmanak.mealient.ui.baseurl +import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.VersionInfo +import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION @@ -13,6 +15,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before @@ -24,6 +27,12 @@ class BaseURLViewModelTest : RobolectricTest() { @MockK(relaxUnitFun = true) lateinit var serverInfoRepo: ServerInfoRepo + @MockK(relaxUnitFun = true) + lateinit var authRepo: AuthRepo + + @MockK(relaxUnitFun = true) + lateinit var recipeRepo: RecipeRepo + @MockK lateinit var versionDataSource: VersionDataSource @@ -34,16 +43,75 @@ class BaseURLViewModelTest : RobolectricTest() { @Before fun setUp() { MockKAnnotations.init(this) - subject = BaseURLViewModel(serverInfoRepo, versionDataSource, logger) + subject = BaseURLViewModel( + serverInfoRepo = serverInfoRepo, + authRepo = authRepo, + recipeRepo = recipeRepo, + versionDataSource = versionDataSource, + logger = logger, + ) + } + + + @Test + fun `when saveBaseURL expect no version checks given that current URL matches new`() = runTest { + setupSaveBaseUrlWithOldUrl() + coVerify(inverse = true) { versionDataSource.getVersionInfo(any()) } } @Test - fun `when saveBaseUrl and getVersionInfo returns result then saves to storage`() = runTest { + fun `when saveBaseURL expect URL isn't saved given that current URL matches new`() = runTest { + setupSaveBaseUrlWithOldUrl() + coVerify(inverse = true) { serverInfoRepo.storeBaseURL(any(), any()) } + } + + @Test + fun `when saveBaseURL expect no logout given that current URL matches new`() = runTest { + setupSaveBaseUrlWithOldUrl() + coVerify(inverse = true) { authRepo.logout() } + } + + @Test + fun `when saveBaseURL expect data intact given that current URL matches new`() = runTest { + setupSaveBaseUrlWithOldUrl() + coVerify(inverse = true) { recipeRepo.clearLocalData() } + } + + private fun TestScope.setupSaveBaseUrlWithOldUrl() { + coEvery { serverInfoRepo.getUrl() } returns TEST_BASE_URL + versionDataSourceReturnsSuccess() + subject.saveBaseUrl(TEST_BASE_URL) + advanceUntilIdle() + } + + @Test + fun `when saveBaseUrl expect URL is saved given that new URL doesn't match old`() = runTest { + setupSaveBaseUrlWithNewUrl() + coVerify { serverInfoRepo.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) } + } + + @Test + fun `when saveBaseURL expect logout given that new URL doesn't match old`() = runTest { + setupSaveBaseUrlWithNewUrl() + coVerify { authRepo.logout() } + } + + @Test + fun `when saveBaseURL expect recipes removed given that new URL doesn't match old`() = runTest { + setupSaveBaseUrlWithNewUrl() + coVerify { recipeRepo.clearLocalData() } + } + + private fun TestScope.setupSaveBaseUrlWithNewUrl() { + coEvery { serverInfoRepo.getUrl() } returns null + versionDataSourceReturnsSuccess() + subject.saveBaseUrl(TEST_BASE_URL) + advanceUntilIdle() + } + + private fun versionDataSourceReturnsSuccess() { coEvery { versionDataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VersionInfo(TEST_VERSION) - subject.saveBaseUrl(TEST_BASE_URL) - advanceUntilIdle() - coVerify { serverInfoRepo.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) } } } \ No newline at end of file From 612101f1b4b3d17f462d71cd38ca83749e7c72bc Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 12 Nov 2022 13:21:26 +0100 Subject: [PATCH 5/5] Fix base URL view model coverage calculation --- .../kirmanak/mealient/test/RobolectricTest.kt | 10 ------ .../ui/baseurl/BaseURLViewModelTest.kt | 31 ++++++++++++++++--- 2 files changed, 26 insertions(+), 15 deletions(-) delete mode 100644 app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt diff --git a/app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt b/app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt deleted file mode 100644 index 5fc79bf..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package gq.kirmanak.mealient.test - -import android.app.Application -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.runner.RunWith -import org.robolectric.annotation.Config - -@RunWith(AndroidJUnit4::class) -@Config(application = Application::class, manifest = Config.NONE) -abstract class RobolectricTest \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt index 688c497..355d33f 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.ui.baseurl +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.VersionDataSource @@ -9,20 +11,22 @@ import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION import gq.kirmanak.mealient.test.FakeLogger -import gq.kirmanak.mealient.test.RobolectricTest +import gq.kirmanak.mealient.ui.OperationUiState import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.* +import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test +import java.io.IOException @OptIn(ExperimentalCoroutinesApi::class) -class BaseURLViewModelTest : RobolectricTest() { +class BaseURLViewModelTest { @MockK(relaxUnitFun = true) lateinit var serverInfoRepo: ServerInfoRepo @@ -36,6 +40,9 @@ class BaseURLViewModelTest : RobolectricTest() { @MockK lateinit var versionDataSource: VersionDataSource + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + private val logger: Logger = FakeLogger() lateinit var subject: BaseURLViewModel @@ -43,6 +50,7 @@ class BaseURLViewModelTest : RobolectricTest() { @Before fun setUp() { MockKAnnotations.init(this) + Dispatchers.setMain(UnconfinedTestDispatcher()) subject = BaseURLViewModel( serverInfoRepo = serverInfoRepo, authRepo = authRepo, @@ -52,6 +60,10 @@ class BaseURLViewModelTest : RobolectricTest() { ) } + @After + fun tearDown() { + Dispatchers.resetMain() + } @Test fun `when saveBaseURL expect no version checks given that current URL matches new`() = runTest { @@ -114,4 +126,13 @@ class BaseURLViewModelTest : RobolectricTest() { versionDataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VersionInfo(TEST_VERSION) } + + @Test + fun `when saveBaseURL expect error given that version can't be fetched`() = runTest { + coEvery { serverInfoRepo.getUrl() } returns null + coEvery { versionDataSource.getVersionInfo(eq(TEST_BASE_URL)) } throws IOException() + subject.saveBaseUrl(TEST_BASE_URL) + advanceUntilIdle() + assertThat(subject.uiState.value).isInstanceOf(OperationUiState.Failure::class.java) + } } \ No newline at end of file