From 62a3dfe6396153c76cfcc509c4edfc02b46e4963 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 1 Sep 2025 20:49:45 +0100 Subject: [PATCH 01/11] Handle the application_passwords_disabled_for_user error code --- .../ApplicationPasswordsManager.kt | 8 ++++--- .../ApplicationPasswordManagerTests.kt | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsManager.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsManager.kt index 7c6ca96fa2cf..8e0c32cd6e09 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsManager.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsManager.kt @@ -19,6 +19,7 @@ private const val UNAUTHORIZED = 401 private const val CONFLICT = 409 private const val NOT_FOUND = 404 private const val APPLICATION_PASSWORDS_DISABLED_ERROR_CODE = "application_passwords_disabled" +private const val APPLICATION_PASSWORDS_DISABLED_USER_ERROR_CODE = "application_passwords_disabled_for_user" @Singleton internal class ApplicationPasswordsManager @Inject constructor( @@ -162,7 +163,8 @@ internal class ApplicationPasswordsManager @Inject constructor( } statusCode == NOT_FOUND || - errorCode == APPLICATION_PASSWORDS_DISABLED_ERROR_CODE -> { + errorCode == APPLICATION_PASSWORDS_DISABLED_ERROR_CODE || + errorCode == APPLICATION_PASSWORDS_DISABLED_USER_ERROR_CODE -> { appLogWrapper.w( MAIN, "Application Password feature not supported, " + @@ -236,8 +238,8 @@ internal class ApplicationPasswordsManager @Inject constructor( val error = payload.error appLogWrapper.w( AppLog.T.MAIN, "Application password deletion failed, error: " + - "${error.type} ${error.message}\n" + - "${error.volleyError?.toString()}" + "${error.type} ${error.message}\n" + + "${error.volleyError?.toString()}" ) ApplicationPasswordDeletionResult.Failure(error) } diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordManagerTests.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordManagerTests.kt index 2f691d359328..fd5fcf13caac 100644 --- a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordManagerTests.kt +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordManagerTests.kt @@ -166,6 +166,29 @@ class ApplicationPasswordManagerTests { assertEquals(ApplicationPasswordCreationResult.NotSupported(networkError), result) } + @Test + fun `when a jetpack site returns application_passwords_disabled_for_user, then return feature not available`() = + runTest { + val site = testSite.apply { + origin = SiteModel.ORIGIN_WPCOM_REST + } + val networkError = WPComGsonNetworkError(BaseNetworkError(GenericErrorType.SERVER_ERROR)).apply { + apiError = "application_passwords_disabled_for_user" + } + + whenever(applicationPasswordsStore.getCredentials(testSite)).thenReturn(null) + whenever(mJetpackApplicationPasswordsRestClient.fetchWPAdminUsername(site)) + .thenReturn(UsernameFetchPayload(testCredentials.userName)) + whenever(mJetpackApplicationPasswordsRestClient.createApplicationPassword(site, applicationName)) + .thenReturn(ApplicationPasswordCreationPayload(networkError)) + + val result = mApplicationPasswordsManager.getApplicationCredentials( + testSite + ) + + assertEquals(ApplicationPasswordCreationResult.NotSupported(networkError), result) + } + @Test fun `when a non-jetpack site returns 404, then return feature not available`() = runTest { From b3957dab35fae07ef314ff9d333df1eba24a321e Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 1 Sep 2025 21:19:40 +0100 Subject: [PATCH 02/11] Add initial skeleton of the error handler --- ...JetpackApplicationPasswordsErrorHandler.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt new file mode 100644 index 000000000000..17c0db171899 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -0,0 +1,38 @@ +package org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords + +import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class JetpackApplicationPasswordsErrorHandler @Inject constructor() { + private var failuresCount: Int = 0 + + fun handleError(error: WPAPINetworkError) { + val httpStatusCode = error.volleyError?.networkResponse?.statusCode + val apiErrorCode = error.errorCode + + if (httpStatusCode == UNAUTHORIZED || + httpStatusCode == FORBIDDEN || + httpStatusCode == TOO_MANY_REQUESTS || + apiErrorCode == "incorrect_password" || + apiErrorCode == "application_passwords_disabled_for_user" + ) { + // App passwords not supported + TODO() + } else { + failuresCount++ + if (failuresCount >= FAILURES_THRESHOLD) { + // Too many failures, disable app passwords + TODO() + } + } + } + + companion object { + private const val UNAUTHORIZED = 401 + private const val FORBIDDEN = 403 + private const val TOO_MANY_REQUESTS = 429 + private const val FAILURES_THRESHOLD = 10 + } +} From f590f3c0e78e922e77a4a8dfd2d4fed121a6c046 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 10:31:32 +0100 Subject: [PATCH 03/11] Add skeleton of JetpackApplicationPasswordsSupport and update the error handler to use it --- .../JetpackApplicationPasswordsErrorHandler.kt | 13 +++++++------ .../JetpackApplicationPasswordsSupport.kt | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt index 17c0db171899..42ed61fd45f8 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -1,14 +1,17 @@ package org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError import javax.inject.Inject import javax.inject.Singleton @Singleton -class JetpackApplicationPasswordsErrorHandler @Inject constructor() { +class JetpackApplicationPasswordsErrorHandler @Inject constructor( + private val jetpackApplicationPasswordsSupport: JetpackApplicationPasswordsSupport +) { private var failuresCount: Int = 0 - fun handleError(error: WPAPINetworkError) { + fun handleError(siteModel: SiteModel, error: WPAPINetworkError) { val httpStatusCode = error.volleyError?.networkResponse?.statusCode val apiErrorCode = error.errorCode @@ -18,13 +21,11 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor() { apiErrorCode == "incorrect_password" || apiErrorCode == "application_passwords_disabled_for_user" ) { - // App passwords not supported - TODO() + jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } else { failuresCount++ if (failuresCount >= FAILURES_THRESHOLD) { - // Too many failures, disable app passwords - TODO() + jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } } } diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt new file mode 100644 index 000000000000..eedb75abd7dd --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt @@ -0,0 +1,14 @@ +package org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords + +import org.wordpress.android.fluxc.model.SiteModel +import javax.inject.Inject + +class JetpackApplicationPasswordsSupport @Inject constructor() { + fun supportsAppPasswords(siteModel: SiteModel): Boolean { + TODO() + } + + fun flagAsUnsupported(siteModel: SiteModel) { + TODO() + } +} From 0b63df9e76e6fe2149e10d9a42d16b6755abb342 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 10:39:41 +0100 Subject: [PATCH 04/11] Update WooNetwork to integrate with error handling --- .../fluxc/network/rest/wpcom/wc/WooNetwork.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetwork.kt b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetwork.kt index a87077de7fa5..89f837e88d67 100644 --- a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetwork.kt +++ b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetwork.kt @@ -1,7 +1,5 @@ package org.wordpress.android.fluxc.network.rest.wpcom.wc -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetwork import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError @@ -9,8 +7,9 @@ import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkingMode import org.wordpress.android.fluxc.network.rest.wpapi.WPAPIResponse import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsNetwork +import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.JetpackApplicationPasswordsErrorHandler +import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.JetpackApplicationPasswordsSupport import org.wordpress.android.fluxc.network.rest.wpcom.JetpackTunnelWPAPINetwork -import org.wordpress.android.fluxc.persistence.SiteSqlUtils import org.wordpress.android.util.AppLog import javax.inject.Inject import javax.inject.Singleton @@ -30,7 +29,8 @@ class WooNetwork @Inject constructor( private val applicationPasswordsConfiguration: ApplicationPasswordsConfiguration, private val applicationPasswordsNetwork: ApplicationPasswordsNetwork, private val jetpackTunnelWPAPINetwork: JetpackTunnelWPAPINetwork, - private val siteSqlUtils: SiteSqlUtils + private val jetpackApplicationPasswordsSupport: JetpackApplicationPasswordsSupport, + private val jetpackApplicationPasswordsErrorHandler: JetpackApplicationPasswordsErrorHandler ) : WPAPINetwork { override suspend fun executeGetGsonRequest( site: SiteModel, @@ -128,7 +128,7 @@ class WooNetwork @Inject constructor( request: suspend WPAPINetwork.() -> WPAPIResponse ): WPAPIResponse { val appPasswordsEnabled = applicationPasswordsConfiguration.isEnabledForJetpackAccess() - if (!appPasswordsEnabled || !site.isApplicationPasswordsSupported) { + if (!appPasswordsEnabled || !jetpackApplicationPasswordsSupport.supportsAppPasswords(site)) { if (appPasswordsEnabled) { AppLog.v( AppLog.T.API, @@ -150,15 +150,12 @@ class WooNetwork @Inject constructor( is WPAPIResponse.Error<*> -> { logMessageForFailedRequest(requestContext, site.url, appPasswordsResponse.error) - if (appPasswordsResponse.error.errorCode == - ApplicationPasswordsNetwork.APPLICATION_PASSWORDS_NOT_SUPPORT_ERROR_CODE - ) { - withContext(Dispatchers.IO) { - site.applicationPasswordsAuthorizeUrl = null - siteSqlUtils.insertOrUpdateSite(site) + jetpackTunnelWPAPINetwork.request().also { + if (it is WPAPIResponse.Success) { + // when the fallback to Jetpack Tunnel succeeds, notify the error handler + jetpackApplicationPasswordsErrorHandler.handleError(site, appPasswordsResponse.error) } - } - jetpackTunnelWPAPINetwork.request().copyWith( + }.copyWith( networkingMode = WPAPINetworkingMode.JetpackTunnel( isFallback = true, applicationPasswordsError = appPasswordsResponse.error From 1e32000da0901aa2caf06bcb21d143408bb421e6 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 10:54:31 +0100 Subject: [PATCH 05/11] Handle app passwords generation failures too --- .../JetpackApplicationPasswordsErrorHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt index 42ed61fd45f8..2d5c5d302a83 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -19,7 +19,8 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor( httpStatusCode == FORBIDDEN || httpStatusCode == TOO_MANY_REQUESTS || apiErrorCode == "incorrect_password" || - apiErrorCode == "application_passwords_disabled_for_user" + apiErrorCode == "application_passwords_disabled_for_user" || + apiErrorCode == ApplicationPasswordsNetwork.APPLICATION_PASSWORDS_NOT_SUPPORT_ERROR_CODE ) { jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } else { From 8bd2b07ea5327ec0253cdb06a7ff2ccdc0a99738 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 12:41:58 +0100 Subject: [PATCH 06/11] Handle flagging unsupported sites when detecting failures --- libs/fluxc/build.gradle | 1 + .../JetpackApplicationPasswordsSupport.kt | 20 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/libs/fluxc/build.gradle b/libs/fluxc/build.gradle index 2f1b96cb2699..05c7a802efed 100644 --- a/libs/fluxc/build.gradle +++ b/libs/fluxc/build.gradle @@ -65,6 +65,7 @@ android.buildTypes.all { buildType -> dependencies { implementation libs.androidx.exifinterface implementation libs.androidx.security.crypto + implementation libs.androidx.core.ktx implementation(libs.wordpress.utils) { // Using official volley package diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt index eedb75abd7dd..5c90044d08f8 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsSupport.kt @@ -1,14 +1,28 @@ package org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords +import android.content.Context +import androidx.core.content.edit import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.utils.PreferenceUtils import javax.inject.Inject -class JetpackApplicationPasswordsSupport @Inject constructor() { +class JetpackApplicationPasswordsSupport @Inject constructor(context: Context) { + private val fluxCPreferences by lazy { PreferenceUtils.getFluxCPreferences(context) } + fun supportsAppPasswords(siteModel: SiteModel): Boolean { - TODO() + return siteModel.isApplicationPasswordsSupported && + fluxCPreferences.getStringSet(UNSUPPORTED_JETPACK_APP_PASSWORDS_SITES, null) + ?.contains(siteModel.siteId.toString()) != true } fun flagAsUnsupported(siteModel: SiteModel) { - TODO() + val unsupportedSites = fluxCPreferences.getStringSet(UNSUPPORTED_JETPACK_APP_PASSWORDS_SITES, null) ?: setOf() + fluxCPreferences.edit { + putStringSet(UNSUPPORTED_JETPACK_APP_PASSWORDS_SITES, unsupportedSites + siteModel.siteId.toString()) + } + } + + companion object { + private const val UNSUPPORTED_JETPACK_APP_PASSWORDS_SITES = "unsupported_jetpack_app_passwords_sites" } } From 220c7709b11921d67d5801bd7640f4dfaded18c7 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 12:45:42 +0100 Subject: [PATCH 07/11] Store failure counts of each site separately --- .../JetpackApplicationPasswordsErrorHandler.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt index 2d5c5d302a83..f63da7653d09 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -9,7 +9,7 @@ import javax.inject.Singleton class JetpackApplicationPasswordsErrorHandler @Inject constructor( private val jetpackApplicationPasswordsSupport: JetpackApplicationPasswordsSupport ) { - private var failuresCount: Int = 0 + private var failuresCount: MutableMap = mutableMapOf() fun handleError(siteModel: SiteModel, error: WPAPINetworkError) { val httpStatusCode = error.volleyError?.networkResponse?.statusCode @@ -24,8 +24,10 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor( ) { jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } else { - failuresCount++ - if (failuresCount >= FAILURES_THRESHOLD) { + val siteFailuresCount = (failuresCount[siteModel.siteId] ?: 0) + 1 + failuresCount[siteModel.siteId] = siteFailuresCount + + if (siteFailuresCount >= FAILURES_THRESHOLD) { jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } } From d54b016c6792e28273ca7c7d895ad9dbb55e9aa3 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 12:45:50 +0100 Subject: [PATCH 08/11] Add some logs --- .../JetpackApplicationPasswordsErrorHandler.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt index f63da7653d09..bde4cb25a056 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -2,6 +2,7 @@ package org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError +import org.wordpress.android.util.AppLog import javax.inject.Inject import javax.inject.Singleton @@ -22,12 +23,22 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor( apiErrorCode == "application_passwords_disabled_for_user" || apiErrorCode == ApplicationPasswordsNetwork.APPLICATION_PASSWORDS_NOT_SUPPORT_ERROR_CODE ) { + AppLog.w( + AppLog.T.API, + "Disabling Jetpack Application Passwords support for site ${siteModel.siteId} " + + "due to error: $httpStatusCode / $apiErrorCode" + ) jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } else { val siteFailuresCount = (failuresCount[siteModel.siteId] ?: 0) + 1 failuresCount[siteModel.siteId] = siteFailuresCount if (siteFailuresCount >= FAILURES_THRESHOLD) { + AppLog.w( + AppLog.T.API, + "Disabling Jetpack Application Passwords support for site ${siteModel.siteId} " + + "after $failuresCount failures" + ) jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } } From ab41f4111928271042c10078837d0adcd5fce2a9 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 14:51:44 +0100 Subject: [PATCH 09/11] Propagate the app passwords generation failure as received --- .../ApplicationPasswordsNetwork.kt | 14 +++----------- .../JetpackApplicationPasswordsErrorHandler.kt | 3 +-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsNetwork.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsNetwork.kt index c6d2eb65590f..912df347cc29 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsNetwork.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsNetwork.kt @@ -105,15 +105,11 @@ class ApplicationPasswordsNetwork @Inject constructor( } is ApplicationPasswordCreationResult.NotSupported -> { + val networkError = credentialsResult.originalError.toWPAPINetworkError() if (listener.isPresent) { - listener.get().onFeatureUnavailable(site, credentialsResult.originalError.toWPAPINetworkError()) + listener.get().onFeatureUnavailable(site, networkError) } - return WPAPIResponse.Error( - WPAPINetworkError( - baseError = credentialsResult.originalError.toWPAPINetworkError(), - errorCode = APPLICATION_PASSWORDS_NOT_SUPPORT_ERROR_CODE - ) - ) + return WPAPIResponse.Error(networkError) } } @@ -187,10 +183,6 @@ class ApplicationPasswordsNetwork @Inject constructor( clazz: Class, params: Map ) = executeGsonRequest(site, HttpMethod.DELETE, path, clazz, params) - - companion object { - const val APPLICATION_PASSWORDS_NOT_SUPPORT_ERROR_CODE = "application_passwords_not_supported" - } } private fun BaseNetworkError.toWPAPINetworkError(): WPAPINetworkError { diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt index bde4cb25a056..fc3f0b03d2a3 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -20,8 +20,7 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor( httpStatusCode == FORBIDDEN || httpStatusCode == TOO_MANY_REQUESTS || apiErrorCode == "incorrect_password" || - apiErrorCode == "application_passwords_disabled_for_user" || - apiErrorCode == ApplicationPasswordsNetwork.APPLICATION_PASSWORDS_NOT_SUPPORT_ERROR_CODE + apiErrorCode == "application_passwords_disabled_for_user" ) { AppLog.w( AppLog.T.API, From d15286943db1c4500bbf643e0812a41d47cad728 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 18:13:11 +0100 Subject: [PATCH 10/11] Add and update unit tests --- .../network/rest/wpcom/wc/WooNetworkTest.kt | 61 ++++----- ...JetpackApplicationPasswordsErrorHandler.kt | 10 +- ...ckApplicationPasswordsErrorHandlerTests.kt | 124 ++++++++++++++++++ 3 files changed, 155 insertions(+), 40 deletions(-) create mode 100644 libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandlerTests.kt diff --git a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetworkTest.kt b/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetworkTest.kt index 206dfde7eeee..82e6c11ce420 100644 --- a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetworkTest.kt +++ b/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/WooNetworkTest.kt @@ -7,38 +7,42 @@ import org.mockito.kotlin.given import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError import org.wordpress.android.fluxc.network.rest.wpapi.WPAPIResponse import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsNetwork +import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.JetpackApplicationPasswordsErrorHandler +import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.JetpackApplicationPasswordsSupport import org.wordpress.android.fluxc.network.rest.wpcom.JetpackTunnelWPAPINetwork -import org.wordpress.android.fluxc.persistence.SiteSqlUtils import org.wordpress.android.fluxc.test @RunWith(RobolectricTestRunner::class) class WooNetworkTest { private val testSite = SiteModel().apply { + origin = SiteModel.ORIGIN_WPCOM_REST url = "https://example.com" } private val testPath = "path" private val applicationPasswordsConfiguration = FakeApplicationPasswordsConfiguration() private val applicationPasswordsNetwork: ApplicationPasswordsNetwork = mock() private val jetpackTunnelWPAPINetwork: JetpackTunnelWPAPINetwork = mock() - private val siteSqlUtils: SiteSqlUtils = mock() + private val jetpackApplicationPasswordsErrorHandler: JetpackApplicationPasswordsErrorHandler = mock() + private val jetpackApplicationPasswordsSupport: JetpackApplicationPasswordsSupport = mock() private val sut = WooNetwork( applicationPasswordsConfiguration = applicationPasswordsConfiguration, applicationPasswordsNetwork = applicationPasswordsNetwork, jetpackTunnelWPAPINetwork = jetpackTunnelWPAPINetwork, - siteSqlUtils = siteSqlUtils + jetpackApplicationPasswordsSupport = jetpackApplicationPasswordsSupport, + jetpackApplicationPasswordsErrorHandler = jetpackApplicationPasswordsErrorHandler ) @Test fun `given Jetpack site supports app passwords, when making request, then use app passwords network`() = test { - testSite.origin = SiteModel.ORIGIN_WPCOM_REST - testSite.applicationPasswordsAuthorizeUrl = "authorize_url" + whenever(jetpackApplicationPasswordsSupport.supportsAppPasswords(testSite)).thenReturn(true) val sampleResponse = SampleResponse("value") givenAppPasswordsResponse(WPAPIResponse.Success(SampleResponse("value"), emptyList())) @@ -59,8 +63,7 @@ class WooNetworkTest { @Test fun `given jetpack site that supports app passwords, when request fails, then fall back to jetpack tunnel`() = test { - testSite.origin = SiteModel.ORIGIN_WPCOM_REST - testSite.applicationPasswordsAuthorizeUrl = "authorize_url" + whenever(jetpackApplicationPasswordsSupport.supportsAppPasswords(testSite)).thenReturn(true) givenAppPasswordsResponse( WPAPIResponse.Error( WPAPINetworkError( @@ -84,8 +87,7 @@ class WooNetworkTest { @Test fun `given jetpack site that does not support app passwords, when making request, then use jetpack tunnel`() = test { - testSite.origin = SiteModel.ORIGIN_WPCOM_REST - testSite.applicationPasswordsAuthorizeUrl = null + whenever(jetpackApplicationPasswordsSupport.supportsAppPasswords(testSite)).thenReturn(false) val sampleResponse = SampleResponse("value") givenJetpackTunnelResponse(WPAPIResponse.Success(sampleResponse, emptyList())) @@ -101,8 +103,7 @@ class WooNetworkTest { @Test fun `when app passwords are disabled for jetpack access, then always use jetpack tunnel`() = test { applicationPasswordsConfiguration.isEnabledForJetpackAccessValue = false - testSite.origin = SiteModel.ORIGIN_WPCOM_REST - testSite.applicationPasswordsAuthorizeUrl = "authorize_url" + whenever(jetpackApplicationPasswordsSupport.supportsAppPasswords(testSite)).thenReturn(true) val sampleResponse = SampleResponse("value") givenJetpackTunnelResponse(WPAPIResponse.Success(sampleResponse, emptyList())) @@ -121,33 +122,21 @@ class WooNetworkTest { } @Test - fun `when detecting that a site doesn't support app passwords, then update cached site with correct status`() = - test { - testSite.origin = SiteModel.ORIGIN_WPCOM_REST - testSite.applicationPasswordsAuthorizeUrl = "authorize_url" - givenAppPasswordsResponse( - WPAPIResponse.Error( - WPAPINetworkError( - mock(), - errorCode = ApplicationPasswordsNetwork.APPLICATION_PASSWORDS_NOT_SUPPORT_ERROR_CODE - ) - ) - ) - givenJetpackTunnelResponse( - WPAPIResponse.Success(SampleResponse("value"), emptyList()) - ) + fun `when jetpack tunnel fallback succeeds after app passwords failure, then notify error handler`() = test { + whenever(jetpackApplicationPasswordsSupport.supportsAppPasswords(testSite)).thenReturn(true) + val appPasswordsNetworkError = WPAPINetworkError(mock(), "error") + givenAppPasswordsResponse(WPAPIResponse.Error(appPasswordsNetworkError)) + val sampleResponse = SampleResponse("value") + givenJetpackTunnelResponse(WPAPIResponse.Success(sampleResponse, emptyList())) - sut.executeGetGsonRequest( - site = testSite, - path = testPath, - clazz = SampleResponse::class.java - ) + sut.executeGetGsonRequest( + site = testSite, + path = testPath, + clazz = SampleResponse::class.java + ) - val expectedSite = testSite.apply { - applicationPasswordsAuthorizeUrl = null - } - verify(siteSqlUtils).insertOrUpdateSite(expectedSite) - } + verify(jetpackApplicationPasswordsErrorHandler).handleError(testSite, appPasswordsNetworkError) + } private suspend fun givenAppPasswordsResponse(response: WPAPIResponse) { given( diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt index fc3f0b03d2a3..f449f53ab931 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -2,13 +2,15 @@ package org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError +import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.util.AppLog import javax.inject.Inject import javax.inject.Singleton @Singleton class JetpackApplicationPasswordsErrorHandler @Inject constructor( - private val jetpackApplicationPasswordsSupport: JetpackApplicationPasswordsSupport + private val jetpackApplicationPasswordsSupport: JetpackApplicationPasswordsSupport, + private val appLogWrapper: AppLogWrapper ) { private var failuresCount: MutableMap = mutableMapOf() @@ -22,7 +24,7 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor( apiErrorCode == "incorrect_password" || apiErrorCode == "application_passwords_disabled_for_user" ) { - AppLog.w( + appLogWrapper.w( AppLog.T.API, "Disabling Jetpack Application Passwords support for site ${siteModel.siteId} " + "due to error: $httpStatusCode / $apiErrorCode" @@ -33,10 +35,10 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor( failuresCount[siteModel.siteId] = siteFailuresCount if (siteFailuresCount >= FAILURES_THRESHOLD) { - AppLog.w( + appLogWrapper.w( AppLog.T.API, "Disabling Jetpack Application Passwords support for site ${siteModel.siteId} " + - "after $failuresCount failures" + "after $siteFailuresCount failures" ) jetpackApplicationPasswordsSupport.flagAsUnsupported(siteModel) } diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandlerTests.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandlerTests.kt new file mode 100644 index 000000000000..e38eaffb25c2 --- /dev/null +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandlerTests.kt @@ -0,0 +1,124 @@ +package org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords + +import com.android.volley.NetworkResponse +import com.android.volley.VolleyError +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError +import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError +import org.wordpress.android.fluxc.utils.AppLogWrapper + +class JetpackApplicationPasswordsErrorHandlerTests { + private val jetpackApplicationPasswordsSupport: JetpackApplicationPasswordsSupport = mock() + private val appLogWrapper: AppLogWrapper = mock() + private val errorHandler: JetpackApplicationPasswordsErrorHandler = JetpackApplicationPasswordsErrorHandler( + jetpackApplicationPasswordsSupport = jetpackApplicationPasswordsSupport, + appLogWrapper = appLogWrapper + ) + + private val testSite = SiteModel().apply { + siteId = 123L + } + + @Test + fun `given HTTP 401 error, when handling error, then flag site as unsupported`() { + // Given + val volleyError = VolleyError(NetworkResponse(401, null, true, 0, emptyList())) + val baseError = BaseNetworkError(volleyError) + val networkError = WPAPINetworkError(baseError) + + // When + errorHandler.handleError(testSite, networkError) + + // Then + verify(jetpackApplicationPasswordsSupport).flagAsUnsupported(testSite) + } + + @Test + fun `given HTTP 403 error, when handling error, then flag site as unsupported`() { + // Given + val volleyError = VolleyError(NetworkResponse(403, null, true, 0, emptyList())) + val baseError = BaseNetworkError(volleyError) + val networkError = WPAPINetworkError(baseError) + + // When + errorHandler.handleError(testSite, networkError) + + // Then + verify(jetpackApplicationPasswordsSupport).flagAsUnsupported(testSite) + } + + @Test + fun `given HTTP 429 error, when handling error, then flag site as unsupported`() { + // Given + val volleyError = VolleyError(NetworkResponse(429, null, true, 0, emptyList())) + val baseError = BaseNetworkError(volleyError) + val networkError = WPAPINetworkError(baseError) + + // When + errorHandler.handleError(testSite, networkError) + + // Then + verify(jetpackApplicationPasswordsSupport).flagAsUnsupported(testSite) + } + + @Test + fun `given incorrect_password error code, when handling error, then flag site as unsupported`() { + // Given + val baseError = BaseNetworkError(mock()) + val networkError = WPAPINetworkError(baseError, "incorrect_password") + + // When + errorHandler.handleError(testSite, networkError) + + // Then + verify(jetpackApplicationPasswordsSupport).flagAsUnsupported(testSite) + } + + @Test + fun `given application_passwords_disabled_for_user error code, when handling error, then flag site as unsupported`() { + // Given + val baseError = BaseNetworkError(mock()) + val networkError = WPAPINetworkError(baseError, "application_passwords_disabled_for_user") + + // When + errorHandler.handleError(testSite, networkError) + + // Then + verify(jetpackApplicationPasswordsSupport).flagAsUnsupported(testSite) + } + + @Test + fun `given generic error, when handling error once, then do not flag site as unsupported`() { + // Given + val volleyError = VolleyError(NetworkResponse(500, null, true, 0, emptyList())) + val baseError = BaseNetworkError(volleyError) + val networkError = WPAPINetworkError(baseError, "generic_error") + + // When + errorHandler.handleError(testSite, networkError) + + // Then + verify(jetpackApplicationPasswordsSupport, never()).flagAsUnsupported(testSite) + } + + @Test + fun `given generic error, when handling error 10 times, then flag site as unsupported`() { + // Given + val volleyError = VolleyError(NetworkResponse(500, null, true, 0, emptyList())) + val baseError = BaseNetworkError(volleyError) + val networkError = WPAPINetworkError(baseError, "generic_error") + + // When + repeat(10) { + errorHandler.handleError(testSite, networkError) + } + + // Then + verify(jetpackApplicationPasswordsSupport, times(1)).flagAsUnsupported(testSite) + } +} From c977e889ff25876ac44c558021135d5221ae265b Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 2 Sep 2025 18:14:00 +0100 Subject: [PATCH 11/11] Fix detekt issue --- .../JetpackApplicationPasswordsErrorHandler.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt index f449f53ab931..67ed6d8eb61c 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/JetpackApplicationPasswordsErrorHandler.kt @@ -14,6 +14,7 @@ class JetpackApplicationPasswordsErrorHandler @Inject constructor( ) { private var failuresCount: MutableMap = mutableMapOf() + @Suppress("ComplexCondition") fun handleError(siteModel: SiteModel, error: WPAPINetworkError) { val httpStatusCode = error.volleyError?.networkResponse?.statusCode val apiErrorCode = error.errorCode