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..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 @@ -5,14 +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 @@ -20,9 +22,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 +39,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() @@ -64,12 +65,13 @@ class MainActivity : AppCompatActivity() { 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() + R.id.change_url -> NavGraphDirections.actionGlobalBaseURLFragment() else -> throw IllegalArgumentException("Unknown menu item id: ${menuItem.itemId}") } - navigateDeepLink(deepLink) + navigateTo(directions) binding.drawer.close() return true } @@ -109,7 +111,7 @@ class MainActivity : AppCompatActivity() { 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 +123,8 @@ class MainActivity : AppCompatActivity() { 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/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 6d506bc..b50d1a7 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 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 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 73048b0..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,32 +1,48 @@ 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 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 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.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 + @MockK(relaxUnitFun = true) + lateinit var authRepo: AuthRepo + + @MockK(relaxUnitFun = true) + lateinit var recipeRepo: RecipeRepo + @MockK lateinit var versionDataSource: VersionDataSource + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + private val logger: Logger = FakeLogger() lateinit var subject: BaseURLViewModel @@ -34,16 +50,89 @@ class BaseURLViewModelTest : RobolectricTest() { @Before fun setUp() { MockKAnnotations.init(this) - subject = BaseURLViewModel(serverInfoRepo, versionDataSource, logger) + Dispatchers.setMain(UnconfinedTestDispatcher()) + subject = BaseURLViewModel( + serverInfoRepo = serverInfoRepo, + authRepo = authRepo, + recipeRepo = recipeRepo, + versionDataSource = versionDataSource, + logger = logger, + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() } @Test - fun `when saveBaseUrl and getVersionInfo returns result then saves to storage`() = runTest { + 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 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) + } + + @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() - coVerify { serverInfoRepo.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) } + assertThat(subject.uiState.value).isInstanceOf(OperationUiState.Failure::class.java) } } \ No newline at end of file