From 2b7a0bbeaf149bb16b80f8ef7aa3fd28328478ca Mon Sep 17 00:00:00 2001 From: adalpari Date: Mon, 15 Jun 2026 12:40:14 +0200 Subject: [PATCH 1/4] Fix NPE when creating app password for site with no username getOrFetchUsername passed a nullable SiteModel.username into UsernameFetchPayload's non-null String constructor, crashing self-hosted sites without a stored username. Guard for null/empty and return a NOT_AUTHENTICATED error payload instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ApplicationPasswordsManager.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 ea38e1403a6a..a5e7948954ef 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 @@ -74,7 +74,20 @@ internal class ApplicationPasswordsManager @Inject constructor( return if (site.origin == SiteModel.ORIGIN_WPCOM_REST) { jetpackApplicationPasswordsRestClient.fetchWPAdminUsername(site) } else { - UsernameFetchPayload(site.username) + val username = site.username + if (username.isNullOrEmpty()) { + UsernameFetchPayload( + WPAPINetworkError( + BaseNetworkError( + GenericErrorType.NOT_AUTHENTICATED, + "Site username is missing. " + + "The application password was probably authorized using the Web flow" + ) + ) + ) + } else { + UsernameFetchPayload(username) + } } } From f932eae51ba17ccdef46f7119588675905afe501 Mon Sep 17 00:00:00 2001 From: adalpari Date: Mon, 15 Jun 2026 12:44:48 +0200 Subject: [PATCH 2/4] Add test for null username when creating app password Verifies a non-WPCOM site with no username returns a NOT_AUTHENTICATED Failure instead of crashing. Confirmed to reproduce the original NPE when run against the unfixed code. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ApplicationPasswordManagerTests.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 99c91d22991f..221fa0443ef3 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 @@ -9,6 +9,7 @@ import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError @@ -159,6 +160,26 @@ class ApplicationPasswordManagerTests { assertEquals(ApplicationPasswordCreationResult.Created(testCredentials), result) } + @Test + fun `given a non-jetpack site with no username, when we ask for a password, then return Failure`() = + runTest { + val site = SiteModel().apply { + origin = SiteModel.ORIGIN_XMLRPC + url = "http://test-site.com" + username = null + } + whenever(applicationPasswordsStore.getCredentials(site)).thenReturn(null) + + val result = mApplicationPasswordsManager.getApplicationCredentials(site) + + Assert.assertTrue(result is ApplicationPasswordCreationResult.Failure) + assertEquals( + GenericErrorType.NOT_AUTHENTICATED, + (result as ApplicationPasswordCreationResult.Failure).error.type + ) + verifyNoInteractions(mWpApiApplicationPasswordsRestClient) + } + @Test fun `when a jetpack site returns 404, then return feature not available`() = runTest { From e9f7bfebca357b5d3f0a37b656930519b8250f8d Mon Sep 17 00:00:00 2001 From: adalpari Date: Mon, 15 Jun 2026 14:15:58 +0200 Subject: [PATCH 3/4] Return Failure when site lacks credentials for app password generation The guard in createApplicationPassword constructed a Failure result but never returned it, so execution fell through to the network call. Add the missing return so the guard short-circuits as intended. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../wpapi/applicationpasswords/ApplicationPasswordsManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a5e7948954ef..174c586bfbc5 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 @@ -96,7 +96,7 @@ internal class ApplicationPasswordsManager @Inject constructor( username: String ): ApplicationPasswordCreationResult { if (!site.supportsApplicationPasswordsGeneration) { - ApplicationPasswordCreationResult.Failure( + return ApplicationPasswordCreationResult.Failure( WPAPINetworkError( BaseNetworkError( GenericErrorType.NOT_AUTHENTICATED, From f420d15c1961d3c90fe81bf01704e69b2c720c8c Mon Sep 17 00:00:00 2001 From: adalpari Date: Mon, 15 Jun 2026 14:16:33 +0200 Subject: [PATCH 4/4] Add test for missing password guard in app password generation Verifies that a non-jetpack site with a username but no password returns a Failure (NOT_AUTHENTICATED) without hitting the network, locking in the short-circuit guard in createApplicationPassword. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ApplicationPasswordManagerTests.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 221fa0443ef3..b5de2191d79b 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 @@ -180,6 +180,27 @@ class ApplicationPasswordManagerTests { verifyNoInteractions(mWpApiApplicationPasswordsRestClient) } + @Test + fun `given a non-jetpack site with no password, when we ask for a password, then return Failure`() = + runTest { + val site = SiteModel().apply { + origin = SiteModel.ORIGIN_XMLRPC + url = "http://test-site.com" + username = "username" + password = null + } + whenever(applicationPasswordsStore.getCredentials(site)).thenReturn(null) + + val result = mApplicationPasswordsManager.getApplicationCredentials(site) + + Assert.assertTrue(result is ApplicationPasswordCreationResult.Failure) + assertEquals( + GenericErrorType.NOT_AUTHENTICATED, + (result as ApplicationPasswordCreationResult.Failure).error.type + ) + verifyNoInteractions(mWpApiApplicationPasswordsRestClient) + } + @Test fun `when a jetpack site returns 404, then return feature not available`() = runTest {