Add batch identity checks to stories and share/forward flows.

This commit is contained in:
Cody Henthorne
2022-07-26 16:31:55 -04:00
parent 87cb2d6bf8
commit a7a5f2e8c6
9 changed files with 418 additions and 24 deletions

View File

@@ -0,0 +1,246 @@
package org.thoughtcrime.securesms.contacts.paged
import android.app.Application
import io.reactivex.rxjava3.core.Single
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockedStatic
import org.mockito.internal.configuration.plugins.Plugins
import org.mockito.internal.junit.JUnitRule
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.RecipientDatabaseTestUtils
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testutil.SystemOutLogger
import org.thoughtcrime.securesms.util.IdentityUtil
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.services.ProfileService
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.IdentityCheckResponse
import java.io.IOException
import java.util.Optional
import java.util.concurrent.TimeUnit
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class SafetyNumberRepositoryTest {
@Rule
@JvmField
val mockitoRule: MockitoRule = JUnitRule(Plugins.getMockitoLogger(), Strictness.STRICT_STUBS)
@Mock
lateinit var profileService: ProfileService
@Mock(lenient = true)
lateinit var aciIdentityStore: SignalIdentityKeyStore
@Mock
lateinit var staticIdentityUtil: MockedStatic<IdentityUtil>
@Mock
lateinit var staticRecipient: MockedStatic<Recipient>
private var now: Long = System.currentTimeMillis()
private lateinit var recipientPool: MutableList<Recipient>
private lateinit var identityPool: MutableMap<Recipient, IdentityRecord>
private lateinit var repository: SafetyNumberRepository
companion object {
@BeforeClass
@JvmStatic
fun setUpClass() {
Log.initialize(SystemOutLogger())
}
}
@Before
fun setUp() {
now = System.currentTimeMillis()
repository = SafetyNumberRepository(profileService, aciIdentityStore)
recipientPool = mutableListOf()
identityPool = mutableMapOf()
for (id in 1L until 12) {
val recipient = RecipientDatabaseTestUtils.createRecipient(resolved = true, recipientId = RecipientId.from(id))
staticRecipient.`when`<Recipient> { Recipient.resolved(RecipientId.from(id)) }.thenReturn(recipient)
recipientPool.add(recipient)
val record = IdentityRecord(
recipientId = recipient.id,
identityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey,
verifiedStatus = IdentityDatabase.VerifiedStatus.DEFAULT,
firstUse = false,
timestamp = 0,
nonblockingApproval = false
)
whenever(aciIdentityStore.getIdentityRecord(recipient.id)).thenReturn(Optional.of(record))
identityPool[recipient] = record
}
staticRecipient.`when`<Recipient> { Recipient.self() }.thenReturn(recipientPool[0])
}
/**
* Batch request for a current identity key should return an empty list and not perform any identity key updates.
*/
@Test
fun batchSafetyNumberCheckSync_batchOf1_noChanges() {
val other = recipientPool[1]
val keys = listOf(ContactSearchKey.RecipientSearchKey.KnownRecipient(other.id))
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(keys.map { it.recipientId }) }) }.thenReturn(listOf(other))
whenever(profileService.performIdentityCheck(mapOf(other.requireServiceId() to identityPool[other]!!.identityKey)))
.thenReturn(Single.just(ServiceResponse.forResult(IdentityCheckResponse(listOf()), 200, "")))
repository.batchSafetyNumberCheckSync(keys, now)
staticIdentityUtil.verifyNoInteractions()
}
/**
* Batch request for an out-of-date identity key should return the new identity key and update the store.
*/
@Test
fun batchSafetyNumberCheckSync_batchOf1_oneChange() {
val other = recipientPool[1]
val otherAci = ACI.from(other.requireServiceId())
val otherNewIdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey
val keys = listOf(ContactSearchKey.RecipientSearchKey.KnownRecipient(other.id))
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(keys.map { it.recipientId }) }) }.thenReturn(listOf(other))
whenever(profileService.performIdentityCheck(mapOf(other.requireServiceId() to identityPool[other]!!.identityKey)))
.thenReturn(Single.just(ServiceResponse.forResult(IdentityCheckResponse(listOf(IdentityCheckResponse.AciIdentityPair(otherAci, otherNewIdentityKey))), 200, "")))
repository.batchSafetyNumberCheckSync(keys, now)
staticIdentityUtil.verify { IdentityUtil.saveIdentity(otherAci.toString(), otherNewIdentityKey) }
staticIdentityUtil.verifyNoMoreInteractions()
}
/**
* Batch request for an out-of-date identity key should return the new identity key and update the store.
*/
@Test
fun batchSafetyNumberCheckSync_batchOf2_oneChange() {
val other = recipientPool[1]
val secondOther = recipientPool[2]
val otherAci = ACI.from(other.requireServiceId())
val otherNewIdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey
val keys = listOf(ContactSearchKey.RecipientSearchKey.KnownRecipient(other.id), ContactSearchKey.RecipientSearchKey.KnownRecipient(secondOther.id))
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(keys.map { it.recipientId }) }) }.thenReturn(listOf(other, secondOther))
whenever(profileService.performIdentityCheck(mapOf(other.requireServiceId() to identityPool[other]!!.identityKey, secondOther.requireServiceId() to identityPool[secondOther]!!.identityKey)))
.thenReturn(Single.just(ServiceResponse.forResult(IdentityCheckResponse(listOf(IdentityCheckResponse.AciIdentityPair(otherAci, otherNewIdentityKey))), 200, "")))
repository.batchSafetyNumberCheckSync(keys, now)
staticIdentityUtil.verify { IdentityUtil.saveIdentity(otherAci.toString(), otherNewIdentityKey) }
staticIdentityUtil.verifyNoMoreInteractions()
}
/**
* Batch request for a current identity key should previously checked should abort checking.
*/
@Test
fun batchSafetyNumberCheckSync_batchOf1_abortOnPriorRecentCheck() {
val other = recipientPool[1]
val keys = listOf(ContactSearchKey.RecipientSearchKey.KnownRecipient(other.id))
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(keys.map { it.recipientId }) }) }.thenReturn(listOf(other))
whenever(profileService.performIdentityCheck(mapOf(other.requireServiceId() to identityPool[other]!!.identityKey)))
.thenReturn(Single.just(ServiceResponse.forResult(IdentityCheckResponse(listOf()), 200, "")))
repository.batchSafetyNumberCheckSync(keys, now)
verify(profileService, times(1)).performIdentityCheck(any())
repository.batchSafetyNumberCheckSync(keys, now + TimeUnit.SECONDS.toMillis(10))
verify(profileService, times(1)).performIdentityCheck(any())
repository.batchSafetyNumberCheckSync(keys, now + TimeUnit.SECONDS.toMillis(31))
verify(profileService, times(2)).performIdentityCheck(any())
staticIdentityUtil.verifyNoInteractions()
}
/**
* Batch request for a current identity keys should return an empty list and not perform any identity key updates.
*/
@Test
fun batchSafetyNumberCheckSync_batchOf10WithSmallBatchSize_noChanges() {
val keys = recipientPool.map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it.id) }
val others = recipientPool.subList(1, recipientPool.lastIndex)
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(others.map { it.id }) }) }.thenReturn(others)
for (chunk in others.chunked(2)) {
whenever(profileService.performIdentityCheck(chunk.associate { it.requireServiceId() to identityPool[it]!!.identityKey }))
.thenReturn(Single.just(ServiceResponse.forResult(IdentityCheckResponse(listOf()), 200, "")))
}
repository.batchSafetyNumberCheckSync(keys, now, 2)
staticIdentityUtil.verifyNoInteractions()
}
@Test
fun batchSafetyNumberCheckSync_serverError() {
val other = recipientPool[1]
val keys = listOf(ContactSearchKey.RecipientSearchKey.KnownRecipient(other.id))
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(keys.map { it.recipientId }) }) }.thenReturn(listOf(other))
whenever(profileService.performIdentityCheck(mapOf(other.requireServiceId() to identityPool[other]!!.identityKey)))
.thenReturn(Single.just(ServiceResponse.forApplicationError(NonSuccessfulResponseCodeException(400), 400, "")))
repository.batchSafetyNumberCheckSync(keys, now)
staticIdentityUtil.verifyNoInteractions()
}
@Test
fun batchSafetyNumberCheckSync_networkError() {
val other = recipientPool[1]
val keys = listOf(ContactSearchKey.RecipientSearchKey.KnownRecipient(other.id))
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(keys.map { it.recipientId }) }) }.thenReturn(listOf(other))
whenever(profileService.performIdentityCheck(mapOf(other.requireServiceId() to identityPool[other]!!.identityKey)))
.thenReturn(Single.just(ServiceResponse.forUnknownError(IOException())))
repository.batchSafetyNumberCheckSync(keys, now)
staticIdentityUtil.verifyNoInteractions()
}
@Test
fun batchSafetyNumberCheckSync_badJson() {
val other = recipientPool[1]
val keys = listOf(ContactSearchKey.RecipientSearchKey.KnownRecipient(other.id))
staticRecipient.`when`<List<Recipient>> { Recipient.resolvedList(argThat { containsAll(keys.map { it.recipientId }) }) }.thenReturn(listOf(other))
whenever(profileService.performIdentityCheck(mapOf(other.requireServiceId() to identityPool[other]!!.identityKey)))
.thenReturn(Single.just(ServiceResponse.forResult(IdentityCheckResponse(), 200, "")))
repository.batchSafetyNumberCheckSync(keys, now)
staticIdentityUtil.verifyNoInteractions()
}
}