diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginActivity.kt index 5419211cb9d2..4e7a0be86bd4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginActivity.kt @@ -2,15 +2,22 @@ package org.wordpress.android.ui.accounts.applicationpassword import android.content.Intent import android.os.Bundle +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.wordpress.android.R -import org.wordpress.android.util.ToastUtils +import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.main.BaseAppCompatActivity import org.wordpress.android.ui.main.WPMainActivity +import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.extensions.setContent import javax.inject.Inject @AndroidEntryPoint @@ -23,6 +30,14 @@ class ApplicationPasswordLoginActivity: BaseAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initViewModel() + setContent { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + CircularProgressIndicator() + } + } } private fun initViewModel() { @@ -31,13 +46,13 @@ class ApplicationPasswordLoginActivity: BaseAppCompatActivity() { viewModel!!.setupSite(intent.dataString.orEmpty()) } - private fun openMainActivity(siteUrl: String?) { - if (siteUrl != null) { + private fun openMainActivity(navigationActionData: ApplicationPasswordLoginViewModel.NavigationActionData) { + if (!navigationActionData.isError && navigationActionData.siteUrl != null) { ToastUtils.showToast( this, getString( R.string.application_password_credentials_stored, - siteUrl + navigationActionData.siteUrl ) ) intent.setData(null) @@ -46,17 +61,35 @@ class ApplicationPasswordLoginActivity: BaseAppCompatActivity() { this, getString( R.string.application_password_credentials_storing_error, - siteUrl + navigationActionData.siteUrl ) ) } - val mainActivityIntent = - Intent(this, WPMainActivity::class.java) - mainActivityIntent.setFlags( - (Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK - or Intent.FLAG_ACTIVITY_CLEAR_TASK) - ) - startActivity(mainActivityIntent) + + if (navigationActionData.isError) { + ActivityLauncher.showMainActivity(this) + } else if (navigationActionData.showSiteSelector) { + ActivityLauncher.showMainActivityAndLoginEpilogue(this, navigationActionData.oldSitesIDs, false) + } else if (navigationActionData.showPostSignupInterstitial) { + ActivityLauncher.showPostSignupInterstitial(this) + } else { + val mainActivityIntent = Intent(this, WPMainActivity::class.java) + mainActivityIntent.setFlags( + (Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_ACTIVITY_CLEAR_TASK) + ) + startActivity(mainActivityIntent) + } finish() } + + override fun onStart() { + super.onStart() + viewModel?.onStart() + } + + override fun onStop() { + super.onStop() + viewModel?.onStop() + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModel.kt index 45c1eab40c6c..e28f6daadcba 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModel.kt @@ -1,6 +1,5 @@ package org.wordpress.android.ui.accounts.applicationpassword -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineDispatcher @@ -8,31 +7,55 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.generated.SiteActionBuilder import org.wordpress.android.fluxc.network.discovery.SelfHostedEndpointFinder -import org.wordpress.android.fluxc.store.SiteStore.RefreshSitesXMLRPCPayload import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.fluxc.store.SiteStore.OnProfileFetched +import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged +import org.wordpress.android.fluxc.store.SiteStore.RefreshSitesXMLRPCPayload +import org.wordpress.android.fluxc.utils.AppLogWrapper +import org.wordpress.android.login.util.SiteUtils import org.wordpress.android.modules.IO_THREAD import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper - +import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.UriLogin +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.UrlUtils import javax.inject.Inject import javax.inject.Named -private const val TAG = "ApplicationPasswordLoginViewModel" - class ApplicationPasswordLoginViewModel @Inject constructor( @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher, + // Dispatcher is the way to dispatch actions to Flux. It will call siteStore.onAction() + private val dispatcher: Dispatcher, private val applicationPasswordLoginHelper: ApplicationPasswordLoginHelper, private val selfHostedEndpointFinder: SelfHostedEndpointFinder, private val siteStore: SiteStore, + private val appPrefsWrapper: AppPrefsWrapper, + private val appLogWrapper: AppLogWrapper, ) : ViewModel() { - private val _onFinishedEvent = MutableSharedFlow() + private val _onFinishedEvent = MutableSharedFlow() /** * A shared flow that emits the site URL when the setup is finished. * It can emit null if the site could not be set up. */ val onFinishedEvent = _onFinishedEvent.asSharedFlow() + private var currentUrlLogin: UriLogin? = null + private var oldSitesIDs: ArrayList? = null + + fun onStart() { + dispatcher.register(this) + oldSitesIDs = SiteUtils.getCurrentSiteIds(siteStore, false) + } + + fun onStop() { + dispatcher.unregister(this) + } + /** * This method is called to set up the site with the provided raw data. * @@ -40,32 +63,70 @@ class ApplicationPasswordLoginViewModel @Inject constructor( */ fun setupSite(rawData: String) { viewModelScope.launch { + if (rawData.isEmpty()) { + appLogWrapper.e(AppLog.T.MAIN, "Cannot store credentials: rawData is empty") + _onFinishedEvent.emit( + NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = "", + oldSitesIDs = oldSitesIDs, + isError = true + ) + ) + return@launch + } val urlLogin = applicationPasswordLoginHelper.getSiteUrlLoginFromRawData(rawData) // Store credentials if the site already exists val credentialsStored = storeCredentials(rawData) + // If the site already exists, we can skip fetching it again if (credentialsStored) { - _onFinishedEvent.emit(urlLogin.siteUrl) + _onFinishedEvent.emit( + NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = oldSitesIDs, + isError = false + ) + ) + } else { + fetchSites(urlLogin) + currentUrlLogin = urlLogin + } + } + } + + @Suppress("TooGenericExceptionCaught") + private suspend fun storeCredentials(rawData: String): Boolean = withContext(ioDispatcher) { + try { + if (rawData.isEmpty()) { + appLogWrapper.e(AppLog.T.DB, "Cannot store credentials: rawData is empty") + false } else { - val siteFetched = fetchSites(urlLogin) - _onFinishedEvent.emit(if (siteFetched) urlLogin.siteUrl else null) + val credentialsStored = applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData) + credentialsStored } + } catch (e: Exception) { + appLogWrapper.e(AppLog.T.DB, "Error storing credentials: ${e.stackTrace}") + false } } @Suppress("TooGenericExceptionCaught") private suspend fun fetchSites( - urlLogin: ApplicationPasswordLoginHelper.UriLogin - ): Boolean = withContext(ioDispatcher) { + urlLogin: UriLogin + ) = withContext(ioDispatcher) { try { if (urlLogin.user.isNullOrEmpty() || urlLogin.password.isNullOrEmpty() || urlLogin.siteUrl.isNullOrEmpty()) { - Log.e(TAG, "Cannot store credentials: rawData is empty") - false + appLogWrapper.e(AppLog.T.MAIN, "Cannot store credentials: rawData is empty") + emitErrorFetching(urlLogin) } else { val xmlRpcEndpoint = selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl) - siteStore.onAction( + dispatcher.dispatch( SiteActionBuilder.newFetchSitesXmlRpcFromApplicationPasswordAction( RefreshSitesXMLRPCPayload( username = urlLogin.user, @@ -74,27 +135,68 @@ class ApplicationPasswordLoginViewModel @Inject constructor( ) ) ) - true } } catch (e: Exception) { - Log.e(TAG, "Error storing credentials", e) - false + appLogWrapper.e(AppLog.T.API, "Error storing credentials: ${e.stackTrace}") + emitErrorFetching(urlLogin) } } - @Suppress("TooGenericExceptionCaught") - private suspend fun storeCredentials(rawData: String): Boolean = withContext(ioDispatcher) { - try { - if (rawData.isEmpty()) { - Log.e(TAG, "Cannot store credentials: rawData is empty") - false - } else { - val credentialsStored = applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData) - credentialsStored + private suspend fun emitErrorFetching(urlLogin: UriLogin) = _onFinishedEvent.emit( + NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = oldSitesIDs, + isError = true + ) + ) + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onSiteChanged(event: OnSiteChanged) { + val currentNormalizedUrl = UrlUtils.normalizeUrl(currentUrlLogin?.siteUrl) + val site = siteStore.sites.firstOrNull { UrlUtils.normalizeUrl(it.url) == currentNormalizedUrl } + if (site == null) { + appLogWrapper.e(AppLog.T.MAIN, "Site not found for URL: ${currentUrlLogin?.siteUrl}") + viewModelScope.launch { + _onFinishedEvent.emit( + NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = currentUrlLogin?.siteUrl, + oldSitesIDs = oldSitesIDs, + isError = true + ) + ) } - } catch (e: Exception) { - Log.e(TAG, "Error storing credentials", e) - false + } else { + dispatcher.dispatch(SiteActionBuilder.newFetchProfileXmlRpcAction(site)) } } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onProfileFetched(event: OnProfileFetched) { + viewModelScope.launch { + _onFinishedEvent.emit( + NavigationActionData( + showSiteSelector = siteStore.hasSite(), + showPostSignupInterstitial = !siteStore.hasSite() + && appPrefsWrapper.shouldShowPostSignupInterstitial, + siteUrl = event.site.url, + oldSitesIDs = oldSitesIDs, + isError = false + ) + ) + } + } + + data class NavigationActionData( + val showSiteSelector: Boolean, + val showPostSignupInterstitial: Boolean, + val siteUrl: String?, + val oldSitesIDs: ArrayList?, + val isError: Boolean + ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt index 9f0ffa5947d3..31a8ea1e6771 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt @@ -11,6 +11,7 @@ import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.util.AppLog import org.wordpress.android.util.BuildConfigWrapper +import org.wordpress.android.util.UrlUtils import rs.wordpress.api.kotlin.ApiDiscoveryResult import rs.wordpress.api.kotlin.WpLoginClient import javax.inject.Inject @@ -77,7 +78,8 @@ class ApplicationPasswordLoginHelper @Inject constructor( if (uriLogin.user.isNullOrEmpty() || uriLogin.password.isNullOrEmpty() ) { false } else { - val site = siteSqlUtils.getSites().firstOrNull { it.url == uriLogin.siteUrl } + val normalizedUrl = UrlUtils.normalizeUrl(uriLogin.siteUrl) + val site = siteSqlUtils.getSites().firstOrNull { UrlUtils.normalizeUrl(it.url) == normalizedUrl} if (site != null) { site.apply { apiRestUsernamePlain = uriLogin.user diff --git a/WordPress/src/test/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModelTest.kt index 5c3c686de530..cf52c17c1cf0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/accounts/applicationpassword/ApplicationPasswordLoginViewModelTest.kt @@ -15,37 +15,64 @@ import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper import org.wordpress.android.fluxc.network.discovery.SelfHostedEndpointFinder import org.wordpress.android.fluxc.store.SiteStore import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.utils.AppLogWrapper +import org.wordpress.android.ui.prefs.AppPrefsWrapper import kotlin.test.assertEquals @ExperimentalCoroutinesApi +@Suppress("MaxLineLength") class ApplicationPasswordLoginViewModelTest : BaseUnitTest() { + @Mock + lateinit var dispatcher: Dispatcher + @Mock lateinit var applicationPasswordLoginHelper: ApplicationPasswordLoginHelper + @Mock lateinit var selfHostedEndpointFinder: SelfHostedEndpointFinder + @Mock lateinit var siteStore: SiteStore + @Mock + lateinit var appPrefsWrapper: AppPrefsWrapper + + @Mock + lateinit var appLogWrapper: AppLogWrapper + private lateinit var viewModel: ApplicationPasswordLoginViewModel private val rawData = "url=callback?site_url=https://example.com&user_login=user&password=pass" - private val uriLogin = ApplicationPasswordLoginHelper.UriLogin("https://example.com", "user", "pass") + private val urlLogin = ApplicationPasswordLoginHelper.UriLogin("https://example.com", "user", "pass") @Before fun setUp() { MockitoAnnotations.openMocks(this) viewModel = ApplicationPasswordLoginViewModel( testDispatcher(), + dispatcher, applicationPasswordLoginHelper, selfHostedEndpointFinder, - siteStore + siteStore, + appPrefsWrapper, + appLogWrapper ) - whenever(applicationPasswordLoginHelper.getSiteUrlLoginFromRawData(rawData)).thenReturn(uriLogin) + whenever(applicationPasswordLoginHelper.getSiteUrlLoginFromRawData(rawData)).thenReturn(urlLogin) } @Test - fun `valid rawData stores credentials and emits siteUrl`() = runTest { + fun `given intent rawData, when setup site and able to store credentials, then emit ok`() = runTest { // Given + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = null, + isError = false + ) whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(true) // When @@ -54,7 +81,7 @@ class ApplicationPasswordLoginViewModelTest : BaseUnitTest() { // Then val finishedEvent = awaitItem() - assertEquals(uriLogin.siteUrl, finishedEvent, "onFinishedEvent should emit the siteUrl") + assertEquals(expectedResult, finishedEvent) verify(applicationPasswordLoginHelper, times(1)) .storeApplicationPasswordCredentialsFrom(rawData) verify(selfHostedEndpointFinder, times(0)).verifyOrDiscoverXMLRPCEndpoint(any()) @@ -63,9 +90,16 @@ class ApplicationPasswordLoginViewModelTest : BaseUnitTest() { } @Test - fun `empty rawData does not store credentials nor site and emits null`()= runTest { + fun `given empty rawData, when setup site, then emit error`() = runTest { // Given val emptyRawData = "" + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = "", + oldSitesIDs = null, + isError = true + ) // When viewModel.onFinishedEvent.test { @@ -73,7 +107,7 @@ class ApplicationPasswordLoginViewModelTest : BaseUnitTest() { // Then val finishedEvent = awaitItem() - assertEquals(finishedEvent, null, "onFinishedEvent should emit null") + assertEquals(expectedResult, finishedEvent) verify(applicationPasswordLoginHelper, times(0)) .storeApplicationPasswordCredentialsFrom(rawData) verify(selfHostedEndpointFinder, times(0)).verifyOrDiscoverXMLRPCEndpoint(any()) @@ -82,82 +116,232 @@ class ApplicationPasswordLoginViewModelTest : BaseUnitTest() { } @Test - fun `when credentials not stored, fetchSites succeeds, emits siteUrl`() = runTest { - // Given - whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(false) - whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(uriLogin.siteUrl!!)) - .thenReturn("https://example.com/xmlrpc.php") + fun `given intent rawData, when setup site and not able to store credentials and data is empty, then fetch them and emit error`() = + runTest { + // Given + val malformedRawData = "malformed ray data" + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = "", + oldSitesIDs = null, + isError = true + ) + whenever(applicationPasswordLoginHelper.getSiteUrlLoginFromRawData(malformedRawData)) + .thenReturn( + ApplicationPasswordLoginHelper.UriLogin("", "", "") + ) - // When - viewModel.onFinishedEvent.test { - viewModel.setupSite(rawData) + // When + viewModel.onFinishedEvent.test { + viewModel.setupSite(malformedRawData) - // Then - val finishedEvent = awaitItem() - assertEquals(uriLogin.siteUrl, finishedEvent, "onFinishedEvent should emit the siteUrl") - verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(uriLogin.siteUrl!!) - verify(siteStore, times(1)).onAction(any()) - cancelAndIgnoreRemainingEvents() + // Then + val finishedEvent = awaitItem() + assertEquals(expectedResult, finishedEvent) + verify(selfHostedEndpointFinder, times(0)).verifyOrDiscoverXMLRPCEndpoint(any()) + cancelAndIgnoreRemainingEvents() + } } - } @Test - fun `when credentials not stored due to exception, fetchSites succeeds, emits siteUrl`() = runTest { - // Given - whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)) - .thenThrow(RuntimeException()) - whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(uriLogin.siteUrl!!)) - .thenReturn("https://example.com/xmlrpc.php") + fun `given intent rawData, when setup site and not able to store credentials and throw error fetching, then fetch them and emit error`() = + runTest { + // Given + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = null, + isError = true + ) + whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(false) + whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(any())).thenThrow(RuntimeException()) - // When - viewModel.onFinishedEvent.test { - viewModel.setupSite(rawData) + // When + viewModel.onFinishedEvent.test { + viewModel.setupSite(rawData) - // Then - val finishedEvent = awaitItem() - assertEquals(uriLogin.siteUrl, finishedEvent, "onFinishedEvent should emit the siteUrl") - verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(uriLogin.siteUrl!!) - verify(siteStore, times(1)).onAction(any()) - cancelAndIgnoreRemainingEvents() + // Then + val finishedEvent = awaitItem() + assertEquals(expectedResult, finishedEvent) + verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(eq(urlLogin.siteUrl!!)) + cancelAndIgnoreRemainingEvents() + } } - } @Test - fun `when credentials not stored, fetchSites fails, emits null`() = runTest { - // Given - whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)) - .thenReturn(false) - whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(uriLogin.siteUrl!!)) - .thenThrow(RuntimeException()) + fun `given intent rawData, when setup site and not able to store credentials nor store fetch, then emit error`() = + runTest { + // Given + val xmlRpcEndpoint = "https://example.com/xmlrpc.php" + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = null, + isError = true + ) + whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(false) + whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!)) + .thenReturn(xmlRpcEndpoint) - // When - viewModel.onFinishedEvent.test { - viewModel.setupSite(rawData) + // When + viewModel.onFinishedEvent.test { + viewModel.setupSite(rawData) + // Mock onSiteChanged event + viewModel.onSiteChanged( + SiteStore.OnSiteChanged( + rowsAffected = 1, + ) + ) - // Then - val finishedEvent = awaitItem() - assertEquals(null, finishedEvent, "onFinishedEvent should emit null on fetchSites failure") - verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(uriLogin.siteUrl!!) - verify(siteStore, times(0)).onAction(any()) - cancelAndIgnoreRemainingEvents() + // Then + val finishedEvent = awaitItem() + assertEquals(expectedResult, finishedEvent) + verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!) + verify(siteStore, times(1)).sites + cancelAndIgnoreRemainingEvents() + } } - } @Test - fun `when credentials not stored, fetchSites with missing user or password emits null`() = runTest { - // Given - val malformedRawData = "malformed ray data" + fun `given intent rawData, when setup site and not able to store credentials but store fetch, then emit ok with site selector`() = + runTest { + // Given + val xmlRpcEndpoint = "https://example.com/xmlrpc.php" + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = true, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = null, + isError = false + ) + whenever(siteStore.hasSite()).thenReturn(true) + whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(false) + whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!)) + .thenReturn(xmlRpcEndpoint) - // When - viewModel.onFinishedEvent.test { - viewModel.setupSite(malformedRawData) + // When + viewModel.onFinishedEvent.test { + viewModel.setupSite(rawData) + // Mock onSiteChanged event + viewModel.onProfileFetched( + SiteStore.OnProfileFetched( + SiteModel().apply { url = urlLogin.siteUrl } + ) + ) - // Then - val finishedEvent = awaitItem() - assertEquals(null, finishedEvent, "onFinishedEvent should emit null if user or password is missing") - verify(selfHostedEndpointFinder, times(0)).verifyOrDiscoverXMLRPCEndpoint(any()) - verify(siteStore, times(0)).onAction(any()) - cancelAndIgnoreRemainingEvents() + // Then + val finishedEvent = awaitItem() + assertEquals(expectedResult, finishedEvent) + verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given intent rawData, when setup site and not able to store credentials but store fetch, then emit ok with no site selector nor interstitial`() = + runTest { + // Given + val xmlRpcEndpoint = "https://example.com/xmlrpc.php" + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = null, + isError = false + ) + whenever(siteStore.hasSite()).thenReturn(false) + whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(false) + whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!)) + .thenReturn(xmlRpcEndpoint) + + // When + viewModel.onFinishedEvent.test { + viewModel.setupSite(rawData) + // Mock onSiteChanged event + viewModel.onProfileFetched( + SiteStore.OnProfileFetched( + SiteModel().apply { url = urlLogin.siteUrl } + ) + ) + + // Then + val finishedEvent = awaitItem() + assertEquals(expectedResult, finishedEvent) + verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given intent rawData, when setup site and not able to store credentials but store fetch, then emit ok with no interstitial by sites`() = + runTest { + // Given + val xmlRpcEndpoint = "https://example.com/xmlrpc.php" + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = true, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = null, + isError = false + ) + whenever(siteStore.hasSite()).thenReturn(true) + whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(false) + whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!)) + .thenReturn(xmlRpcEndpoint) + + // When + viewModel.onFinishedEvent.test { + viewModel.setupSite(rawData) + // Mock onSiteChanged event + viewModel.onProfileFetched( + SiteStore.OnProfileFetched( + SiteModel().apply { url = urlLogin.siteUrl } + ) + ) + + // Then + val finishedEvent = awaitItem() + assertEquals(expectedResult, finishedEvent) + verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given intent rawData, when setup site and not able to store credentials but store fetch, then emit ok with no interstitial by preferences`() = + runTest { + // Given + val xmlRpcEndpoint = "https://example.com/xmlrpc.php" + val expectedResult = ApplicationPasswordLoginViewModel.NavigationActionData( + showSiteSelector = false, + showPostSignupInterstitial = false, + siteUrl = urlLogin.siteUrl, + oldSitesIDs = null, + isError = false + ) + whenever(appPrefsWrapper.shouldShowPostSignupInterstitial).thenReturn(false) + whenever(applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)).thenReturn(false) + whenever(selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!)) + .thenReturn(xmlRpcEndpoint) + + // When + viewModel.onFinishedEvent.test { + viewModel.setupSite(rawData) + // Mock onSiteChanged event + viewModel.onProfileFetched( + SiteStore.OnProfileFetched( + SiteModel().apply { url = urlLogin.siteUrl } + ) + ) + + // Then + val finishedEvent = awaitItem() + assertEquals(expectedResult, finishedEvent) + verify(selfHostedEndpointFinder, times(1)).verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl!!) + cancelAndIgnoreRemainingEvents() + } } - } }