kopia lustrzana https://github.com/ryukoposting/Signal-Android
457 wiersze
17 KiB
Kotlin
457 wiersze
17 KiB
Kotlin
package org.thoughtcrime.securesms.groups.v2.processing
|
|
|
|
import android.app.Application
|
|
import androidx.test.core.app.ApplicationProvider
|
|
import org.hamcrest.MatcherAssert.assertThat
|
|
import org.hamcrest.Matchers.`is`
|
|
import org.junit.After
|
|
import org.junit.Before
|
|
import org.junit.Rule
|
|
import org.junit.Test
|
|
import org.junit.runner.RunWith
|
|
import org.mockito.ArgumentMatchers.eq
|
|
import org.mockito.Mockito.any
|
|
import org.mockito.Mockito.doReturn
|
|
import org.mockito.Mockito.isA
|
|
import org.mockito.Mockito.mock
|
|
import org.mockito.Mockito.reset
|
|
import org.mockito.Mockito.verify
|
|
import org.robolectric.RobolectricTestRunner
|
|
import org.robolectric.annotation.Config
|
|
import org.signal.core.util.Hex.fromStringCondensed
|
|
import org.signal.core.util.logging.Log
|
|
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
|
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
|
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
|
import org.signal.storageservice.protos.groups.local.DecryptedTimer
|
|
import org.thoughtcrime.securesms.SignalStoreRule
|
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
|
import org.thoughtcrime.securesms.database.GroupStateTestData
|
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
|
import org.thoughtcrime.securesms.database.model.databaseprotos.member
|
|
import org.thoughtcrime.securesms.database.model.databaseprotos.requestingMember
|
|
import org.thoughtcrime.securesms.database.setNewDescription
|
|
import org.thoughtcrime.securesms.database.setNewTitle
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
import org.thoughtcrime.securesms.groups.GroupId
|
|
import org.thoughtcrime.securesms.groups.GroupsV2Authorization
|
|
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
|
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api
|
|
import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup
|
|
import org.whispersystems.signalservice.api.push.ACI
|
|
import org.whispersystems.signalservice.api.push.PNI
|
|
import org.whispersystems.signalservice.api.push.ServiceId
|
|
import org.whispersystems.signalservice.api.push.ServiceIds
|
|
import java.util.UUID
|
|
|
|
@RunWith(RobolectricTestRunner::class)
|
|
@Config(manifest = Config.NONE, application = Application::class)
|
|
class GroupsV2StateProcessorTest {
|
|
|
|
companion object {
|
|
private val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
|
private val selfAci: ACI = ACI.from(UUID.randomUUID())
|
|
private val serviceIds: ServiceIds = ServiceIds(selfAci, PNI.from(UUID.randomUUID()))
|
|
private val otherSid: ServiceId = ServiceId.from(UUID.randomUUID())
|
|
private val selfAndOthers: List<DecryptedMember> = listOf(member(selfAci), member(otherSid))
|
|
private val others: List<DecryptedMember> = listOf(member(otherSid))
|
|
}
|
|
|
|
private lateinit var groupDatabase: GroupDatabase
|
|
private lateinit var recipientDatabase: RecipientDatabase
|
|
private lateinit var groupsV2API: GroupsV2Api
|
|
private lateinit var groupsV2Authorization: GroupsV2Authorization
|
|
private lateinit var profileAndMessageHelper: GroupsV2StateProcessor.ProfileAndMessageHelper
|
|
|
|
private lateinit var processor: GroupsV2StateProcessor.StateProcessorForGroup
|
|
|
|
@get:Rule
|
|
val signalStore: SignalStoreRule = SignalStoreRule()
|
|
|
|
@Before
|
|
fun setUp() {
|
|
Log.initialize(SystemOutLogger())
|
|
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
|
|
|
|
groupDatabase = mock(GroupDatabase::class.java)
|
|
recipientDatabase = mock(RecipientDatabase::class.java)
|
|
groupsV2API = mock(GroupsV2Api::class.java)
|
|
groupsV2Authorization = mock(GroupsV2Authorization::class.java)
|
|
profileAndMessageHelper = mock(GroupsV2StateProcessor.ProfileAndMessageHelper::class.java)
|
|
|
|
processor = GroupsV2StateProcessor.StateProcessorForGroup(serviceIds, ApplicationProvider.getApplicationContext(), groupDatabase, groupsV2API, groupsV2Authorization, masterKey, profileAndMessageHelper)
|
|
}
|
|
|
|
@After
|
|
fun tearDown() {
|
|
reset(ApplicationDependencies.getJobManager())
|
|
}
|
|
|
|
private fun given(init: GroupStateTestData.() -> Unit) {
|
|
val data = givenData(init)
|
|
|
|
doReturn(data.groupRecord).`when`(groupDatabase).getGroup(any(GroupId.V2::class.java))
|
|
doReturn(!data.groupRecord.isPresent).`when`(groupDatabase).isUnknownGroup(any())
|
|
|
|
data.serverState?.let { serverState ->
|
|
val testPartial = object : PartialDecryptedGroup(null, serverState, null, null) {
|
|
override fun getFullyDecryptedGroup(): DecryptedGroup {
|
|
return serverState
|
|
}
|
|
}
|
|
doReturn(testPartial).`when`(groupsV2API).getPartialDecryptedGroup(any(), any())
|
|
}
|
|
|
|
data.changeSet?.let { changeSet ->
|
|
doReturn(changeSet.toApiResponse()).`when`(groupsV2API).getGroupHistoryPage(any(), eq(data.requestedRevision), any(), eq(data.includeFirst))
|
|
}
|
|
}
|
|
|
|
private fun givenData(init: GroupStateTestData.() -> Unit): GroupStateTestData {
|
|
val data = GroupStateTestData(masterKey)
|
|
data.init()
|
|
return data
|
|
}
|
|
|
|
@Test
|
|
fun `when local revision matches server revision, then return consistent or ahead`() {
|
|
given {
|
|
localState(
|
|
revision = 5,
|
|
members = selfAndOthers
|
|
)
|
|
serverState(
|
|
revision = 5,
|
|
extendGroup = localState
|
|
)
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
|
assertThat("local and server match revisions", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_CONSISTENT_OR_AHEAD))
|
|
}
|
|
|
|
@Test
|
|
fun `when local revision is one less than latest server version, then update from server with group change only`() {
|
|
given {
|
|
localState(
|
|
revision = 5,
|
|
title = "Fdsa",
|
|
members = selfAndOthers
|
|
)
|
|
serverState(
|
|
revision = 6,
|
|
extendGroup = localState,
|
|
title = "Asdf"
|
|
)
|
|
changeSet {
|
|
changeLog(6) {
|
|
change {
|
|
setNewTitle("Asdf")
|
|
}
|
|
}
|
|
}
|
|
apiCallParameters(requestedRevision = 5, includeFirst = false)
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("title changed to match server", result.latestServer!!.title, `is`("Asdf"))
|
|
}
|
|
|
|
@Test
|
|
fun `when local revision is two less than server revision, then update from server with full group state and change`() {
|
|
given {
|
|
localState(
|
|
revision = 5,
|
|
title = "Fdsa",
|
|
members = selfAndOthers
|
|
)
|
|
serverState(
|
|
revision = 7,
|
|
title = "Asdf!"
|
|
)
|
|
changeSet {
|
|
changeLog(6) {
|
|
fullSnapshot(extendGroup = localState, title = "Asdf")
|
|
change {
|
|
setNewTitle("Asdf")
|
|
}
|
|
}
|
|
changeLog(7) {
|
|
change {
|
|
setNewTitle("Asdf!")
|
|
}
|
|
}
|
|
}
|
|
apiCallParameters(requestedRevision = 5, includeFirst = true)
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(7))
|
|
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!"))
|
|
}
|
|
|
|
@Test
|
|
fun `when change log returns a group state more than one higher than our local state, then still update to server state`() {
|
|
given {
|
|
localState(
|
|
revision = 100,
|
|
title = "To infinity",
|
|
members = selfAndOthers
|
|
)
|
|
serverState(
|
|
revision = 111,
|
|
title = "And beyond",
|
|
description = "Description"
|
|
)
|
|
changeSet {
|
|
changeLog(110) {
|
|
fullSnapshot(
|
|
extendGroup = localState,
|
|
title = "And beyond"
|
|
)
|
|
}
|
|
changeLog(111) {
|
|
change {
|
|
setNewDescription("Description")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(111))
|
|
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("And beyond"))
|
|
assertThat("Description updated in change after full snapshot", result.latestServer!!.description, `is`("Description"))
|
|
}
|
|
|
|
@Test
|
|
fun `when receiving peer change for next revision, then apply change without server call`() {
|
|
given {
|
|
localState(
|
|
revision = 5,
|
|
disappearingMessageTimer = DecryptedTimer.newBuilder().setDuration(1000).build()
|
|
)
|
|
}
|
|
|
|
val signedChange = DecryptedGroupChange.newBuilder().apply {
|
|
revision = 6
|
|
setNewTimer(DecryptedTimer.newBuilder().setDuration(5000))
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(6, 0, signedChange.build())
|
|
assertThat("revision matches peer change", result.latestServer!!.revision, `is`(6))
|
|
assertThat("timer changed by peer change", result.latestServer!!.disappearingMessagesTimer.duration, `is`(5000))
|
|
}
|
|
|
|
@Test
|
|
fun `when freshly added to a group, with no group changes after being added, then update from server at the revision we were added`() {
|
|
given {
|
|
serverState(
|
|
revision = 2,
|
|
title = "Breaking Signal for Science",
|
|
description = "We break stuff, because we must.",
|
|
members = listOf(member(otherSid), member(selfAci, joinedAt = 2))
|
|
)
|
|
changeSet {
|
|
changeLog(2) {
|
|
fullSnapshot(serverState)
|
|
}
|
|
}
|
|
apiCallParameters(2, true)
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(2, 0, DecryptedGroupChange.getDefaultInstance())
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(2))
|
|
}
|
|
|
|
@Test
|
|
fun `when freshly added to a group, with additional group changes after being added, then only update from server at the revision we were added, and then schedule pulling additional changes later`() {
|
|
given {
|
|
serverState(
|
|
revision = 3,
|
|
title = "Breaking Signal for Science",
|
|
description = "We break stuff, because we must.",
|
|
members = listOf(member(otherSid), member(selfAci, joinedAt = 2))
|
|
)
|
|
changeSet {
|
|
changeLog(2) {
|
|
fullSnapshot(serverState, title = "Baking Signal for Science")
|
|
}
|
|
changeLog(3) {
|
|
change {
|
|
setNewTitle("Breaking Signal for Science")
|
|
}
|
|
}
|
|
}
|
|
apiCallParameters(2, true)
|
|
}
|
|
|
|
doReturn(true).`when`(groupDatabase).isUnknownGroup(any())
|
|
|
|
val result = processor.updateLocalGroupToRevision(2, 0, DecryptedGroupChange.getDefaultInstance())
|
|
|
|
assertThat("local should update to revision added", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches peer revision added", result.latestServer!!.revision, `is`(2))
|
|
assertThat("title matches that as it was in revision added", result.latestServer!!.title, `is`("Baking Signal for Science"))
|
|
|
|
verify(ApplicationDependencies.getJobManager()).add(isA(RequestGroupV2InfoJob::class.java))
|
|
}
|
|
|
|
@Test
|
|
fun `when learning of a group via storage service, then update from server to latest revision`() {
|
|
given {
|
|
localState(
|
|
revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION
|
|
)
|
|
serverState(
|
|
revision = 10,
|
|
title = "Stargate Fan Club",
|
|
description = "Indeed.",
|
|
members = selfAndOthers
|
|
)
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
|
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches latest server", result.latestServer!!.revision, `is`(10))
|
|
}
|
|
|
|
@Test
|
|
fun `when request to join group is approved, with no group changes after approved, then update from server to revision we were added`() {
|
|
given {
|
|
localState(
|
|
revision = GroupsV2StateProcessor.PLACEHOLDER_REVISION,
|
|
title = "Beam me up",
|
|
requestingMembers = listOf(requestingMember(selfAci))
|
|
)
|
|
serverState(
|
|
revision = 3,
|
|
title = "Beam me up",
|
|
members = listOf(member(otherSid), member(selfAci, joinedAt = 3))
|
|
)
|
|
changeSet {
|
|
changeLog(3) {
|
|
fullSnapshot(serverState)
|
|
change {
|
|
addNewMembers(member(selfAci, joinedAt = 3))
|
|
}
|
|
}
|
|
}
|
|
apiCallParameters(requestedRevision = 3, includeFirst = true)
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(3, 0, null)
|
|
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(3))
|
|
}
|
|
|
|
@Test
|
|
fun `when request to join group is approved, with group changes occurring after approved, then update from server to revision we were added, and then schedule pulling additional changes later`() {
|
|
given {
|
|
localState(
|
|
revision = GroupsV2StateProcessor.PLACEHOLDER_REVISION,
|
|
title = "Beam me up",
|
|
requestingMembers = listOf(requestingMember(selfAci))
|
|
)
|
|
serverState(
|
|
revision = 5,
|
|
title = "Beam me up!",
|
|
members = listOf(member(otherSid), member(selfAci, joinedAt = 3))
|
|
)
|
|
changeSet {
|
|
changeLog(3) {
|
|
fullSnapshot(extendGroup = serverState, title = "Beam me up")
|
|
change {
|
|
addNewMembers(member(selfAci, joinedAt = 3))
|
|
}
|
|
}
|
|
changeLog(4) {
|
|
change {
|
|
setNewTitle("May the force be with you")
|
|
}
|
|
}
|
|
changeLog(5) {
|
|
change {
|
|
setNewTitle("Beam me up!")
|
|
}
|
|
}
|
|
}
|
|
apiCallParameters(requestedRevision = 3, includeFirst = true)
|
|
}
|
|
|
|
val result = processor.updateLocalGroupToRevision(3, 0, null)
|
|
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches revision approved at", result.latestServer!!.revision, `is`(3))
|
|
assertThat("title matches revision approved at", result.latestServer!!.title, `is`("Beam me up"))
|
|
verify(ApplicationDependencies.getJobManager()).add(isA(RequestGroupV2InfoJob::class.java))
|
|
}
|
|
|
|
@Test
|
|
fun `when failing to update fully to desired revision, then try again forcing inclusion of full group state, and then successfully update from server to latest revision`() {
|
|
val randomMembers = listOf(member(UUID.randomUUID()), member(UUID.randomUUID()))
|
|
given {
|
|
localState(
|
|
revision = 100,
|
|
title = "Title",
|
|
members = others
|
|
)
|
|
serverState(
|
|
extendGroup = localState,
|
|
revision = 101,
|
|
members = listOf(others[0], randomMembers[0], member(selfAci, joinedAt = 100))
|
|
)
|
|
changeSet {
|
|
changeLog(100) {
|
|
change {
|
|
addNewMembers(member(selfAci, joinedAt = 100))
|
|
}
|
|
}
|
|
changeLog(101) {
|
|
change {
|
|
addDeleteMembers(randomMembers[1].uuid)
|
|
addModifiedProfileKeys(randomMembers[0])
|
|
}
|
|
}
|
|
}
|
|
apiCallParameters(100, false)
|
|
}
|
|
|
|
val secondApiCallChangeSet = GroupStateTestData(masterKey).apply {
|
|
changeSet {
|
|
changeLog(100) {
|
|
fullSnapshot(
|
|
extendGroup = localState,
|
|
members = selfAndOthers + randomMembers[0] + randomMembers[1]
|
|
)
|
|
change {
|
|
addNewMembers(member(selfAci, joinedAt = 100))
|
|
}
|
|
}
|
|
changeLog(101) {
|
|
change {
|
|
addDeleteMembers(randomMembers[1].uuid)
|
|
addModifiedProfileKeys(randomMembers[0])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
doReturn(secondApiCallChangeSet.changeSet!!.toApiResponse()).`when`(groupsV2API).getGroupHistoryPage(any(), eq(100), any(), eq(true))
|
|
|
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
|
|
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
|
assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101))
|
|
}
|
|
}
|