mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-24 19:56:00 +00:00
Refactor group state processing.
This commit is contained in:
@@ -16,7 +16,7 @@ import org.signal.storageservice.protos.groups.local.EnabledState
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
@@ -67,7 +67,7 @@ class ChangeSet {
|
||||
}
|
||||
|
||||
fun toApiResponse(): GroupHistoryPage {
|
||||
return GroupHistoryPage(changeSet.map { DecryptedGroupHistoryEntry(Optional.ofNullable(it.groupSnapshot), Optional.ofNullable(it.groupChange)) }, GroupHistoryPage.PagingData.NONE)
|
||||
return GroupHistoryPage(changeSet.map { DecryptedGroupChangeLog(it.groupSnapshot, it.groupChange) }, GroupHistoryPage.PagingData.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,9 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou
|
||||
var groupChange: GroupChange? = null
|
||||
var includeFirst: Boolean = false
|
||||
var requestedRevision: Int = 0
|
||||
var expectTableCreate: Boolean = false
|
||||
var expectTableUpdate: Boolean = false
|
||||
var joinedAtRevision: Int? = null
|
||||
|
||||
fun localState(
|
||||
active: Boolean = true,
|
||||
|
||||
@@ -82,7 +82,7 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie
|
||||
|
||||
@Override
|
||||
public @NonNull JobManager provideJobManager() {
|
||||
return mock(JobManager.class);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class GlobalGroupStateTest {
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void cannot_ask_latestVersionNumber_of_empty_state() {
|
||||
GlobalGroupState emptyState = new GlobalGroupState(null, emptyList());
|
||||
|
||||
emptyState.getLatestRevisionNumber();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void latestRevisionNumber_of_state_and_empty_list() {
|
||||
GlobalGroupState emptyState = new GlobalGroupState(state(10), emptyList());
|
||||
|
||||
assertEquals(10, emptyState.getLatestRevisionNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void latestRevisionNumber_of_state_and_list() {
|
||||
GlobalGroupState emptyState = new GlobalGroupState(state(2), asList(logEntry(3), logEntry(4)));
|
||||
|
||||
assertEquals(4, emptyState.getLatestRevisionNumber());
|
||||
}
|
||||
|
||||
private static ServerGroupLogEntry logEntry(int revision) {
|
||||
return new ServerGroupLogEntry(state(revision), change(revision));
|
||||
}
|
||||
|
||||
private static DecryptedGroup state(int revision) {
|
||||
return new DecryptedGroup.Builder().revision(revision).build();
|
||||
}
|
||||
|
||||
private static DecryptedGroupChange change(int revision) {
|
||||
return new DecryptedGroupChange.Builder().revision(revision).build();
|
||||
}
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.signal.core.util.logging.Log;
|
||||
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.DecryptedString;
|
||||
import org.thoughtcrime.securesms.testutil.LogRecorder;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import kotlin.collections.CollectionsKt;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.groups.v2.processing.GroupStateMapper.LATEST;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
public final class GroupStateMapperTest {
|
||||
|
||||
private static final UUID KNOWN_EDITOR = UUID.randomUUID();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Log.initialize(new LogRecorder());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_group_with_no_states_to_update() {
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, emptyList()), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertNull(advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_with_no_states_to_update() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, emptyList()), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertSame(currentState, advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_group_single_state_to_update() {
|
||||
ServerGroupLogEntry log0 = serverLogEntry(0);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, singletonList(log0)), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log0.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_single_state_to_update() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log1)), 1);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log1))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log1.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_two_states_to_update() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_two_states_to_update_already_on_one() {
|
||||
DecryptedGroup currentState = state(1);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log2))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_stop_at_2() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||
assertNewState(new GlobalGroupState(log2.getGroup(), singletonList(log3)), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2), asLocal(log3))));
|
||||
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_maximum_group_revisions() {
|
||||
DecryptedGroup currentState = state(Integer.MAX_VALUE - 2);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(Integer.MAX_VALUE - 1);
|
||||
ServerGroupLogEntry log2 = serverLogEntry(Integer.MAX_VALUE);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||
assertNewState(new GlobalGroupState(log2.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_group_single_state_to_update_with_missing_change() {
|
||||
ServerGroupLogEntry log0 = serverLogEntryWholeStateOnly(0);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, singletonList(log0)), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log0.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_single_state_to_update_with_missing_change() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntryWholeStateOnly(1);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log1)), 1);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(localLogEntryNoEditor(1))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log1.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest_handle_missing_change() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
ServerGroupLogEntry log2 = serverLogEntryWholeStateOnly(2);
|
||||
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), localLogEntryNoEditor(2), asLocal(log3))));
|
||||
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest_handle_gap_with_no_changes() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log3)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log3))));
|
||||
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest_handle_gap_with_changes() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
DecryptedGroup state3a = new DecryptedGroup.Builder()
|
||||
.revision(3)
|
||||
.title("Group Revision " + 3)
|
||||
.build();
|
||||
DecryptedGroup state3 = new DecryptedGroup.Builder()
|
||||
.revision(3)
|
||||
.title("Group Revision " + 3)
|
||||
.avatar("Lost Avatar Update")
|
||||
.build();
|
||||
ServerGroupLogEntry log3 = new ServerGroupLogEntry(state3, change(3));
|
||||
DecryptedGroup state4 = new DecryptedGroup.Builder()
|
||||
.revision(4)
|
||||
.title("Group Revision " + 4)
|
||||
.avatar("Lost Avatar Update")
|
||||
.build();
|
||||
ServerGroupLogEntry log4 = new ServerGroupLogEntry(state4, change(4));
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log3, log4)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
|
||||
new LocalGroupLogEntry(state3a, log3.getChange()),
|
||||
new LocalGroupLogEntry(state3, new DecryptedGroupChange.Builder()
|
||||
.revision(3)
|
||||
.newAvatar(new DecryptedString.Builder().value_("Lost Avatar Update").build())
|
||||
.build()),
|
||||
asLocal(log4))));
|
||||
|
||||
assertNewState(new GlobalGroupState(log4.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log4.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updates_with_all_changes_missing() {
|
||||
DecryptedGroup currentState = state(5);
|
||||
ServerGroupLogEntry log6 = serverLogEntryWholeStateOnly(6);
|
||||
ServerGroupLogEntry log7 = serverLogEntryWholeStateOnly(7);
|
||||
ServerGroupLogEntry log8 = serverLogEntryWholeStateOnly(8);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log6, log7, log8)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(localLogEntryNoEditor(6), localLogEntryNoEditor(7), localLogEntryNoEditor(8))));
|
||||
assertNewState(new GlobalGroupState(log8.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updates_with_all_group_states_missing() {
|
||||
DecryptedGroup currentState = state(6);
|
||||
ServerGroupLogEntry log7 = logEntryMissingState(7);
|
||||
ServerGroupLogEntry log8 = logEntryMissingState(8);
|
||||
ServerGroupLogEntry log9 = logEntryMissingState(9);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log7, log8, log9)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(serverLogEntry(7)), asLocal(serverLogEntry(8)), asLocal(serverLogEntry(9)))));
|
||||
assertNewState(new GlobalGroupState(state(9), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(state(9), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updates_with_a_server_mismatch_inserts_additional_update() {
|
||||
DecryptedGroup currentState = state(6);
|
||||
ServerGroupLogEntry log7 = serverLogEntry(7);
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup state7b = new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.build();
|
||||
DecryptedGroup state8 = new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.members(Collections.singletonList(newMember))
|
||||
.build();
|
||||
ServerGroupLogEntry log8 = new ServerGroupLogEntry(state8,
|
||||
change(8));
|
||||
ServerGroupLogEntry log9 = new ServerGroupLogEntry(new DecryptedGroup.Builder()
|
||||
.revision(9)
|
||||
.members(Collections.singletonList(newMember))
|
||||
.title("Group Revision " + 9)
|
||||
.build(),
|
||||
change(9));
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log7, log8, log9)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log7),
|
||||
new LocalGroupLogEntry(state7b, log8.getChange()),
|
||||
new LocalGroupLogEntry(state8, new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build()),
|
||||
asLocal(log9))));
|
||||
assertNewState(new GlobalGroupState(log9.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log9.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void local_up_to_date_no_repair_necessary() {
|
||||
DecryptedGroup currentState = state(6);
|
||||
ServerGroupLogEntry log6 = serverLogEntryWholeStateOnly(6);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log6)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertNewState(new GlobalGroupState(state(6), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(state(6), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void no_repair_change_is_posted_if_the_local_state_is_a_placeholder() {
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(GroupStateMapper.PLACEHOLDER_REVISION)
|
||||
.title("Incorrect group title, Revision " + 6)
|
||||
.build();
|
||||
ServerGroupLogEntry log6 = serverLogEntry(6);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log6)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log6))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log6.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clears_changes_duplicated_in_the_placeholder() {
|
||||
ACI newMemberAci = ACI.from(UUID.randomUUID());
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build();
|
||||
DecryptedMember existingMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(GroupStateMapper.PLACEHOLDER_REVISION)
|
||||
.title("Group Revision " + 8)
|
||||
.members(Collections.singletonList(newMember))
|
||||
.build();
|
||||
ServerGroupLogEntry log8 = new ServerGroupLogEntry(new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(newMemberAci.toByteString())
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build());
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log8)), LATEST);
|
||||
|
||||
assertNotNull(log8.getGroup());
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertNewState(new GlobalGroupState(log8.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clears_changes_duplicated_in_a_non_placeholder() {
|
||||
ACI editorAci = ACI.from(UUID.randomUUID());
|
||||
ACI newMemberAci = ACI.from(UUID.randomUUID());
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build();
|
||||
DecryptedMember existingMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.members(Collections.singletonList(existingMember))
|
||||
.build();
|
||||
ServerGroupLogEntry log8 = new ServerGroupLogEntry(new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(editorAci.toByteString())
|
||||
.newMembers(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.build());
|
||||
|
||||
DecryptedGroupChange expectedChange = new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(editorAci.toByteString())
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build();
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log8)), LATEST);
|
||||
|
||||
assertNotNull(log8.getGroup());
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new LocalGroupLogEntry(log8.getGroup(), expectedChange))));
|
||||
assertNewState(new GlobalGroupState(log8.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notices_changes_in_avatar_and_title_but_not_members_in_placeholder() {
|
||||
ACI newMemberAci = ACI.from(UUID.randomUUID());
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build();
|
||||
DecryptedMember existingMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(GroupStateMapper.PLACEHOLDER_REVISION)
|
||||
.title("Incorrect group title")
|
||||
.avatar("Incorrect group avatar")
|
||||
.members(Collections.singletonList(newMember))
|
||||
.build();
|
||||
ServerGroupLogEntry log8 = new ServerGroupLogEntry(new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.avatar("Group Avatar " + 8)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(newMemberAci.toByteString())
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build());
|
||||
|
||||
DecryptedGroupChange expectedChange = new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.newTitle(new DecryptedString.Builder().value_("Group Revision " + 8).build())
|
||||
.newAvatar(new DecryptedString.Builder().value_("Group Avatar " + 8).build())
|
||||
.build();
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log8)), LATEST);
|
||||
|
||||
assertNotNull(log8.getGroup());
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new LocalGroupLogEntry(log8.getGroup(), expectedChange))));
|
||||
assertNewState(new GlobalGroupState(log8.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void no_actual_change() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||
ServerGroupLogEntry log2 = new ServerGroupLogEntry(log1.getGroup().newBuilder()
|
||||
.revision(2)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(2)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.newTitle(new DecryptedString.Builder().value_(log1.getGroup().title).build())
|
||||
.build());
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
|
||||
new LocalGroupLogEntry(log2.getGroup(), new DecryptedGroupChange.Builder()
|
||||
.revision(2)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.build()))));
|
||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||
}
|
||||
|
||||
private static void assertNewState(GlobalGroupState expected, GlobalGroupState actual) {
|
||||
assertEquals(expected.getLocalState(), actual.getLocalState());
|
||||
assertThat(actual.getServerHistory(), is(expected.getServerHistory()));
|
||||
}
|
||||
|
||||
private static ServerGroupLogEntry serverLogEntry(int revision) {
|
||||
return new ServerGroupLogEntry(state(revision), change(revision));
|
||||
}
|
||||
|
||||
private static LocalGroupLogEntry localLogEntryNoEditor(int revision) {
|
||||
return new LocalGroupLogEntry(state(revision), changeNoEditor(revision));
|
||||
}
|
||||
|
||||
private static ServerGroupLogEntry serverLogEntryWholeStateOnly(int revision) {
|
||||
return new ServerGroupLogEntry(state(revision), null);
|
||||
}
|
||||
|
||||
private static ServerGroupLogEntry logEntryMissingState(int revision) {
|
||||
return new ServerGroupLogEntry(null, change(revision));
|
||||
}
|
||||
|
||||
private static DecryptedGroup state(int revision) {
|
||||
return new DecryptedGroup.Builder()
|
||||
.revision(revision)
|
||||
.title("Group Revision " + revision)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedGroupChange change(int revision) {
|
||||
return new DecryptedGroupChange.Builder()
|
||||
.revision(revision)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.newTitle(new DecryptedString.Builder().value_("Group Revision " + revision).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedGroupChange changeNoEditor(int revision) {
|
||||
return new DecryptedGroupChange.Builder()
|
||||
.revision(revision)
|
||||
.newTitle(new DecryptedString.Builder().value_("Group Revision " + revision).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static LocalGroupLogEntry asLocal(ServerGroupLogEntry logEntry) {
|
||||
assertNotNull(logEntry.getGroup());
|
||||
return new LocalGroupLogEntry(logEntry.getGroup(), logEntry.getChange());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.signal.core.util.logging.Log;
|
||||
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.DecryptedString;
|
||||
import org.thoughtcrime.securesms.testutil.LogRecorder;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import kotlin.collections.CollectionsKt;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.groups.v2.processing.GroupStatePatcher.LATEST;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
public final class GroupStatePatcherTest {
|
||||
|
||||
private static final UUID KNOWN_EDITOR = UUID.randomUUID();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Log.initialize(new LogRecorder());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_group_with_no_states_to_update() {
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, emptyList()), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertNull(advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_with_no_states_to_update() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, emptyList()), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertSame(currentState, advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_group_single_state_to_update() {
|
||||
DecryptedGroupChangeLog log0 = serverLogEntry(0);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0)), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log0.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_single_state_to_update() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1)), 1);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log1))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log1.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_two_states_to_update() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroupChangeLog log2 = serverLogEntry(2);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_two_states_to_update_already_on_one() {
|
||||
DecryptedGroup currentState = state(1);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroupChangeLog log2 = serverLogEntry(2);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log2))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_stop_at_2() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroupChangeLog log2 = serverLogEntry(2);
|
||||
DecryptedGroupChangeLog log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||
assertNewState(log2.getGroup(), singletonList(log3), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroupChangeLog log2 = serverLogEntry(2);
|
||||
DecryptedGroupChangeLog log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2), asLocal(log3))));
|
||||
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log3.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void apply_maximum_group_revisions() {
|
||||
DecryptedGroup currentState = state(Integer.MAX_VALUE - 2);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(Integer.MAX_VALUE - 1);
|
||||
DecryptedGroupChangeLog log2 = serverLogEntry(Integer.MAX_VALUE);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||
assertNewState(log2.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_group_single_state_to_update_with_missing_change() {
|
||||
DecryptedGroupChangeLog log0 = serverLogEntryWholeStateOnly(0);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0)), 10);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log0.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_single_state_to_update_with_missing_change() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntryWholeStateOnly(1);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1)), 1);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(localLogEntryNoEditor(1))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log1.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest_handle_missing_change() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroupChangeLog log2 = serverLogEntryWholeStateOnly(2);
|
||||
DecryptedGroupChangeLog log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), localLogEntryNoEditor(2), asLocal(log3))));
|
||||
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log3.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest_handle_gap_with_no_changes() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroupChangeLog log3 = serverLogEntry(3);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log3))));
|
||||
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log3.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void known_group_three_states_to_update_update_latest_handle_gap_with_changes() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroup state3a = new DecryptedGroup.Builder()
|
||||
.revision(3)
|
||||
.title("Group Revision " + 3)
|
||||
.build();
|
||||
DecryptedGroup state3 = new DecryptedGroup.Builder()
|
||||
.revision(3)
|
||||
.title("Group Revision " + 3)
|
||||
.avatar("Lost Avatar Update")
|
||||
.build();
|
||||
DecryptedGroupChangeLog log3 = new DecryptedGroupChangeLog(state3, change(3));
|
||||
DecryptedGroup state4 = new DecryptedGroup.Builder()
|
||||
.revision(4)
|
||||
.title("Group Revision " + 4)
|
||||
.avatar("Lost Avatar Update")
|
||||
.build();
|
||||
DecryptedGroupChangeLog log4 = new DecryptedGroupChangeLog(state4, change(4));
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3, log4)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
|
||||
new AppliedGroupChangeLog(state3a, log3.getChange()),
|
||||
new AppliedGroupChangeLog(state3, new DecryptedGroupChange.Builder()
|
||||
.revision(3)
|
||||
.newAvatar(new DecryptedString.Builder().value_("Lost Avatar Update").build())
|
||||
.build()),
|
||||
asLocal(log4))));
|
||||
|
||||
assertNewState(log4.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log4.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updates_with_all_changes_missing() {
|
||||
DecryptedGroup currentState = state(5);
|
||||
DecryptedGroupChangeLog log6 = serverLogEntryWholeStateOnly(6);
|
||||
DecryptedGroupChangeLog log7 = serverLogEntryWholeStateOnly(7);
|
||||
DecryptedGroupChangeLog log8 = serverLogEntryWholeStateOnly(8);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log6, log7, log8)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(localLogEntryNoEditor(6), localLogEntryNoEditor(7), localLogEntryNoEditor(8))));
|
||||
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updates_with_all_group_states_missing() {
|
||||
DecryptedGroup currentState = state(6);
|
||||
DecryptedGroupChangeLog log7 = logEntryMissingState(7);
|
||||
DecryptedGroupChangeLog log8 = logEntryMissingState(8);
|
||||
DecryptedGroupChangeLog log9 = logEntryMissingState(9);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(serverLogEntry(7)), asLocal(serverLogEntry(8)), asLocal(serverLogEntry(9)))));
|
||||
assertNewState(state(9), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(state(9), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updates_with_a_server_mismatch_inserts_additional_update() {
|
||||
DecryptedGroup currentState = state(6);
|
||||
DecryptedGroupChangeLog log7 = serverLogEntry(7);
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup state7b = new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.build();
|
||||
DecryptedGroup state8 = new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.members(Collections.singletonList(newMember))
|
||||
.build();
|
||||
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(state8,
|
||||
change(8));
|
||||
DecryptedGroupChangeLog log9 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
|
||||
.revision(9)
|
||||
.members(Collections.singletonList(newMember))
|
||||
.title("Group Revision " + 9)
|
||||
.build(),
|
||||
change(9));
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log7),
|
||||
new AppliedGroupChangeLog(state7b, log8.getChange()),
|
||||
new AppliedGroupChangeLog(state8, new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build()),
|
||||
asLocal(log9))));
|
||||
assertNewState(log9.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log9.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void local_up_to_date_no_repair_necessary() {
|
||||
DecryptedGroup currentState = state(6);
|
||||
DecryptedGroupChangeLog log6 = serverLogEntryWholeStateOnly(6);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertNewState(state(6), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(state(6), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void no_repair_change_is_posted_if_the_local_state_is_a_placeholder() {
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
|
||||
.title("Incorrect group title, Revision " + 6)
|
||||
.build();
|
||||
DecryptedGroupChangeLog log6 = serverLogEntry(6);
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6)), LATEST);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log6))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log6.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clears_changes_duplicated_in_the_placeholder() {
|
||||
ACI newMemberAci = ACI.from(UUID.randomUUID());
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build();
|
||||
DecryptedMember existingMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
|
||||
.title("Group Revision " + 8)
|
||||
.members(Collections.singletonList(newMember))
|
||||
.build();
|
||||
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(newMemberAci.toByteString())
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build());
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8)), LATEST);
|
||||
|
||||
assertNotNull(log8.getGroup());
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clears_changes_duplicated_in_a_non_placeholder() {
|
||||
ACI editorAci = ACI.from(UUID.randomUUID());
|
||||
ACI newMemberAci = ACI.from(UUID.randomUUID());
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build();
|
||||
DecryptedMember existingMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.members(Collections.singletonList(existingMember))
|
||||
.build();
|
||||
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(editorAci.toByteString())
|
||||
.newMembers(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.build());
|
||||
|
||||
DecryptedGroupChange expectedChange = new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(editorAci.toByteString())
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build();
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8)), LATEST);
|
||||
|
||||
assertNotNull(log8.getGroup());
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange))));
|
||||
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notices_changes_in_avatar_and_title_but_not_members_in_placeholder() {
|
||||
ACI newMemberAci = ACI.from(UUID.randomUUID());
|
||||
DecryptedMember newMember = new DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build();
|
||||
DecryptedMember existingMember = new DecryptedMember.Builder()
|
||||
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build();
|
||||
DecryptedGroup currentState = new DecryptedGroup.Builder()
|
||||
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
|
||||
.title("Incorrect group title")
|
||||
.avatar("Incorrect group avatar")
|
||||
.members(Collections.singletonList(newMember))
|
||||
.build();
|
||||
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.avatar("Group Avatar " + 8)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(newMemberAci.toByteString())
|
||||
.newMembers(Collections.singletonList(newMember))
|
||||
.build());
|
||||
|
||||
DecryptedGroupChange expectedChange = new DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.newTitle(new DecryptedString.Builder().value_("Group Revision " + 8).build())
|
||||
.newAvatar(new DecryptedString.Builder().value_("Group Avatar " + 8).build())
|
||||
.build();
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8)), LATEST);
|
||||
|
||||
assertNotNull(log8.getGroup());
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange))));
|
||||
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
|
||||
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void no_actual_change() {
|
||||
DecryptedGroup currentState = state(0);
|
||||
DecryptedGroupChangeLog log1 = serverLogEntry(1);
|
||||
DecryptedGroupChangeLog log2 = new DecryptedGroupChangeLog(log1.getGroup().newBuilder()
|
||||
.revision(2)
|
||||
.build(),
|
||||
new DecryptedGroupChange.Builder()
|
||||
.revision(2)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.newTitle(new DecryptedString.Builder().value_(log1.getGroup().title).build())
|
||||
.build());
|
||||
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), 2);
|
||||
|
||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
|
||||
new AppliedGroupChangeLog(log2.getGroup(), new DecryptedGroupChange.Builder()
|
||||
.revision(2)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.build()))));
|
||||
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
|
||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
|
||||
}
|
||||
|
||||
private static void assertNewState(DecryptedGroup expectedUpdatedGroupState, List<DecryptedGroupChangeLog> expectedRemainingLogs, DecryptedGroup updatedGroupState, List<DecryptedGroupChangeLog> remainingLogs) {
|
||||
assertEquals(expectedUpdatedGroupState, updatedGroupState);
|
||||
assertThat(remainingLogs, is(expectedRemainingLogs));
|
||||
}
|
||||
|
||||
private static DecryptedGroupChangeLog serverLogEntry(int revision) {
|
||||
return new DecryptedGroupChangeLog(state(revision), change(revision));
|
||||
}
|
||||
|
||||
private static AppliedGroupChangeLog localLogEntryNoEditor(int revision) {
|
||||
return new AppliedGroupChangeLog(state(revision), changeNoEditor(revision));
|
||||
}
|
||||
|
||||
private static DecryptedGroupChangeLog serverLogEntryWholeStateOnly(int revision) {
|
||||
return new DecryptedGroupChangeLog(state(revision), null);
|
||||
}
|
||||
|
||||
private static DecryptedGroupChangeLog logEntryMissingState(int revision) {
|
||||
return new DecryptedGroupChangeLog(null, change(revision));
|
||||
}
|
||||
|
||||
private static DecryptedGroup state(int revision) {
|
||||
return new DecryptedGroup.Builder()
|
||||
.revision(revision)
|
||||
.title("Group Revision " + revision)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedGroupChange change(int revision) {
|
||||
return new DecryptedGroupChange.Builder()
|
||||
.revision(revision)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.newTitle(new DecryptedString.Builder().value_("Group Revision " + revision).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedGroupChange changeNoEditor(int revision) {
|
||||
return new DecryptedGroupChange.Builder()
|
||||
.revision(revision)
|
||||
.newTitle(new DecryptedString.Builder().value_("Group Revision " + revision).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static AppliedGroupChangeLog asLocal(DecryptedGroupChangeLog logEntry) {
|
||||
assertNotNull(logEntry.getGroup());
|
||||
return new AppliedGroupChangeLog(logEntry.getGroup(), logEntry.getChange());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package org.thoughtcrime.securesms.groups.v2.processing
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.spyk
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.hasItem
|
||||
@@ -18,40 +24,59 @@ 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.VerificationFailedException
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
|
||||
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.DecryptedString
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer
|
||||
import org.thoughtcrime.securesms.SignalStoreRule
|
||||
import org.thoughtcrime.securesms.database.GroupStateTestData
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.member
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.pendingMember
|
||||
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.GroupNotAMemberException
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2Authorization
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor.ProfileAndMessageHelper
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api
|
||||
import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup
|
||||
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
@SuppressLint("CheckResult")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class GroupsV2StateProcessorTest {
|
||||
|
||||
companion object {
|
||||
private val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val secretParams = GroupSecretParams.deriveFromMasterKey(masterKey)
|
||||
private val groupId = GroupId.v2(masterKey)
|
||||
private val selfAci: ACI = ACI.from(UUID.randomUUID())
|
||||
private val serviceIds: ServiceIds = ServiceIds(selfAci, PNI.from(UUID.randomUUID()))
|
||||
private val otherAci: ACI = ACI.from(UUID.randomUUID())
|
||||
@@ -63,10 +88,10 @@ class GroupsV2StateProcessorTest {
|
||||
private lateinit var recipientTable: RecipientTable
|
||||
private lateinit var groupsV2API: GroupsV2Api
|
||||
private lateinit var groupsV2Authorization: GroupsV2Authorization
|
||||
private lateinit var profileAndMessageHelper: GroupsV2StateProcessor.ProfileAndMessageHelper
|
||||
private lateinit var profileAndMessageHelper: ProfileAndMessageHelper
|
||||
private lateinit var jobManager: JobManager
|
||||
|
||||
private lateinit var processor: GroupsV2StateProcessor.StateProcessorForGroup
|
||||
private lateinit var processor: GroupsV2StateProcessor
|
||||
|
||||
@get:Rule
|
||||
val signalStore: SignalStoreRule = SignalStoreRule()
|
||||
@@ -76,22 +101,34 @@ class GroupsV2StateProcessorTest {
|
||||
Log.initialize(SystemOutLogger())
|
||||
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
|
||||
|
||||
groupTable = mockk(relaxed = true)
|
||||
groupTable = mockk()
|
||||
recipientTable = mockk()
|
||||
groupsV2API = mockk()
|
||||
groupsV2Authorization = mockk(relaxed = true)
|
||||
profileAndMessageHelper = mockk(relaxed = true)
|
||||
jobManager = mockk(relaxed = true)
|
||||
groupsV2Authorization = mockk()
|
||||
profileAndMessageHelper = spyk(ProfileAndMessageHelper(serviceIds.aci, masterKey, groupId))
|
||||
jobManager = mockk()
|
||||
|
||||
mockkStatic(ApplicationDependencies::class)
|
||||
every { ApplicationDependencies.getJobManager() } returns jobManager
|
||||
every { ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api() } returns groupsV2API
|
||||
every { ApplicationDependencies.getGroupsV2Authorization() } returns groupsV2Authorization
|
||||
|
||||
processor = GroupsV2StateProcessor.StateProcessorForGroup(serviceIds, groupTable, groupsV2API, groupsV2Authorization, masterKey, profileAndMessageHelper)
|
||||
mockkObject(SignalDatabase)
|
||||
every { SignalDatabase.groups } returns groupTable
|
||||
every { SignalDatabase.recipients } returns recipientTable
|
||||
|
||||
mockkObject(ProfileAndMessageHelper)
|
||||
every { ProfileAndMessageHelper.create(any(), any(), any()) } returns profileAndMessageHelper
|
||||
|
||||
processor = GroupsV2StateProcessor.forGroup(serviceIds, masterKey, secretParams)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
// reset(ApplicationDependencies.getJobManager())
|
||||
unmockkStatic(ApplicationDependencies::class)
|
||||
unmockkObject(SignalDatabase)
|
||||
unmockkObject(ProfileAndMessageHelper)
|
||||
unmockkStatic(DecryptedGroupUtil::class)
|
||||
}
|
||||
|
||||
private fun given(init: GroupStateTestData.() -> Unit) {
|
||||
@@ -99,21 +136,37 @@ class GroupsV2StateProcessorTest {
|
||||
|
||||
every { groupTable.getGroup(any<GroupId.V2>()) } returns data.groupRecord
|
||||
every { groupTable.isUnknownGroup(any<GroupId>()) } returns !data.groupRecord.isPresent
|
||||
every { groupTable.isUnknownGroup(any<Optional<GroupRecord>>()) } returns !data.groupRecord.isPresent
|
||||
every { groupTable.isActive(groupId) } returns data.groupRecord.map { it.isActive }.orElse(false)
|
||||
|
||||
every { groupsV2Authorization.getAuthorizationForToday(serviceIds, secretParams) } returns null
|
||||
|
||||
if (data.expectTableUpdate) {
|
||||
justRun { groupTable.update(any<GroupMasterKey>(), any<DecryptedGroup>()) }
|
||||
}
|
||||
|
||||
if (data.expectTableCreate) {
|
||||
every { groupTable.create(any<GroupMasterKey>(), any<DecryptedGroup>()) } returns groupId
|
||||
}
|
||||
|
||||
if (data.expectTableUpdate || data.expectTableCreate) {
|
||||
justRun { profileAndMessageHelper.storeMessage(any(), any(), any()) }
|
||||
justRun { profileAndMessageHelper.persistLearnedProfileKeys(any<ProfileKeySet>()) }
|
||||
}
|
||||
|
||||
data.serverState?.let { serverState ->
|
||||
val testPartial = object : PartialDecryptedGroup(null, serverState, null, null) {
|
||||
override fun getFullyDecryptedGroup(): DecryptedGroup {
|
||||
return serverState
|
||||
}
|
||||
}
|
||||
|
||||
every { groupsV2API.getPartialDecryptedGroup(any(), any()) } returns testPartial
|
||||
every { groupsV2API.getGroup(any(), any()) } returns serverState
|
||||
}
|
||||
|
||||
data.changeSet?.let { changeSet ->
|
||||
every { groupsV2API.getGroupHistoryPage(any(), data.requestedRevision, any(), data.includeFirst) } returns changeSet.toApiResponse()
|
||||
}
|
||||
|
||||
every { groupsV2API.getGroupAsResult(any(), any()) } answers { callOriginal() }
|
||||
|
||||
data.joinedAtRevision?.let { joinedAt ->
|
||||
every { groupsV2API.getGroupJoinedAt(any()) } returns NetworkResult.Success(joinedAt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenData(init: GroupStateTestData.() -> Unit): GroupStateTestData {
|
||||
@@ -129,14 +182,35 @@ class GroupsV2StateProcessorTest {
|
||||
revision = 5,
|
||||
members = selfAndOthers
|
||||
)
|
||||
serverState(
|
||||
changeSet {
|
||||
}
|
||||
apiCallParameters(requestedRevision = 5, includeFirst = false)
|
||||
joinedAtRevision = 0
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local and server match revisions", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_CONSISTENT_OR_AHEAD))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when local revision matches requested revision, then return consistent or ahead`() {
|
||||
given {
|
||||
localState(
|
||||
revision = 5,
|
||||
extendGroup = localState
|
||||
members = selfAndOthers
|
||||
)
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||
assertThat("local and server match revisions", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_CONSISTENT_OR_AHEAD))
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 5,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local and server match revisions", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_CONSISTENT_OR_AHEAD))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -147,11 +221,6 @@ class GroupsV2StateProcessorTest {
|
||||
title = "Fdsa",
|
||||
members = selfAndOthers
|
||||
)
|
||||
serverState(
|
||||
revision = 6,
|
||||
extendGroup = localState,
|
||||
title = "Asdf"
|
||||
)
|
||||
changeSet {
|
||||
changeLog(6) {
|
||||
change {
|
||||
@@ -160,11 +229,19 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
apiCallParameters(requestedRevision = 5, includeFirst = false)
|
||||
joinedAtRevision = 0
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("title changed to match server", result.latestServer!!.title, `is`("Asdf"))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -175,10 +252,6 @@ class GroupsV2StateProcessorTest {
|
||||
title = "Fdsa",
|
||||
members = selfAndOthers
|
||||
)
|
||||
serverState(
|
||||
revision = 7,
|
||||
title = "Asdf!"
|
||||
)
|
||||
changeSet {
|
||||
changeLog(6) {
|
||||
fullSnapshot(extendGroup = localState, title = "Asdf")
|
||||
@@ -192,13 +265,21 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
apiCallParameters(requestedRevision = 5, includeFirst = true)
|
||||
apiCallParameters(requestedRevision = 5, includeFirst = false)
|
||||
joinedAtRevision = 0
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches server", result.latestServer!!.revision, `is`(7))
|
||||
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!"))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -227,13 +308,22 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
apiCallParameters(requestedRevision = 100, includeFirst = false)
|
||||
joinedAtRevision = 0
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.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"))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -241,54 +331,201 @@ class GroupsV2StateProcessorTest {
|
||||
given {
|
||||
localState(
|
||||
revision = 5,
|
||||
disappearingMessageTimer = DecryptedTimer.Builder().duration(1000).build()
|
||||
disappearingMessageTimer = DecryptedTimer(1000)
|
||||
)
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val signedChange = DecryptedGroupChange.Builder().apply {
|
||||
revision = 6
|
||||
newTimer(DecryptedTimer.Builder().duration(5000).build())
|
||||
}
|
||||
val signedChange = DecryptedGroupChange(
|
||||
revision = 6,
|
||||
newTimer = DecryptedTimer(duration = 5000)
|
||||
)
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 6,
|
||||
timestamp = 0,
|
||||
signedGroupChange = signedChange,
|
||||
serverGuid = UUID.randomUUID().toString()
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@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`() {
|
||||
fun applyP2PPromotePendingPni() {
|
||||
given {
|
||||
localState(
|
||||
revision = 5,
|
||||
members = others,
|
||||
pendingMembers = listOf(pendingMember(serviceIds.pni))
|
||||
)
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val signedChange = DecryptedGroupChange(
|
||||
revision = 6,
|
||||
promotePendingPniAciMembers = listOf(member(selfAci).copy(pniBytes = serviceIds.pni.toByteString()))
|
||||
)
|
||||
|
||||
justRun { jobManager.add(any()) }
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 6,
|
||||
timestamp = 0,
|
||||
signedGroupChange = signedChange,
|
||||
serverGuid = UUID.randomUUID().toString()
|
||||
)
|
||||
|
||||
assertThat("revision matches peer change", result.latestServer!!.revision, `is`(6))
|
||||
assertThat("member promoted by peer change", result.latestServer!!.members.map { it.aciBytes }, hasItem(selfAci.toByteString()))
|
||||
|
||||
verify { jobManager.add(ofType(DirectoryRefreshJob::class)) }
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateFromServerIfUnableToApplyP2PChange() {
|
||||
given {
|
||||
localState(
|
||||
revision = 1,
|
||||
members = selfAndOthers
|
||||
)
|
||||
serverState(
|
||||
revision = 2,
|
||||
title = "Breaking Signal for Science",
|
||||
description = "We break stuff, because we must.",
|
||||
members = listOf(member(otherAci), member(selfAci, joinedAt = 2))
|
||||
members = selfAndOthers
|
||||
)
|
||||
changeSet {
|
||||
changeLog(2) {
|
||||
fullSnapshot(serverState)
|
||||
}
|
||||
}
|
||||
apiCallParameters(2, true)
|
||||
apiCallParameters(1, false)
|
||||
joinedAtRevision = 0
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(2, 0, DecryptedGroupChange())
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
mockkStatic(DecryptedGroupUtil::class)
|
||||
every { DecryptedGroupUtil.apply(any(), any()) } throws NotAbleToApplyGroupV2ChangeException()
|
||||
|
||||
val signedChange = DecryptedGroupChange(
|
||||
revision = 2,
|
||||
newTitle = DecryptedString("Breaking Signal for Science")
|
||||
)
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 2,
|
||||
timestamp = 0,
|
||||
signedGroupChange = signedChange,
|
||||
serverGuid = UUID.randomUUID().toString()
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches server", result.latestServer!!.revision, `is`(2))
|
||||
|
||||
verify { groupsV2API.getGroupHistoryPage(secretParams, 1, any(), false) }
|
||||
|
||||
unmockkStatic(DecryptedGroupUtil::class)
|
||||
}
|
||||
|
||||
@Test(expected = GroupNotAMemberException::class)
|
||||
fun skipP2PChangeForGroupNotIn() {
|
||||
given {
|
||||
localState(
|
||||
revision = 1,
|
||||
members = others,
|
||||
active = false
|
||||
)
|
||||
}
|
||||
|
||||
every { groupsV2API.getGroupJoinedAt(any()) } returns NetworkResult.StatusCodeError(NotInGroupException())
|
||||
|
||||
val signedChange = DecryptedGroupChange(
|
||||
revision = 2,
|
||||
newTitle = DecryptedString("Breaking Signal for Science"),
|
||||
newDescription = DecryptedString("We break stuff, because we must.")
|
||||
)
|
||||
|
||||
processor.updateLocalGroupToRevision(
|
||||
targetRevision = 2,
|
||||
timestamp = 0,
|
||||
signedGroupChange = signedChange,
|
||||
serverGuid = UUID.randomUUID().toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun applyP2PChangeForGroupWeThinkAreIn() {
|
||||
given {
|
||||
localState(
|
||||
revision = 1,
|
||||
members = others,
|
||||
active = false
|
||||
)
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
every { groupsV2API.getGroupJoinedAt(any()) } returns NetworkResult.StatusCodeError(NotInGroupException())
|
||||
|
||||
val signedChange = DecryptedGroupChange(
|
||||
revision = 3,
|
||||
newMembers = listOf(member(selfAci))
|
||||
)
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0,
|
||||
signedGroupChange = signedChange,
|
||||
serverGuid = UUID.randomUUID().toString()
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches server", result.latestServer!!.revision, `is`(3))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@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 {
|
||||
changeSet {
|
||||
changeLog(2) {
|
||||
fullSnapshot(
|
||||
title = "Breaking Signal for Science",
|
||||
description = "We break stuff, because we must.",
|
||||
members = listOf(member(otherAci), member(selfAci, joinedAt = 2))
|
||||
)
|
||||
}
|
||||
}
|
||||
apiCallParameters(2, true)
|
||||
joinedAtRevision = 2
|
||||
expectTableCreate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 2,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches server", result.latestServer!!.revision, `is`(2))
|
||||
|
||||
verify { groupTable.create(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@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(otherAci), member(selfAci, joinedAt = 2))
|
||||
)
|
||||
changeSet {
|
||||
changeLog(2) {
|
||||
fullSnapshot(serverState, title = "Baking Signal for Science")
|
||||
fullSnapshot(
|
||||
title = "Baking Signal for Science",
|
||||
description = "We break stuff, because we must.",
|
||||
members = listOf(member(otherAci), member(selfAci, joinedAt = 2))
|
||||
)
|
||||
}
|
||||
changeLog(3) {
|
||||
change {
|
||||
@@ -297,16 +534,24 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
apiCallParameters(2, true)
|
||||
joinedAtRevision = 2
|
||||
expectTableCreate = true
|
||||
}
|
||||
|
||||
every { groupTable.isUnknownGroup(any<GroupId>()) } returns true
|
||||
justRun { jobManager.add(any()) }
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(2, 0, DecryptedGroupChange())
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 2,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to revision added", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
assertThat("local should update to revision added", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.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 { jobManager.add(ofType(RequestGroupV2InfoJob::class)) }
|
||||
verify { groupTable.create(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -321,12 +566,18 @@ class GroupsV2StateProcessorTest {
|
||||
description = "Indeed.",
|
||||
members = selfAndOthers
|
||||
)
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches latest server", result.latestServer!!.revision, `is`(10))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -337,26 +588,32 @@ class GroupsV2StateProcessorTest {
|
||||
title = "Beam me up",
|
||||
requestingMembers = listOf(requestingMember(selfAci))
|
||||
)
|
||||
serverState(
|
||||
revision = 3,
|
||||
title = "Beam me up",
|
||||
members = listOf(member(otherAci), member(selfAci, joinedAt = 3))
|
||||
)
|
||||
|
||||
changeSet {
|
||||
changeLog(3) {
|
||||
fullSnapshot(serverState)
|
||||
fullSnapshot(
|
||||
title = "Beam me up",
|
||||
members = listOf(member(otherAci), member(selfAci, joinedAt = 3))
|
||||
)
|
||||
change {
|
||||
newMembers += member(selfAci, joinedAt = 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
apiCallParameters(requestedRevision = 3, includeFirst = true)
|
||||
joinedAtRevision = 3
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(3, 0, null)
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 3,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches server", result.latestServer!!.revision, `is`(3))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -367,14 +624,12 @@ class GroupsV2StateProcessorTest {
|
||||
title = "Beam me up",
|
||||
requestingMembers = listOf(requestingMember(selfAci))
|
||||
)
|
||||
serverState(
|
||||
revision = 5,
|
||||
title = "Beam me up!",
|
||||
members = listOf(member(otherAci), member(selfAci, joinedAt = 3))
|
||||
)
|
||||
changeSet {
|
||||
changeLog(3) {
|
||||
fullSnapshot(extendGroup = serverState, title = "Beam me up")
|
||||
fullSnapshot(
|
||||
title = "Beam me up",
|
||||
members = listOf(member(otherAci), member(selfAci, joinedAt = 3))
|
||||
)
|
||||
change {
|
||||
newMembers += member(selfAci, joinedAt = 3)
|
||||
}
|
||||
@@ -391,47 +646,35 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
apiCallParameters(requestedRevision = 3, includeFirst = true)
|
||||
joinedAtRevision = 3
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(3, 0, null)
|
||||
justRun { jobManager.add(any()) }
|
||||
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = 3,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.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 { jobManager.add(ofType(RequestGroupV2InfoJob::class)) }
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
@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`() {
|
||||
fun `when local state for same revision does not match server, 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 {
|
||||
newMembers += member(selfAci, joinedAt = 100)
|
||||
}
|
||||
}
|
||||
changeLog(101) {
|
||||
change {
|
||||
deleteMembers += randomMembers[1].aciBytes
|
||||
modifiedProfileKeys += randomMembers[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
apiCallParameters(100, false)
|
||||
}
|
||||
|
||||
val secondApiCallChangeSet = GroupStateTestData(masterKey).apply {
|
||||
changeSet {
|
||||
changeLog(100) {
|
||||
fullSnapshot(
|
||||
@@ -449,13 +692,20 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
apiCallParameters(100, true)
|
||||
joinedAtRevision = 100
|
||||
expectTableUpdate = true
|
||||
}
|
||||
every { groupsV2API.getGroupHistoryPage(any(), 100, any(), true) } returns secondApiCallChangeSet.changeSet!!.toApiResponse()
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,23 +716,12 @@ class GroupsV2StateProcessorTest {
|
||||
fun missedMemberAddResolvesWithMultipleRevisionUpdate() {
|
||||
val secondOther = member(ACI.from(UUID.randomUUID()))
|
||||
|
||||
profileAndMessageHelper.masterKey = masterKey
|
||||
|
||||
val updateMessageContextArgs = mutableListOf<DecryptedGroupV2Context>()
|
||||
every { profileAndMessageHelper.insertUpdateMessages(any(), any(), any(), any()) } answers { callOriginal() }
|
||||
every { profileAndMessageHelper.storeMessage(capture(updateMessageContextArgs), any(), any()) } returns Unit
|
||||
|
||||
given {
|
||||
localState(
|
||||
revision = 8,
|
||||
title = "Whatever",
|
||||
members = selfAndOthers
|
||||
)
|
||||
serverState(
|
||||
revision = 10,
|
||||
title = "Changed",
|
||||
members = selfAndOthers + secondOther
|
||||
)
|
||||
changeSet {
|
||||
changeLog(9) {
|
||||
change {
|
||||
@@ -499,14 +738,25 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
apiCallParameters(requestedRevision = 8, includeFirst = true)
|
||||
apiCallParameters(requestedRevision = 8, includeFirst = false)
|
||||
joinedAtRevision = 0
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
val updateMessageContextArgs = mutableListOf<DecryptedGroupV2Context>()
|
||||
every { profileAndMessageHelper.storeMessage(capture(updateMessageContextArgs), any(), any()) } returns Unit
|
||||
|
||||
val result = processor.updateLocalGroupToRevision(
|
||||
targetRevision = GroupsV2StateProcessor.LATEST,
|
||||
timestamp = 0
|
||||
)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("members contains second other", result.latestServer!!.members, hasItem(secondOther))
|
||||
|
||||
assertThat("group update messages contains new member add", updateMessageContextArgs.map { it.change!!.newMembers }, hasItem(hasItem(secondOther)))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -517,12 +767,6 @@ class GroupsV2StateProcessorTest {
|
||||
fun missedMemberAddResolvesWithForcedUpdate() {
|
||||
val secondOther = member(ACI.from(UUID.randomUUID()))
|
||||
|
||||
profileAndMessageHelper.masterKey = masterKey
|
||||
|
||||
val updateMessageContextArgs = mutableListOf<DecryptedGroupV2Context>()
|
||||
every { profileAndMessageHelper.insertUpdateMessages(any(), any(), any(), any()) } answers { callOriginal() }
|
||||
every { profileAndMessageHelper.storeMessage(capture(updateMessageContextArgs), any(), any()) } returns Unit
|
||||
|
||||
given {
|
||||
localState(
|
||||
revision = 10,
|
||||
@@ -534,14 +778,21 @@ class GroupsV2StateProcessorTest {
|
||||
title = "Changed",
|
||||
members = selfAndOthers + secondOther
|
||||
)
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val updateMessageContextArgs = mutableListOf<DecryptedGroupV2Context>()
|
||||
every { profileAndMessageHelper.insertUpdateMessages(any(), any(), any(), any()) } answers { callOriginal() }
|
||||
every { profileAndMessageHelper.storeMessage(capture(updateMessageContextArgs), any(), any()) } returns Unit
|
||||
|
||||
val result = processor.forceSanityUpdateFromServer(0)
|
||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("members contains second other", result.latestServer!!.members, hasItem(secondOther))
|
||||
assertThat("title should be updated", result.latestServer!!.title, `is`("Changed"))
|
||||
assertThat("group update messages contains new member add", updateMessageContextArgs.map { it.change!!.newMembers }, hasItem(hasItem(secondOther)))
|
||||
assertThat("group update messages contains title change", updateMessageContextArgs.mapNotNull { it.change!!.newTitle }.any { it.value_ == "Changed" })
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -563,6 +814,66 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
|
||||
val result = processor.forceSanityUpdateFromServer(0)
|
||||
assertThat("local should be unchanged", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_CONSISTENT_OR_AHEAD))
|
||||
assertThat("local should be unchanged", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_CONSISTENT_OR_AHEAD))
|
||||
}
|
||||
|
||||
/** No local group state fails gracefully during force update */
|
||||
@Test
|
||||
fun missingLocalGroupStateForForcedUpdate() {
|
||||
given { }
|
||||
|
||||
val result = processor.forceSanityUpdateFromServer(0)
|
||||
assertThat("local should be unchanged", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_CONSISTENT_OR_AHEAD))
|
||||
}
|
||||
|
||||
@Test(expected = GroupNotAMemberException::class)
|
||||
fun serverNotInGroupFailsForForcedUpdate() {
|
||||
given {
|
||||
localState(
|
||||
revision = 5,
|
||||
members = selfAndOthers
|
||||
)
|
||||
}
|
||||
|
||||
every { groupsV2API.getGroup(any(), any()) } throws NotInGroupException()
|
||||
|
||||
processor.forceSanityUpdateFromServer(0)
|
||||
}
|
||||
|
||||
@Test(expected = IOException::class)
|
||||
fun serverVerificationFailedFailsForForcedUpdate() {
|
||||
given {
|
||||
localState(
|
||||
revision = 5,
|
||||
members = selfAndOthers
|
||||
)
|
||||
}
|
||||
|
||||
every { groupsV2API.getGroup(any(), any()) } throws VerificationFailedException()
|
||||
|
||||
processor.forceSanityUpdateFromServer(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun restoreFromPlaceholderForcedUpdate() {
|
||||
given {
|
||||
localState(
|
||||
revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION
|
||||
)
|
||||
serverState(
|
||||
revision = 10,
|
||||
members = selfAndOthers,
|
||||
title = "Asdf!"
|
||||
)
|
||||
expectTableUpdate = true
|
||||
}
|
||||
|
||||
val result = processor.forceSanityUpdateFromServer(0)
|
||||
|
||||
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
|
||||
assertThat("revision matches server", result.latestServer!!.revision, `is`(10))
|
||||
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!"))
|
||||
|
||||
verify { groupTable.update(masterKey, result.latestServer!!) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user