@@ -5,6 +5,8 @@ import org.assertj.core.api.Assertions.assertThat
55import org.junit.Before
66import org.junit.Test
77import org.junit.runner.RunWith
8+ import org.mockito.kotlin.mock
9+ import org.mockito.kotlin.whenever
810import org.robolectric.RobolectricTestRunner
911import org.robolectric.RuntimeEnvironment
1012import org.wordpress.android.fluxc.encryption.EncryptionUtils
@@ -14,6 +16,13 @@ import org.wordpress.android.fluxc.model.SiteModel
1416class SiteSqlUtilsTest {
1517 private val siteSqlUtils = SiteSqlUtils (EncryptionUtils ())
1618
19+ // EncryptionUtils uses the AndroidKeyStore provider, which Robolectric can't load, so the production
20+ // util's encrypt/decrypt throw here. A stubbed util lets us exercise the encrypting writers' own
21+ // ContentValues mapping (which half of each encrypt() Pair lands in which column) against a real DB —
22+ // the SiteStore-level tests can't, since they verify a mocked SiteSqlUtils and never run the body.
23+ private val fakeEncryptionUtils = mock<EncryptionUtils >()
24+ private val encryptingSiteSqlUtils = SiteSqlUtils (fakeEncryptionUtils)
25+
1726 @Before
1827 fun setUp () {
1928 val appContext = RuntimeEnvironment .getApplication().applicationContext
@@ -210,6 +219,85 @@ class SiteSqlUtilsTest {
210219 assertThat(stored.apiRestPasswordPlain).isNullOrEmpty()
211220 }
212221
222+ @Test
223+ fun `updateApplicationPasswordCredentials writes each encrypted value into its matching column` () {
224+ // ciphertext (encrypt().first) -> *_ENCRYPTED column, IV (.second) -> *_IV column, and username
225+ // must not be cross-wired with password. A .first/.second swap, a wrong column, or a dropped
226+ // cv.put would all read back wrong here — the failure modes the SiteStore mock can't catch.
227+ whenever(fakeEncryptionUtils.encrypt(" user" )).thenReturn(" enc_user" to " iv_user" )
228+ whenever(fakeEncryptionUtils.encrypt(" pass" )).thenReturn(" enc_pass" to " iv_pass" )
229+ WellSql .insert(SiteModel ().apply { url = " https://example.test" }).execute()
230+ val localId = storedSite().id
231+
232+ val rows = encryptingSiteSqlUtils.updateApplicationPasswordCredentials(localId, " user" , " pass" )
233+
234+ assertThat(rows).isEqualTo(1 )
235+ val stored = storedSite()
236+ assertThat(stored.apiRestUsernameEncrypted).isEqualTo(" enc_user" )
237+ assertThat(stored.apiRestUsernameIV).isEqualTo(" iv_user" )
238+ assertThat(stored.apiRestPasswordEncrypted).isEqualTo(" enc_pass" )
239+ assertThat(stored.apiRestPasswordIV).isEqualTo(" iv_pass" )
240+ }
241+
242+ @Test
243+ fun `updateApplicationPasswordCredentials persists credentials that decrypt back to the originals` () {
244+ whenever(fakeEncryptionUtils.encrypt(" user" )).thenReturn(" enc_user" to " iv_user" )
245+ whenever(fakeEncryptionUtils.encrypt(" pass" )).thenReturn(" enc_pass" to " iv_pass" )
246+ whenever(fakeEncryptionUtils.decrypt(" enc_user" , " iv_user" )).thenReturn(" user" )
247+ whenever(fakeEncryptionUtils.decrypt(" enc_pass" , " iv_pass" )).thenReturn(" pass" )
248+ WellSql .insert(SiteModel ().apply { url = " https://example.test" }).execute()
249+ val localId = storedSite().id
250+
251+ encryptingSiteSqlUtils.updateApplicationPasswordCredentials(localId, " user" , " pass" )
252+
253+ // Read back through the decrypting path: the stored ciphertext/IV must round-trip to the inputs.
254+ val stored = encryptingSiteSqlUtils.getSites().single()
255+ assertThat(stored.apiRestUsernamePlain).isEqualTo(" user" )
256+ assertThat(stored.apiRestPasswordPlain).isEqualTo(" pass" )
257+ }
258+
259+ @Test
260+ fun `updateApplicationPasswordCredentialsForWPAPISite writes the matching WPAPI site by url` () {
261+ whenever(fakeEncryptionUtils.encrypt(" user" )).thenReturn(" enc_user" to " iv_user" )
262+ whenever(fakeEncryptionUtils.encrypt(" pass" )).thenReturn(" enc_pass" to " iv_pass" )
263+ WellSql .insert(SiteModel ().apply {
264+ url = " https://selfhosted.test"
265+ origin = SiteModel .ORIGIN_WPAPI
266+ }).execute()
267+
268+ val rows = encryptingSiteSqlUtils.updateApplicationPasswordCredentialsForWPAPISite(
269+ siteUrl = " https://selfhosted.test" ,
270+ usernamePlain = " user" ,
271+ passwordPlain = " pass"
272+ )
273+
274+ assertThat(rows).isEqualTo(1 )
275+ val stored = storedSite()
276+ assertThat(stored.apiRestUsernameEncrypted).isEqualTo(" enc_user" )
277+ assertThat(stored.apiRestUsernameIV).isEqualTo(" iv_user" )
278+ assertThat(stored.apiRestPasswordEncrypted).isEqualTo(" enc_pass" )
279+ assertThat(stored.apiRestPasswordIV).isEqualTo(" iv_pass" )
280+ }
281+
282+ @Test
283+ fun `updateApplicationPasswordCredentialsForWPAPISite leaves a non-WPAPI site with the same url untouched` () {
284+ WellSql .insert(SiteModel ().apply {
285+ url = " https://shared.test"
286+ origin = SiteModel .ORIGIN_WPCOM_REST
287+ }).execute()
288+
289+ val rows = encryptingSiteSqlUtils.updateApplicationPasswordCredentialsForWPAPISite(
290+ siteUrl = " https://shared.test" ,
291+ usernamePlain = " user" ,
292+ passwordPlain = " pass"
293+ )
294+
295+ assertThat(rows).isEqualTo(0 )
296+ val stored = storedSite()
297+ assertThat(stored.apiRestUsernameEncrypted).isNullOrEmpty()
298+ assertThat(stored.apiRestPasswordEncrypted).isNullOrEmpty()
299+ }
300+
213301 @Test
214302 fun `updateXmlRpcUrl writes the column and leaves other fields alone` () {
215303 WellSql .insert(SiteModel ().apply {
0 commit comments