diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 8136784f6bde..64b5d740343d 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -6,6 +6,7 @@ ----- - [*] Woo POS: Remote Tap to Pay failures now explain what went wrong - phone not eligible for Tap to Pay, NFC turned off, or a payment service error - instead of a generic message or raw error text [https://github.com/woocommerce/woocommerce-android/pull/16384] - [*] QR login now asks you to choose a store when your WordPress.com account has multiple sites instead of preselecting one [https://github.com/woocommerce/woocommerce-android/pull/16400] +- [*] Store login now shows a connection error when a site's REST API hides its namespaces instead of continuing into misleading setup screens [https://github.com/woocommerce/woocommerce-android/pull/16408] - [Internal] Close analytics gaps in the Remote Tap to Pay flow: transport on the reader-ready event, cause chains in error descriptions, no stale card reader model, and a real session ended reason [https://github.com/woocommerce/woocommerce-android/pull/16368] - [*] Tap to Pay no longer disappears from the Payments screen shortly after it appears [https://github.com/woocommerce/woocommerce-android/pull/16370] - [*] Fixed a crash when pressing Back on the "You're already signed in" warning shown after opening a QR login link while signed in [https://github.com/woocommerce/woocommerce-android/pull/16347] diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt index 3fb0709dd7fb..e56c9ae471d4 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt @@ -4,6 +4,8 @@ import com.android.volley.RequestQueue import okhttp3.HttpUrl.Companion.toHttpUrl import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError +import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.INVALID_RESPONSE import org.wordpress.android.fluxc.network.UserAgent import org.wordpress.android.fluxc.network.discovery.DiscoveryUtils import org.wordpress.android.fluxc.network.discovery.DiscoveryWPAPIRestClient @@ -54,15 +56,21 @@ class SiteWPAPIRestClient @Inject constructor( return when (result) { is Success -> { val response = result.data + if (response?.namespaces.isNullOrEmpty()) { + return SiteModel().apply { + error = BaseNetworkError(INVALID_RESPONSE) + } + } + SiteModel().apply { - name = response?.name - timezone = response?.gmtOffset + name = response.name + timezone = response.gmtOffset origin = SiteModel.ORIGIN_WPAPI - hasWooCommerce = response?.namespaces?.any { + hasWooCommerce = response.namespaces.any { it.startsWith(WOO_API_NAMESPACE_PREFIX) - } ?: false + } - applicationPasswordsAuthorizeUrl = response?.authentication?.applicationPasswords + applicationPasswordsAuthorizeUrl = response.authentication?.applicationPasswords ?.endpoints?.authorization if (!applicationPasswordsAuthorizeUrl.isNullOrEmpty() && applicationPasswordsAuthorizeUrl.contains(APPLICATION_PASSWORDS_URL_SUFFIX)) { diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClientTest.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClientTest.kt new file mode 100644 index 000000000000..170a28ca6496 --- /dev/null +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClientTest.kt @@ -0,0 +1,105 @@ +package org.wordpress.android.fluxc.network.rest.wpapi.site + +import com.android.volley.RequestQueue +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.INVALID_RESPONSE +import org.wordpress.android.fluxc.network.UserAgent +import org.wordpress.android.fluxc.network.discovery.DiscoveryWPAPIRestClient +import org.wordpress.android.fluxc.network.discovery.RootWPAPIRestResponse +import org.wordpress.android.fluxc.network.rest.wpapi.WPAPIGsonRequestBuilder +import org.wordpress.android.fluxc.network.rest.wpapi.WPAPIResponse.Success +import org.wordpress.android.fluxc.store.SiteStore.FetchWPAPISitePayload +import org.wordpress.android.fluxc.test + +@Suppress("UnitTestNamingRule") +@RunWith(RobolectricTestRunner::class) +class SiteWPAPIRestClientTest { + private val wpapiGsonRequestBuilder: WPAPIGsonRequestBuilder = mock() + private val discoveryWPAPIRestClient: DiscoveryWPAPIRestClient = mock() + private val dispatcher: Dispatcher = mock() + private val requestQueue: RequestQueue = mock() + private val userAgent: UserAgent = mock() + + private lateinit var restClient: SiteWPAPIRestClient + + @Before + fun setUp() { + restClient = SiteWPAPIRestClient( + wpapiGsonRequestBuilder = wpapiGsonRequestBuilder, + discoveryWPAPIRestClient = discoveryWPAPIRestClient, + dispatcher = dispatcher, + requestQueue = requestQueue, + userAgent = userAgent + ) + } + + @Test + fun `given application passwords and empty namespaces, when fetching site, then return invalid response error`() = + test { + initResponse( + RootWPAPIRestResponse( + namespaces = emptyList(), + authentication = RootWPAPIRestResponse.Authentication( + applicationPasswords = RootWPAPIRestResponse.Authentication.ApplicationPasswords( + endpoints = RootWPAPIRestResponse.Authentication.ApplicationPasswords.Endpoints( + authorization = APPLICATION_PASSWORDS_URL + ) + ) + ) + ) + ) + + val site = restClient.fetchWPAPISite(FetchWPAPISitePayload(SITE_URL)) + + assertThat(site.isError).isTrue() + assertThat(site.error.type).isEqualTo(INVALID_RESPONSE) + assertThat(site.applicationPasswordsAuthorizeUrl).isNull() + } + + @Test + fun `given missing namespaces, when fetching site, then return invalid response error`() = test { + initResponse(RootWPAPIRestResponse(namespaces = null)) + + val site = restClient.fetchWPAPISite(FetchWPAPISitePayload(SITE_URL)) + + assertThat(site.isError).isTrue() + assertThat(site.error.type).isEqualTo(INVALID_RESPONSE) + } + + @Test + fun `given non-empty namespaces without WooCommerce, when fetching site, then return non-Woo site`() = test { + initResponse(RootWPAPIRestResponse(namespaces = listOf("wp/v2"))) + + val site = restClient.fetchWPAPISite(FetchWPAPISitePayload(SITE_URL)) + + assertThat(site.isError).isFalse() + assertThat(site.hasWooCommerce).isFalse() + } + + private suspend fun initResponse(response: RootWPAPIRestResponse) { + whenever(discoveryWPAPIRestClient.discoverWPAPIBaseURL(SITE_URL)).thenReturn(WP_API_URL) + whenever( + wpapiGsonRequestBuilder.syncGetRequest( + restClient = restClient, + url = WP_API_URL, + clazz = RootWPAPIRestResponse::class.java, + params = mapOf("_fields" to FETCH_API_CALL_FIELDS) + ) + ).thenReturn(Success(response, emptyList())) + } + + companion object { + private const val SITE_URL = "https://example.com" + private const val WP_API_URL = "$SITE_URL/wp-json/" + private const val APPLICATION_PASSWORDS_URL = "$SITE_URL/wp-admin/authorize-application.php" + private const val FETCH_API_CALL_FIELDS = + "name,description,gmt_offset,url,authentication,namespaces" + } +}