parent
2a2a6e6a0d
commit
33c918defd
19 changed files with 2001 additions and 1703 deletions
|
@ -1,104 +0,0 @@
|
|||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
public final class SelectedContactSetTest {
|
||||
|
||||
private final SelectedContactSet selectedContactSet = new SelectedContactSet();
|
||||
|
||||
@Test
|
||||
public void add_without_recipient_ids() {
|
||||
SelectedContact contact1 = SelectedContact.forPhone(null, "+1-555-000-0000");
|
||||
SelectedContact contact2 = SelectedContact.forUsername(null, "@alice");
|
||||
|
||||
assertTrue(selectedContactSet.add(contact1));
|
||||
assertTrue(selectedContactSet.add(contact2));
|
||||
|
||||
assertThat(selectedContactSet.getContacts(), is(asList(contact1, contact2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_with_recipient_ids() {
|
||||
SelectedContact contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000");
|
||||
SelectedContact contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice");
|
||||
|
||||
assertTrue(selectedContactSet.add(contact1));
|
||||
assertTrue(selectedContactSet.add(contact2));
|
||||
|
||||
assertThat(selectedContactSet.getContacts(), is(asList(contact1, contact2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_with_same_recipient_id() {
|
||||
SelectedContact contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000");
|
||||
SelectedContact contact2 = SelectedContact.forUsername(RecipientId.from(1), "@alice");
|
||||
|
||||
assertTrue(selectedContactSet.add(contact1));
|
||||
assertFalse(selectedContactSet.add(contact2));
|
||||
|
||||
assertThat(selectedContactSet.getContacts(), is(singletonList(contact1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_by_recipient_id() {
|
||||
SelectedContact contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000");
|
||||
SelectedContact contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice" );
|
||||
SelectedContact contact2Remove = SelectedContact.forUsername(RecipientId.from(2), "@alice2");
|
||||
|
||||
assertTrue(selectedContactSet.add(contact1));
|
||||
assertTrue(selectedContactSet.add(contact2));
|
||||
assertEquals(1, selectedContactSet.remove(contact2Remove));
|
||||
|
||||
assertThat(selectedContactSet.getContacts(), is(singletonList(contact1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_by_number() {
|
||||
SelectedContact contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000");
|
||||
SelectedContact contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice");
|
||||
SelectedContact contact1Remove = SelectedContact.forPhone(null, "+1-555-000-0000");
|
||||
|
||||
assertTrue(selectedContactSet.add(contact1));
|
||||
assertTrue(selectedContactSet.add(contact2));
|
||||
assertEquals(1, selectedContactSet.remove(contact1Remove));
|
||||
|
||||
assertThat(selectedContactSet.getContacts(), is(singletonList(contact2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_by_username() {
|
||||
SelectedContact contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000");
|
||||
SelectedContact contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice");
|
||||
SelectedContact contact2Remove = SelectedContact.forUsername(null, "@alice");
|
||||
|
||||
assertTrue(selectedContactSet.add(contact1));
|
||||
assertTrue(selectedContactSet.add(contact2));
|
||||
assertEquals(1, selectedContactSet.remove(contact2Remove));
|
||||
|
||||
assertThat(selectedContactSet.getContacts(), is(singletonList(contact1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_by_recipient_id_and_username() {
|
||||
SelectedContact contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000");
|
||||
SelectedContact contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice");
|
||||
SelectedContact contact3 = SelectedContact.forUsername(null, "@bob");
|
||||
SelectedContact contact2Remove = SelectedContact.forUsername(RecipientId.from(1), "@bob");
|
||||
|
||||
assertTrue(selectedContactSet.add(contact1));
|
||||
assertTrue(selectedContactSet.add(contact2));
|
||||
assertTrue(selectedContactSet.add(contact3));
|
||||
assertEquals(2, selectedContactSet.remove(contact2Remove));
|
||||
|
||||
assertThat(selectedContactSet.getContacts(), is(singletonList(contact2)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isTrue
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class SelectedContactSetTest {
|
||||
private val selectedContactSet = SelectedContactSet()
|
||||
|
||||
@Test
|
||||
fun add_without_recipient_ids() {
|
||||
val contact1 = SelectedContact.forPhone(null, "+1-555-000-0000")
|
||||
val contact2 = SelectedContact.forUsername(null, "@alice")
|
||||
|
||||
assertThat(selectedContactSet.add(contact1)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact2)).isTrue()
|
||||
|
||||
assertThat(selectedContactSet.contacts).containsExactly(contact1, contact2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun add_with_recipient_ids() {
|
||||
val contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000")
|
||||
val contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice")
|
||||
|
||||
assertThat(selectedContactSet.add(contact1)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact2)).isTrue()
|
||||
|
||||
assertThat(selectedContactSet.contacts).containsExactly(contact1, contact2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun add_with_same_recipient_id() {
|
||||
val contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000")
|
||||
val contact2 = SelectedContact.forUsername(RecipientId.from(1), "@alice")
|
||||
|
||||
assertThat(selectedContactSet.add(contact1)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact2)).isFalse()
|
||||
|
||||
assertThat(selectedContactSet.contacts).containsExactly(contact1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remove_by_recipient_id() {
|
||||
val contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000")
|
||||
val contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice")
|
||||
val contact2Remove = SelectedContact.forUsername(RecipientId.from(2), "@alice2")
|
||||
|
||||
assertThat(selectedContactSet.add(contact1)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact2)).isTrue()
|
||||
assertThat(selectedContactSet.remove(contact2Remove)).isEqualTo(1)
|
||||
|
||||
assertThat(selectedContactSet.contacts).containsExactly(contact1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remove_by_number() {
|
||||
val contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000")
|
||||
val contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice")
|
||||
val contact1Remove = SelectedContact.forPhone(null, "+1-555-000-0000")
|
||||
|
||||
assertThat(selectedContactSet.add(contact1)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact2)).isTrue()
|
||||
assertThat(selectedContactSet.remove(contact1Remove).toLong()).isEqualTo(1)
|
||||
|
||||
assertThat(selectedContactSet.contacts).containsExactly(contact2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remove_by_username() {
|
||||
val contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000")
|
||||
val contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice")
|
||||
val contact2Remove = SelectedContact.forUsername(null, "@alice")
|
||||
|
||||
assertThat(selectedContactSet.add(contact1)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact2)).isTrue()
|
||||
assertThat(selectedContactSet.remove(contact2Remove)).isEqualTo(1)
|
||||
|
||||
assertThat(selectedContactSet.contacts).containsExactly(contact1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remove_by_recipient_id_and_username() {
|
||||
val contact1 = SelectedContact.forPhone(RecipientId.from(1), "+1-555-000-0000")
|
||||
val contact2 = SelectedContact.forUsername(RecipientId.from(2), "@alice")
|
||||
val contact3 = SelectedContact.forUsername(null, "@bob")
|
||||
val contact2Remove = SelectedContact.forUsername(RecipientId.from(1), "@bob")
|
||||
|
||||
assertThat(selectedContactSet.add(contact1)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact2)).isTrue()
|
||||
assertThat(selectedContactSet.add(contact3)).isTrue()
|
||||
assertThat(selectedContactSet.remove(contact2Remove)).isEqualTo(2)
|
||||
|
||||
assertThat(selectedContactSet.contacts).containsExactly(contact2)
|
||||
}
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
package org.thoughtcrime.securesms.groups.v2;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.testutil.LogRecorder;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy;
|
||||
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown;
|
||||
import static org.thoughtcrime.securesms.testutil.LogRecorder.hasMessages;
|
||||
|
||||
public final class ProfileKeySetTest {
|
||||
|
||||
@Test
|
||||
public void empty_change() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).build());
|
||||
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_member_is_not_authoritative() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI newMember = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).addMember(newMember, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_member_by_self_is_authoritative() {
|
||||
ACI newMember = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(newMember).addMember(newMember, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_member_by_self_promote_is_authoritative() {
|
||||
ACI newMember = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(newMember).promote(newMember, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_member_by_promote_by_other_editor_is_not_authoritative() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI newMember = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).promote(newMember, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_member_by_promote_by_unknown_editor_is_not_authoritative() {
|
||||
ACI newMember = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeByUnknown().promote(newMember, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(newMember, profileKey)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void profile_key_update_by_self_is_authoritative() {
|
||||
ACI member = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(member).profileKeyUpdate(member, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(member, profileKey)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void profile_key_update_by_another_is_not_authoritative() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI member = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(member, profileKey)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multiple_updates_overwrite() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI member = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey1 = ProfileKeyUtil.createNew();
|
||||
ProfileKey profileKey2 = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey1).build());
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey2).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(member, profileKey2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authoritative_takes_priority_when_seen_first() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI member = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey1 = ProfileKeyUtil.createNew();
|
||||
ProfileKey profileKey2 = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(member).profileKeyUpdate(member, profileKey1).build());
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey2).build());
|
||||
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(member, profileKey1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authoritative_takes_priority_when_seen_second() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI member = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey1 = ProfileKeyUtil.createNew();
|
||||
ProfileKey profileKey2 = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, profileKey1).build());
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(member).profileKeyUpdate(member, profileKey2).build());
|
||||
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(member, profileKey2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bad_profile_key() {
|
||||
LogRecorder logRecorder = new LogRecorder();
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI member = ACI.from(UUID.randomUUID());
|
||||
byte[] badProfileKey = new byte[10];
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
Log.initialize(logRecorder);
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).profileKeyUpdate(member, badProfileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(logRecorder.getWarnings(), hasMessages("Bad profile key in group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_member_if_editor_is_authoritative() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).requestJoin(profileKey).build());
|
||||
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(editor, profileKey)));
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_member_if_not_editor_is_not_authoritative() {
|
||||
ACI editor = ACI.from(UUID.randomUUID());
|
||||
ACI requesting = ACI.from(UUID.randomUUID());
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).requestJoin(requesting, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(requesting, profileKey)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package org.thoughtcrime.securesms.groups.v2
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsOnly
|
||||
import assertk.assertions.isEmpty
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.logging.Log.initialize
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.testutil.LogRecorder
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
class ProfileKeySetTest {
|
||||
@Test
|
||||
fun empty_change() {
|
||||
val editor = randomACI()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).build())
|
||||
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun new_member_is_not_authoritative() {
|
||||
val editor = randomACI()
|
||||
val newMember = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).addMember(newMember, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
assertThat(profileKeySet.profileKeys).containsOnly(newMember to profileKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun new_member_by_self_is_authoritative() {
|
||||
val newMember = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(newMember).addMember(newMember, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).containsOnly(newMember to profileKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun new_member_by_self_promote_is_authoritative() {
|
||||
val newMember = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(newMember).promote(newMember, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).containsOnly(newMember to profileKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun new_member_by_promote_by_other_editor_is_not_authoritative() {
|
||||
val editor = randomACI()
|
||||
val newMember = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).promote(newMember, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
assertThat(profileKeySet.profileKeys).containsOnly(newMember to profileKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun new_member_by_promote_by_unknown_editor_is_not_authoritative() {
|
||||
val newMember = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeByUnknown().promote(newMember, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
assertThat(profileKeySet.profileKeys).containsOnly(newMember to profileKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun profile_key_update_by_self_is_authoritative() {
|
||||
val member = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(member).profileKeyUpdate(member, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).containsOnly(member to profileKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun profile_key_update_by_another_is_not_authoritative() {
|
||||
val editor = randomACI()
|
||||
val member = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).profileKeyUpdate(member, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
assertThat(profileKeySet.profileKeys).containsOnly(member to profileKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiple_updates_overwrite() {
|
||||
val editor = randomACI()
|
||||
val member = randomACI()
|
||||
val profileKey1 = ProfileKeyUtil.createNew()
|
||||
val profileKey2 = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).profileKeyUpdate(member, profileKey1).build())
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).profileKeyUpdate(member, profileKey2).build())
|
||||
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
assertThat(profileKeySet.profileKeys).containsOnly(member to profileKey2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authoritative_takes_priority_when_seen_first() {
|
||||
val editor = randomACI()
|
||||
val member = randomACI()
|
||||
val profileKey1 = ProfileKeyUtil.createNew()
|
||||
val profileKey2 = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(member).profileKeyUpdate(member, profileKey1).build())
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).profileKeyUpdate(member, profileKey2).build())
|
||||
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).containsOnly(member to profileKey1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authoritative_takes_priority_when_seen_second() {
|
||||
val editor = randomACI()
|
||||
val member = randomACI()
|
||||
val profileKey1 = ProfileKeyUtil.createNew()
|
||||
val profileKey2 = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).profileKeyUpdate(member, profileKey1).build())
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(member).profileKeyUpdate(member, profileKey2).build())
|
||||
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).containsOnly(member to profileKey2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bad_profile_key() {
|
||||
val logRecorder = LogRecorder()
|
||||
val editor = randomACI()
|
||||
val member = randomACI()
|
||||
val badProfileKey = ByteArray(10)
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
initialize(logRecorder)
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).profileKeyUpdate(member, badProfileKey).build())
|
||||
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
assertThat(logRecorder.warnings)
|
||||
.transform { lines ->
|
||||
lines.map { line ->
|
||||
line.message
|
||||
}
|
||||
}
|
||||
.containsOnly("Bad profile key in group")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun new_requesting_member_if_editor_is_authoritative() {
|
||||
val editor = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).requestJoin(profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).containsOnly(editor to profileKey)
|
||||
assertThat(profileKeySet.profileKeys).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun new_requesting_member_if_not_editor_is_not_authoritative() {
|
||||
val editor = randomACI()
|
||||
val requesting = randomACI()
|
||||
val profileKey = ProfileKeyUtil.createNew()
|
||||
val profileKeySet = ProfileKeySet()
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(ChangeBuilder.changeBy(editor).requestJoin(requesting, profileKey).build())
|
||||
|
||||
assertThat(profileKeySet.authoritativeProfileKeys).isEmpty()
|
||||
assertThat(profileKeySet.profileKeys).containsOnly(requesting to profileKey)
|
||||
}
|
||||
|
||||
private fun randomACI(): ServiceId.ACI = ServiceId.ACI.from(UUID.randomUUID())
|
||||
}
|
|
@ -1,515 +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.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(), null), 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(), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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), null), 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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,830 @@
|
|||
package org.thoughtcrime.securesms.groups.v2.processing
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsOnly
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isSameInstanceAs
|
||||
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
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.UUID
|
||||
|
||||
class GroupStatePatcherTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
Log.initialize(LogRecorder())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknown_group_with_no_states_to_update() {
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = null,
|
||||
serverHistory = emptyList(),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
10
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_with_no_states_to_update() {
|
||||
val currentState = state(0)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = emptyList(),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
10
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(currentState).isSameInstanceAs(advanceGroupStateResult.updatedGroupState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknown_group_single_state_to_update() {
|
||||
val log0 = serverLogEntry(0)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = null,
|
||||
serverHistory = listOf(log0),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
10
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log0))
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log0.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_single_state_to_update() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
1
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1))
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log1.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_two_states_to_update() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
val log2 = serverLogEntry(2)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log2),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
2
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log2))
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_two_states_to_update_already_on_one() {
|
||||
val currentState = state(1)
|
||||
val log1 = serverLogEntry(1)
|
||||
val log2 = serverLogEntry(2)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log2),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
2
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log2))
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_three_states_to_update_stop_at_2() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
val log2 = serverLogEntry(2)
|
||||
val log3 = serverLogEntry(3)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log2, log3),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
2
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log2))
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log2.group,
|
||||
expectedRemainingLogs = listOf(log3),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_three_states_to_update_update_latest() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
val log2 = serverLogEntry(2)
|
||||
val log3 = serverLogEntry(3)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log2, log3),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
asLocal(log1),
|
||||
asLocal(log2),
|
||||
asLocal(log3)
|
||||
)
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log3.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log3.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apply_maximum_group_revisions() {
|
||||
val currentState = state(Int.MAX_VALUE - 2)
|
||||
val log1 = serverLogEntry(Int.MAX_VALUE - 1)
|
||||
val log2 = serverLogEntry(Int.MAX_VALUE)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log2),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log2))
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log2.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknown_group_single_state_to_update_with_missing_change() {
|
||||
val log0 = serverLogEntryWholeStateOnly(0)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = null,
|
||||
serverHistory = listOf(log0),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
10
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log0))
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log0.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_single_state_to_update_with_missing_change() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntryWholeStateOnly(1)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
1
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(localLogEntryNoEditor(1))
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log1.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_three_states_to_update_update_latest_handle_missing_change() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
val log2 = serverLogEntryWholeStateOnly(2)
|
||||
val log3 = serverLogEntry(3)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log2, log3),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
asLocal(log1),
|
||||
localLogEntryNoEditor(2),
|
||||
asLocal(log3)
|
||||
)
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log3.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log3.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_three_states_to_update_update_latest_handle_gap_with_no_changes() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
val log3 = serverLogEntry(3)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log3),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log3))
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log3.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log3.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun known_group_three_states_to_update_update_latest_handle_gap_with_changes() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
val state3a = DecryptedGroup.Builder()
|
||||
.revision(3)
|
||||
.title("Group Revision " + 3)
|
||||
.build()
|
||||
val state3 = DecryptedGroup.Builder()
|
||||
.revision(3)
|
||||
.title("Group Revision " + 3)
|
||||
.avatar("Lost Avatar Update")
|
||||
.build()
|
||||
val log3 = DecryptedGroupChangeLog(state3, change(3))
|
||||
val state4 = DecryptedGroup.Builder()
|
||||
.revision(4)
|
||||
.title("Group Revision " + 4)
|
||||
.avatar("Lost Avatar Update")
|
||||
.build()
|
||||
val log4 = DecryptedGroupChangeLog(state4, change(4))
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log3, log4),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
asLocal(log1),
|
||||
AppliedGroupChangeLog(state3a, log3.change),
|
||||
AppliedGroupChangeLog(
|
||||
state3,
|
||||
DecryptedGroupChange.Builder()
|
||||
.revision(3)
|
||||
.newAvatar(DecryptedString.Builder().value_("Lost Avatar Update").build())
|
||||
.build()
|
||||
),
|
||||
asLocal(log4)
|
||||
)
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log4.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log4.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updates_with_all_changes_missing() {
|
||||
val currentState = state(5)
|
||||
val log6 = serverLogEntryWholeStateOnly(6)
|
||||
val log7 = serverLogEntryWholeStateOnly(7)
|
||||
val log8 = serverLogEntryWholeStateOnly(8)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log6, log7, log8),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
localLogEntryNoEditor(6),
|
||||
localLogEntryNoEditor(7),
|
||||
localLogEntryNoEditor(8)
|
||||
)
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log8.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updates_with_all_group_states_missing() {
|
||||
val currentState = state(6)
|
||||
val log7 = logEntryMissingState(7)
|
||||
val log8 = logEntryMissingState(8)
|
||||
val log9 = logEntryMissingState(9)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log7, log8, log9),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
asLocal(serverLogEntry(7)),
|
||||
asLocal(serverLogEntry(8)),
|
||||
asLocal(serverLogEntry(9))
|
||||
)
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = state(9),
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(state(9))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updates_with_a_server_mismatch_inserts_additional_update() {
|
||||
val currentState = state(6)
|
||||
val log7 = serverLogEntry(7)
|
||||
val newMember = DecryptedMember.Builder()
|
||||
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build()
|
||||
val state7b = DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.build()
|
||||
val state8 = DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.members(listOf(newMember))
|
||||
.build()
|
||||
val log8 = DecryptedGroupChangeLog(
|
||||
state8,
|
||||
change(8)
|
||||
)
|
||||
val log9 = DecryptedGroupChangeLog(
|
||||
DecryptedGroup.Builder()
|
||||
.revision(9)
|
||||
.members(listOf(newMember))
|
||||
.title("Group Revision " + 9)
|
||||
.build(),
|
||||
change(9)
|
||||
)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log7, log8, log9),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
asLocal(log7),
|
||||
AppliedGroupChangeLog(state7b, log8.change),
|
||||
AppliedGroupChangeLog(
|
||||
state8,
|
||||
DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.newMembers(listOf(newMember))
|
||||
.build()
|
||||
),
|
||||
asLocal(log9)
|
||||
)
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log9.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log9.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun local_up_to_date_no_repair_necessary() {
|
||||
val currentState = state(6)
|
||||
val log6 = serverLogEntryWholeStateOnly(6)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log6),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = state(6),
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(state(6))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun no_repair_change_is_posted_if_the_local_state_is_a_placeholder() {
|
||||
val currentState = DecryptedGroup.Builder()
|
||||
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
|
||||
.title("Incorrect group title, Revision " + 6)
|
||||
.build()
|
||||
val log6 = serverLogEntry(6)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log6),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log6))
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log6.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clears_changes_duplicated_in_the_placeholder() {
|
||||
val newMemberAci = ServiceId.ACI.from(UUID.randomUUID())
|
||||
val newMember = DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build()
|
||||
val existingMember = DecryptedMember.Builder()
|
||||
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build()
|
||||
val currentState = DecryptedGroup.Builder()
|
||||
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
|
||||
.title("Group Revision " + 8)
|
||||
.members(listOf(newMember))
|
||||
.build()
|
||||
val log8 = DecryptedGroupChangeLog(
|
||||
DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(listOf(existingMember).plus(newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.build(),
|
||||
DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(newMemberAci.toByteString())
|
||||
.newMembers(listOf(newMember))
|
||||
.build()
|
||||
)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log8),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(log8.group).isNotNull()
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log8.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clears_changes_duplicated_in_a_non_placeholder() {
|
||||
val editorAci = ServiceId.ACI.from(UUID.randomUUID())
|
||||
val newMemberAci = ServiceId.ACI.from(UUID.randomUUID())
|
||||
val newMember = DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build()
|
||||
val existingMember = DecryptedMember.Builder()
|
||||
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build()
|
||||
val currentState = DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.title("Group Revision " + 8)
|
||||
.members(listOf(existingMember))
|
||||
.build()
|
||||
val log8 = DecryptedGroupChangeLog(
|
||||
DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(listOf(existingMember).plus(newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.build(),
|
||||
DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(editorAci.toByteString())
|
||||
.newMembers(listOf(existingMember).plus(newMember))
|
||||
.build()
|
||||
)
|
||||
|
||||
val expectedChange = DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(editorAci.toByteString())
|
||||
.newMembers(listOf(newMember))
|
||||
.build()
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log8),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(log8.group).isNotNull()
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
AppliedGroupChangeLog(
|
||||
log8.group!!,
|
||||
expectedChange
|
||||
)
|
||||
)
|
||||
assertNewState(
|
||||
expectedUpdatedGroupState = log8.group,
|
||||
expectedRemainingLogs = emptyList(),
|
||||
updatedGroupState = advanceGroupStateResult.updatedGroupState,
|
||||
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
|
||||
)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notices_changes_in_avatar_and_title_but_not_members_in_placeholder() {
|
||||
val newMemberAci = ServiceId.ACI.from(UUID.randomUUID())
|
||||
val newMember = DecryptedMember.Builder()
|
||||
.aciBytes(newMemberAci.toByteString())
|
||||
.build()
|
||||
val existingMember = DecryptedMember.Builder()
|
||||
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
|
||||
.build()
|
||||
val currentState = DecryptedGroup.Builder()
|
||||
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
|
||||
.title("Incorrect group title")
|
||||
.avatar("Incorrect group avatar")
|
||||
.members(listOf(newMember))
|
||||
.build()
|
||||
val log8 = DecryptedGroupChangeLog(
|
||||
DecryptedGroup.Builder()
|
||||
.revision(8)
|
||||
.members(listOf(existingMember).plus(newMember))
|
||||
.title("Group Revision " + 8)
|
||||
.avatar("Group Avatar " + 8)
|
||||
.build(),
|
||||
DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.editorServiceIdBytes(newMemberAci.toByteString())
|
||||
.newMembers(listOf(newMember))
|
||||
.build()
|
||||
)
|
||||
|
||||
val expectedChange = DecryptedGroupChange.Builder()
|
||||
.revision(8)
|
||||
.newTitle(DecryptedString.Builder().value_("Group Revision " + 8).build())
|
||||
.newAvatar(DecryptedString.Builder().value_("Group Avatar " + 8).build())
|
||||
.build()
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log8),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
GroupStatePatcher.LATEST
|
||||
)
|
||||
|
||||
assertThat(log8.group).isNotNull()
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
AppliedGroupChangeLog(
|
||||
log8.group!!,
|
||||
expectedChange
|
||||
)
|
||||
)
|
||||
assertNewState(log8.group, emptyList(), advanceGroupStateResult.updatedGroupState, advanceGroupStateResult.remainingRemoteGroupChanges)
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun no_actual_change() {
|
||||
val currentState = state(0)
|
||||
val log1 = serverLogEntry(1)
|
||||
val log2 = DecryptedGroupChangeLog(
|
||||
log1.group!!.newBuilder()
|
||||
.revision(2)
|
||||
.build(),
|
||||
DecryptedGroupChange.Builder()
|
||||
.revision(2)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.newTitle(DecryptedString.Builder().value_(log1.group!!.title).build())
|
||||
.build()
|
||||
)
|
||||
|
||||
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
|
||||
/* inputState = */
|
||||
GroupStateDiff(
|
||||
previousGroupState = currentState,
|
||||
serverHistory = listOf(log1, log2),
|
||||
groupSendEndorsementsResponse = null
|
||||
),
|
||||
/* maximumRevisionToApply = */
|
||||
2
|
||||
)
|
||||
|
||||
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
|
||||
asLocal(log1),
|
||||
AppliedGroupChangeLog(
|
||||
log2.group!!,
|
||||
DecryptedGroupChange.Builder()
|
||||
.revision(2)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
|
||||
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KNOWN_EDITOR = UUID.randomUUID()
|
||||
|
||||
private fun assertNewState(
|
||||
expectedUpdatedGroupState: DecryptedGroup?,
|
||||
expectedRemainingLogs: List<DecryptedGroupChangeLog>,
|
||||
updatedGroupState: DecryptedGroup?,
|
||||
remainingLogs: List<DecryptedGroupChangeLog>
|
||||
) {
|
||||
assertThat(updatedGroupState).isEqualTo(expectedUpdatedGroupState)
|
||||
assertThat(remainingLogs).isEqualTo(expectedRemainingLogs)
|
||||
}
|
||||
|
||||
private fun serverLogEntry(revision: Int): DecryptedGroupChangeLog {
|
||||
return DecryptedGroupChangeLog(state(revision), change(revision))
|
||||
}
|
||||
|
||||
private fun localLogEntryNoEditor(revision: Int): AppliedGroupChangeLog {
|
||||
return AppliedGroupChangeLog(state(revision), changeNoEditor(revision))
|
||||
}
|
||||
|
||||
private fun serverLogEntryWholeStateOnly(revision: Int): DecryptedGroupChangeLog {
|
||||
return DecryptedGroupChangeLog(state(revision), null)
|
||||
}
|
||||
|
||||
private fun logEntryMissingState(revision: Int): DecryptedGroupChangeLog {
|
||||
return DecryptedGroupChangeLog(null, change(revision))
|
||||
}
|
||||
|
||||
private fun state(revision: Int): DecryptedGroup {
|
||||
return DecryptedGroup.Builder()
|
||||
.revision(revision)
|
||||
.title("Group Revision $revision")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun change(revision: Int): DecryptedGroupChange {
|
||||
return DecryptedGroupChange.Builder()
|
||||
.revision(revision)
|
||||
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||
.newTitle(DecryptedString.Builder().value_("Group Revision $revision").build())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun changeNoEditor(revision: Int): DecryptedGroupChange {
|
||||
return DecryptedGroupChange.Builder()
|
||||
.revision(revision)
|
||||
.newTitle(DecryptedString.Builder().value_("Group Revision $revision").build())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun asLocal(logEntry: DecryptedGroupChangeLog): AppliedGroupChangeLog {
|
||||
assertThat(logEntry.group).isNotNull()
|
||||
return AppliedGroupChangeLog(logEntry.group!!, logEntry.change)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
public final class StateChainTest {
|
||||
|
||||
private static final int BAD_DELTA = 256;
|
||||
|
||||
private final StateChain<Character, Integer> stateChain = new StateChain<>(
|
||||
(c, d) -> {
|
||||
if (d == BAD_DELTA) return null;
|
||||
return (char) (c + d);
|
||||
},
|
||||
(a, b) -> a - b,
|
||||
(a, b)->a==b);
|
||||
|
||||
@Test
|
||||
public void push_one_state_pair() {
|
||||
stateChain.push('A', 0);
|
||||
|
||||
assertThat(stateChain.getList(), is(singletonList(pair('A', 0))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_two_state_pairs() {
|
||||
stateChain.push('A', 0);
|
||||
stateChain.push('B', 1);
|
||||
|
||||
assertThat(stateChain.getList(), is(asList(pair('A', 0),
|
||||
pair('B', 1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_two_state_pairs_null_first_delta() {
|
||||
stateChain.push('A', null);
|
||||
stateChain.push('B', 1);
|
||||
|
||||
assertThat(stateChain.getList(), is(asList(pair('A', null),
|
||||
pair('B', 1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_two_state_pairs_with_missing_delta() {
|
||||
stateChain.push('A', 0);
|
||||
stateChain.push('B', null);
|
||||
|
||||
assertThat(stateChain.getList(), is(asList(pair('A', 0),
|
||||
pair('B', 1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_two_state_pairs_with_missing_state() {
|
||||
stateChain.push('A', 0);
|
||||
stateChain.push(null, 1);
|
||||
|
||||
assertThat(stateChain.getList(), is(asList(pair('A', 0),
|
||||
pair('B', 1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_one_state_pairs_with_missing_state_and_delta() {
|
||||
stateChain.push(null, null);
|
||||
|
||||
assertThat(stateChain.getList(), is(emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_two_state_pairs_with_missing_state_and_delta() {
|
||||
stateChain.push('A', 0);
|
||||
stateChain.push(null, null);
|
||||
|
||||
assertThat(stateChain.getList(), is(singletonList(pair('A', 0))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_two_state_pairs_that_do_not_match() {
|
||||
stateChain.push('D', 0);
|
||||
stateChain.push('E', 2);
|
||||
|
||||
assertThat(stateChain.getList(), is(asList(pair('D', 0),
|
||||
pair('F', 2),
|
||||
pair('E', -1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_one_state_pair_null_delta() {
|
||||
stateChain.push('A', null);
|
||||
|
||||
assertThat(stateChain.getList(), is(singletonList(pair('A', null))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_two_state_pairs_with_no_diff() {
|
||||
stateChain.push('Z', null);
|
||||
stateChain.push('Z', 0);
|
||||
|
||||
assertThat(stateChain.getList(), is(singletonList(pair('Z', null))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void push_one_state_pair_null_state() {
|
||||
stateChain.push(null, 1);
|
||||
|
||||
assertThat(stateChain.getList(), is(emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bad_delta_results_in_reconstruction() {
|
||||
stateChain.push('C', 0);
|
||||
stateChain.push('F', BAD_DELTA);
|
||||
|
||||
assertThat(stateChain.getList(), is(asList(pair('C', 0),
|
||||
pair('F', 3))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bad_delta_and_no_state_results_in_change_ignore() {
|
||||
stateChain.push('C', 0);
|
||||
stateChain.push(null, BAD_DELTA);
|
||||
|
||||
assertThat(stateChain.getList(), is(singletonList(pair('C', 0))));
|
||||
}
|
||||
|
||||
private static StateChain.Pair<Character, Integer> pair(char c, Integer i) {
|
||||
return new StateChain.Pair<>(c, i);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package org.thoughtcrime.securesms.groups.v2.processing
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEmpty
|
||||
import org.junit.Test
|
||||
|
||||
class StateChainTest {
|
||||
private val stateChain = StateChain<Char, Int>(
|
||||
{ state, delta ->
|
||||
if (delta == BAD_DELTA) return@StateChain null
|
||||
(state.code + delta).toChar()
|
||||
},
|
||||
{ state1, state2 -> state1.code - state2.code },
|
||||
{ state1, state2 -> state1 == state2 }
|
||||
)
|
||||
|
||||
@Test
|
||||
fun push_one_state_pair() {
|
||||
stateChain.push('A', 0)
|
||||
|
||||
assertThat(stateChain.list).containsExactly(stateChainPair('A', 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_two_state_pairs() {
|
||||
stateChain.push('A', 0)
|
||||
stateChain.push('B', 1)
|
||||
|
||||
assertThat(stateChain.list)
|
||||
.containsExactly(stateChainPair('A', 0), stateChainPair('B', 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_two_state_pairs_null_first_delta() {
|
||||
stateChain.push('A', null)
|
||||
stateChain.push('B', 1)
|
||||
|
||||
assertThat(stateChain.list)
|
||||
.containsExactly(stateChainPair('A', null), stateChainPair('B', 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_two_state_pairs_with_missing_delta() {
|
||||
stateChain.push('A', 0)
|
||||
stateChain.push('B', null)
|
||||
|
||||
assertThat(stateChain.list)
|
||||
.containsExactly(stateChainPair('A', 0), stateChainPair('B', 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_two_state_pairs_with_missing_state() {
|
||||
stateChain.push('A', 0)
|
||||
stateChain.push(null, 1)
|
||||
|
||||
assertThat(stateChain.list)
|
||||
.containsExactly(stateChainPair('A', 0), stateChainPair('B', 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_one_state_pairs_with_missing_state_and_delta() {
|
||||
stateChain.push(null, null)
|
||||
|
||||
assertThat(stateChain.list).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_two_state_pairs_with_missing_state_and_delta() {
|
||||
stateChain.push('A', 0)
|
||||
stateChain.push(null, null)
|
||||
|
||||
assertThat(stateChain.list).containsExactly(stateChainPair('A', 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_two_state_pairs_that_do_not_match() {
|
||||
stateChain.push('D', 0)
|
||||
stateChain.push('E', 2)
|
||||
|
||||
assertThat(stateChain.list).containsExactly(
|
||||
stateChainPair('D', 0),
|
||||
stateChainPair('F', 2),
|
||||
stateChainPair('E', -1)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_one_state_pair_null_delta() {
|
||||
stateChain.push('A', null)
|
||||
|
||||
assertThat(stateChain.list).containsExactly(stateChainPair('A', null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_two_state_pairs_with_no_diff() {
|
||||
stateChain.push('Z', null)
|
||||
stateChain.push('Z', 0)
|
||||
|
||||
assertThat(stateChain.list).containsExactly(stateChainPair('Z', null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_one_state_pair_null_state() {
|
||||
stateChain.push(null, 1)
|
||||
|
||||
assertThat(stateChain.list).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bad_delta_results_in_reconstruction() {
|
||||
stateChain.push('C', 0)
|
||||
stateChain.push('F', BAD_DELTA)
|
||||
|
||||
assertThat(stateChain.list).containsExactly(stateChainPair('C', 0), stateChainPair('F', 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bad_delta_and_no_state_results_in_change_ignore() {
|
||||
stateChain.push('C', 0)
|
||||
stateChain.push(null, BAD_DELTA)
|
||||
|
||||
assertThat(stateChain.list).containsExactly(stateChainPair('C', 0))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BAD_DELTA = 256
|
||||
|
||||
private fun <A, B> stateChainPair(first: A & Any, second: B): StateChain.Pair<A & Any, B> {
|
||||
return StateChain.Pair<A & Any, B>(first, second)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package org.thoughtcrime.securesms.l10n;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class SupportArticleTest {
|
||||
|
||||
private static final File MAIN_STRINGS = new File("src/main/res/values/strings.xml");
|
||||
private static final Pattern SUPPORT_ARTICLE = Pattern.compile(".*:\\/\\/support.signal.org\\/.*articles\\/.*");
|
||||
private static final Pattern CORRECT_SUPPORT_ARTICLE = Pattern.compile("https:\\/\\/support.signal.org\\/hc\\/articles\\/\\d+(#[a-z_]+)?");
|
||||
|
||||
/**
|
||||
* Tests that support articles found in strings.xml:
|
||||
* <p>
|
||||
* - Do not have a locale mentioned in the url.
|
||||
* - Only have an article number, i.e. no trailing text.
|
||||
* - Are https.
|
||||
* - Are marked as translatable="false".
|
||||
*/
|
||||
@Test
|
||||
public void ensure_format_and_translatable_state_of_all_support_article_urls() throws Exception {
|
||||
assertTrue(MAIN_STRINGS.exists());
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
List<String> errors = new LinkedList<>();
|
||||
int seen = 0;
|
||||
|
||||
try (InputStream fileStream = new FileInputStream(MAIN_STRINGS)) {
|
||||
Document doc = builder.parse(fileStream);
|
||||
NodeList strings = doc.getElementsByTagName("string");
|
||||
|
||||
for (int i = 0; i < strings.getLength(); i++) {
|
||||
Node stringNode = strings.item(i);
|
||||
String string = stringNode.getTextContent();
|
||||
String stringName = stringName(stringNode);
|
||||
|
||||
if (SUPPORT_ARTICLE.matcher(string).matches()) {
|
||||
seen++;
|
||||
|
||||
if (!CORRECT_SUPPORT_ARTICLE.matcher(string).matches()) {
|
||||
errors.add(String.format("Article url format is not correct [%s] url: %s", stringName, string));
|
||||
}
|
||||
if (isTranslatable(stringNode)) {
|
||||
errors.add(String.format("Article string is translatable [%s], add translatable=\"false\"", stringName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(seen, greaterThan(0));
|
||||
assertThat(errors, is(Collections.emptyList()));
|
||||
}
|
||||
|
||||
private static boolean isTranslatable(Node item) {
|
||||
if (item.hasAttributes()) {
|
||||
Node translatableAttribute = item.getAttributes().getNamedItem("translatable");
|
||||
return translatableAttribute == null || !"false".equals(translatableAttribute.getTextContent());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String stringName(Node item) {
|
||||
return item.getAttributes().getNamedItem("name").getTextContent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package org.thoughtcrime.securesms.l10n
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isGreaterThan
|
||||
import org.junit.Test
|
||||
import org.w3c.dom.Node
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
class SupportArticleTest {
|
||||
/**
|
||||
* Tests that support articles found in strings.xml:
|
||||
* <p>
|
||||
* - Do not have a locale mentioned in the URL.
|
||||
* - Only have an article number, i.e. no trailing text.
|
||||
* - Are https.
|
||||
* - Are marked as translatable="false".
|
||||
*/
|
||||
@Test
|
||||
fun ensure_format_and_translatable_state_of_all_support_article_urls() {
|
||||
val errors = mutableListOf<String>()
|
||||
var seen = 0
|
||||
|
||||
val strings = DocumentBuilderFactory.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.parse(MAIN_STRINGS_PATH.inputStream())
|
||||
.getElementsByTagName("string")
|
||||
|
||||
for (i in 0 until strings.length) {
|
||||
val stringNode = strings.item(i)
|
||||
val string = stringNode.textContent
|
||||
val stringName = stringName(stringNode)
|
||||
|
||||
if (SUPPORT_ARTICLE.matches(string)) {
|
||||
seen++
|
||||
|
||||
if (!CORRECT_SUPPORT_ARTICLE.matches(string)) {
|
||||
errors.add("Article URL format is not correct [$stringName] URL: $string")
|
||||
}
|
||||
if (isTranslatable(stringNode)) {
|
||||
errors.add("Article string is translatable [$stringName], add translatable=\"false\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(seen).isGreaterThan(0)
|
||||
assertThat(errors).isEmpty()
|
||||
}
|
||||
|
||||
private fun isTranslatable(item: Node): Boolean {
|
||||
val translatableAttribute = item.attributes.getNamedItem("translatable")
|
||||
return translatableAttribute == null || translatableAttribute.textContent != "false"
|
||||
}
|
||||
|
||||
private fun stringName(item: Node): String {
|
||||
return item.attributes.getNamedItem("name").textContent
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MAIN_STRINGS_PATH = Path("src/main/res/values/strings.xml")
|
||||
private val SUPPORT_ARTICLE = Regex(".*://support.signal.org/.*articles/.*")
|
||||
private val CORRECT_SUPPORT_ARTICLE = Regex("https://support.signal.org/hc/articles/\\d+(#[a-z_]+)?")
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
public final class RecipientIdSerializationTest {
|
||||
|
||||
@Test
|
||||
public void toSerializedList_empty() {
|
||||
assertEquals("", RecipientId.toSerializedList(emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toSerializedList_one_item() {
|
||||
assertEquals("123", RecipientId.toSerializedList(singletonList(RecipientId.from(123))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toSerializedList_two_items() {
|
||||
assertEquals("123,987", RecipientId.toSerializedList(asList(RecipientId.from(123), RecipientId.from("987"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromSerializedList_empty() {
|
||||
assertThat(RecipientId.fromSerializedList(""), is(emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromSerializedList_one_item() {
|
||||
assertThat(RecipientId.fromSerializedList("123"), is(singletonList(RecipientId.from(123))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromSerializedList_two_items() {
|
||||
assertThat(RecipientId.fromSerializedList("123,456"), is(asList(RecipientId.from(123), RecipientId.from(456))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromSerializedList_recipient_serialize() {
|
||||
List<RecipientId> recipientIds = RecipientId.fromSerializedList(RecipientId.from(123).serialize());
|
||||
assertThat(recipientIds, hasSize(1));
|
||||
assertThat(recipientIds, contains(RecipientId.from(123)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_empty_list_does_not_contain_item() {
|
||||
assertFalse(RecipientId.serializedListContains("", RecipientId.from(456)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_single_list_does_not_contain_item() {
|
||||
assertFalse(RecipientId.serializedListContains("123", RecipientId.from(456)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_single_list_does_contain_item() {
|
||||
assertTrue(RecipientId.serializedListContains("456", RecipientId.from(456)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_double_list_does_contain_item_in_first_position() {
|
||||
assertTrue(RecipientId.serializedListContains("456,123", RecipientId.from(456)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_double_list_does_contain_item_in_second_position() {
|
||||
assertTrue(RecipientId.serializedListContains("123,456", RecipientId.from(456)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_single_list_does_not_contain_item_due_to_extra_digit_at_start() {
|
||||
assertFalse(RecipientId.serializedListContains("1456", RecipientId.from(456)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_single_list_does_not_contain_item_due_to_extra_digit_at_end() {
|
||||
assertFalse(RecipientId.serializedListContains("4561", RecipientId.from(456)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_find_all_items_in_triple_list() {
|
||||
assertTrue(RecipientId.serializedListContains("11,12,13", RecipientId.from(11)));
|
||||
assertTrue(RecipientId.serializedListContains("11,12,13", RecipientId.from(12)));
|
||||
assertTrue(RecipientId.serializedListContains("11,12,13", RecipientId.from(13)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializedListContains_cant_find_similar_items_in_triple_list() {
|
||||
assertFalse(RecipientId.serializedListContains("11,12,13", RecipientId.from(1)));
|
||||
assertFalse(RecipientId.serializedListContains("11,12,13", RecipientId.from(2)));
|
||||
assertFalse(RecipientId.serializedListContains("11,12,13", RecipientId.from(3)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package org.thoughtcrime.securesms.recipients
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.single
|
||||
import org.junit.Test
|
||||
|
||||
class RecipientIdSerializationTest {
|
||||
@Test
|
||||
fun toSerializedList_empty() {
|
||||
assertThat(RecipientId.toSerializedList(emptyList())).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toSerializedList_one_item() {
|
||||
assertThat(RecipientId.toSerializedList(listOf(RecipientId.from(123)))).isEqualTo("123")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toSerializedList_two_items() {
|
||||
val ids = listOf(RecipientId.from(123), RecipientId.from("987"))
|
||||
val serializedList = RecipientId.toSerializedList(ids)
|
||||
assertThat(serializedList).isEqualTo("123,987")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fromSerializedList_empty() {
|
||||
assertThat(RecipientId.fromSerializedList("")).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fromSerializedList_one_item() {
|
||||
assertThat(RecipientId.fromSerializedList("123"))
|
||||
.single()
|
||||
.isEqualTo(RecipientId.from(123))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fromSerializedList_two_items() {
|
||||
assertThat(RecipientId.fromSerializedList("123,456"))
|
||||
.containsExactly(RecipientId.from(123), RecipientId.from(456))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fromSerializedList_recipient_serialize() {
|
||||
val recipientIds = RecipientId.fromSerializedList(RecipientId.from(123).serialize())
|
||||
assertThat(recipientIds)
|
||||
.single()
|
||||
.isEqualTo(RecipientId.from(123))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_empty_list_does_not_contain_item() {
|
||||
assertThat(RecipientId.serializedListContains("", RecipientId.from(456))).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_single_list_does_not_contain_item() {
|
||||
assertThat(RecipientId.serializedListContains("123", RecipientId.from(456))).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_single_list_does_contain_item() {
|
||||
assertThat(RecipientId.serializedListContains("456", RecipientId.from(456))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_double_list_does_contain_item_in_first_position() {
|
||||
assertThat(RecipientId.serializedListContains("456,123", RecipientId.from(456))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_double_list_does_contain_item_in_second_position() {
|
||||
assertThat(RecipientId.serializedListContains("123,456", RecipientId.from(456))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_single_list_does_not_contain_item_due_to_extra_digit_at_start() {
|
||||
assertThat(RecipientId.serializedListContains("1456", RecipientId.from(456))).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_single_list_does_not_contain_item_due_to_extra_digit_at_end() {
|
||||
assertThat(RecipientId.serializedListContains("4561", RecipientId.from(456))).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_find_all_items_in_triple_list() {
|
||||
assertThat(RecipientId.serializedListContains("11,12,13", RecipientId.from(11))).isTrue()
|
||||
assertThat(RecipientId.serializedListContains("11,12,13", RecipientId.from(12))).isTrue()
|
||||
assertThat(RecipientId.serializedListContains("11,12,13", RecipientId.from(13))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializedListContains_cant_find_similar_items_in_triple_list() {
|
||||
assertThat(RecipientId.serializedListContains("11,12,13", RecipientId.from(1))).isFalse()
|
||||
assertThat(RecipientId.serializedListContains("11,12,13", RecipientId.from(2))).isFalse()
|
||||
assertThat(RecipientId.serializedListContains("11,12,13", RecipientId.from(3))).isFalse()
|
||||
}
|
||||
}
|
|
@ -1,253 +0,0 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc.collections;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class ParticipantCollectionTest {
|
||||
|
||||
private final ParticipantCollection testSubject = new ParticipantCollection(3);
|
||||
|
||||
@Test
|
||||
public void givenAnEmptyCollection_whenIAdd3Participants_thenIExpectThemToBeOrderedByAddedToCallTime() {
|
||||
// GIVEN
|
||||
List<CallParticipant> input = Arrays.asList(participant(1, 1, 4), participant(2, 1, 2), participant(3, 1, 3));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = testSubject.getNext(input);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(3), id(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnEmptyCollection_whenIAdd3Participants_thenIExpectNoListParticipants() {
|
||||
// GIVEN
|
||||
List<CallParticipant> input = Arrays.asList(participant(1, 1, 4), participant(2, 1, 2), participant(3, 1, 3));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = testSubject.getNext(input);
|
||||
|
||||
// THEN
|
||||
assertEquals(result.getListParticipants().size(), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnEmptyColletion_whenIAdd4Participants_thenIExpectThemToBeOrderedByLastSpokenThenAddedToCallTime() {
|
||||
// GIVEN
|
||||
List<CallParticipant> input = Arrays.asList(participant(1, 1, 2),
|
||||
participant(2, 5, 2),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 0));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = testSubject.getNext(input);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(4), id(3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenACollection_whenIUpdateWithEmptyList_thenIExpectEmptyList() {
|
||||
// GIVEN
|
||||
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
|
||||
ParticipantCollection initialCollection = testSubject.getNext(initial);
|
||||
List<CallParticipant> next = Collections.emptyList();
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = initialCollection.getNext(next);
|
||||
|
||||
// THEN
|
||||
assertEquals(0, result.getGridParticipants().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenACollection_whenIUpdateWithLatestSpeakerAndSpeakerIsAlreadyInGridSection_thenIExpectTheSameGridSectionOrder() {
|
||||
// GIVEN
|
||||
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
|
||||
ParticipantCollection initialCollection = testSubject.getNext(initial);
|
||||
List<CallParticipant> next = Arrays.asList(participant(1, 1, 2), participant(2, 2, 3), participant(3, 1, 4));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = initialCollection.getNext(next);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(1), id(2), id(3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenACollection_whenSomeoneLeaves_thenIDoNotExpectToSeeThemInTheNewList() {
|
||||
// GIVEN
|
||||
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
|
||||
ParticipantCollection initialCollection = testSubject.getNext(initial);
|
||||
List<CallParticipant> next = Arrays.asList(participant(2, 2, 3), participant(3, 1, 4));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = initialCollection.getNext(next);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenACollection_whenMultipleLeave_thenIDoNotExpectToSeeThemInTheNewList() {
|
||||
// GIVEN
|
||||
ParticipantCollection testSubject = new ParticipantCollection(4);
|
||||
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4), participant(4, 1, 5));
|
||||
ParticipantCollection initialCollection = testSubject.getNext(initial);
|
||||
List<CallParticipant> next = Arrays.asList(participant(3, 1, 4), participant(2, 1, 3));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = initialCollection.getNext(next);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(3)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void bigTest() {
|
||||
|
||||
// Welcome to the Thunder dome. 10 people enter...
|
||||
|
||||
ParticipantCollection testSubject = new ParticipantCollection(6);
|
||||
List<CallParticipant> init = Arrays.asList(participant(1, 1, 1), // Alice
|
||||
participant(2, 1, 1), // Bob
|
||||
participant(3, 1, 1), // Charlie
|
||||
participant(4, 1, 1), // Diane
|
||||
participant(5, 1, 1), // Ethel
|
||||
participant(6, 1, 1), // Francis
|
||||
participant(7, 1, 1), // Georgina
|
||||
participant(8, 1, 1), // Henry
|
||||
participant(9, 1, 1), // Ignace
|
||||
participant(10, 1, 1)); // Jericho
|
||||
|
||||
ParticipantCollection initialCollection = testSubject.getNext(init);
|
||||
|
||||
assertThat(initialCollection.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(6)));
|
||||
assertThat(initialCollection.getListParticipants(), Matchers.contains(id(7), id(8), id(9), id(10)));
|
||||
|
||||
// Bob speaks about his trip to antigua...
|
||||
|
||||
List<CallParticipant> bobSpoke = Arrays.asList(participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 1, 1),
|
||||
participant(9, 1, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterBobSpoke = initialCollection.getNext(bobSpoke);
|
||||
|
||||
assertThat(afterBobSpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(6)));
|
||||
assertThat(afterBobSpoke.getListParticipants(), Matchers.contains(id(7), id(8), id(9), id(10)));
|
||||
|
||||
// Henry interjects and says now is not the time, this is the thunderdome.
|
||||
|
||||
List<CallParticipant> henrySpoke = Arrays.asList(participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 1, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterHenrySpoke = afterBobSpoke.getNext(henrySpoke);
|
||||
|
||||
assertThat(afterHenrySpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(8)));
|
||||
assertThat(afterHenrySpoke.getListParticipants(), Matchers.contains(id(6), id(7), id(9), id(10)));
|
||||
|
||||
// Ignace asks how everone's holidays were
|
||||
|
||||
List<CallParticipant> ignaceSpoke = Arrays.asList(participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterIgnaceSpoke = afterHenrySpoke.getNext(ignaceSpoke);
|
||||
|
||||
assertThat(afterIgnaceSpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(9), id(8)));
|
||||
assertThat(afterIgnaceSpoke.getListParticipants(), Matchers.contains(id(5), id(6), id(7), id(10)));
|
||||
|
||||
// Alice is the first to fall
|
||||
|
||||
List<CallParticipant> aliceLeft = Arrays.asList(participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterAliceLeft = afterIgnaceSpoke.getNext(aliceLeft);
|
||||
|
||||
assertThat(afterAliceLeft.getGridParticipants(), Matchers.contains(id(5), id(2), id(3), id(4), id(9), id(8)));
|
||||
assertThat(afterAliceLeft.getListParticipants(), Matchers.contains(id(6), id(7), id(10)));
|
||||
|
||||
// Just kidding, Alice is back. Georgina and Charlie gasp!
|
||||
|
||||
List<CallParticipant> mixUp = Arrays.asList(participant(1, 1, 5),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 6, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 5, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterMixUp = afterAliceLeft.getNext(mixUp);
|
||||
|
||||
assertThat(afterMixUp.getGridParticipants(), Matchers.contains(id(7), id(2), id(3), id(4), id(9), id(8)));
|
||||
assertThat(afterMixUp.getListParticipants(), Matchers.contains(id(5), id(6), id(10), id(1)));
|
||||
}
|
||||
|
||||
private Matcher<CallParticipant> id(long serializedId) {
|
||||
return Matchers.hasProperty("callParticipantId", Matchers.equalTo(new CallParticipantId(serializedId, RecipientId.from(serializedId))));
|
||||
}
|
||||
|
||||
private static CallParticipant participant(long serializedId,long lastSpoke, long added) {
|
||||
return CallParticipant.createRemote(
|
||||
new CallParticipantId(serializedId, RecipientId.from(serializedId)),
|
||||
Recipient.UNKNOWN,
|
||||
null,
|
||||
new BroadcastVideoSink(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
CallParticipant.HAND_LOWERED,
|
||||
lastSpoke,
|
||||
false,
|
||||
added,
|
||||
false,
|
||||
CallParticipant.DeviceOrdinal.PRIMARY);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc.collections
|
||||
|
||||
import assertk.Assert
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.each
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.CallParticipant.Companion.createRemote
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class ParticipantCollectionTest {
|
||||
private val testSubject = ParticipantCollection(3)
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyCollection_whenIAdd3Participants_thenIExpectThemToBeOrderedByAddedToCallTime() {
|
||||
// GIVEN
|
||||
val input = listOf(
|
||||
participant(1, 1, 4),
|
||||
participant(2, 1, 2),
|
||||
participant(3, 1, 3)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.getNext(input)
|
||||
|
||||
// THEN
|
||||
assertThat(result.gridParticipants).containsParticipantIds(2, 3, 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyCollection_whenIAdd3Participants_thenIExpectNoListParticipants() {
|
||||
// GIVEN
|
||||
val input = listOf(
|
||||
participant(1, 1, 4),
|
||||
participant(2, 1, 2),
|
||||
participant(3, 1, 3)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.getNext(input)
|
||||
|
||||
// THEN
|
||||
assertThat(result.listParticipants).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyColletion_whenIAdd4Participants_thenIExpectThemToBeOrderedByLastSpokenThenAddedToCallTime() {
|
||||
// GIVEN
|
||||
val input = listOf(
|
||||
participant(1, 1, 2),
|
||||
participant(2, 5, 2),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 0)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.getNext(input)
|
||||
|
||||
// THEN
|
||||
assertThat(result.gridParticipants).containsParticipantIds(2, 4, 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACollection_whenIUpdateWithEmptyList_thenIExpectEmptyList() {
|
||||
// GIVEN
|
||||
val initial = listOf(
|
||||
participant(1, 1, 2),
|
||||
participant(2, 1, 3),
|
||||
participant(3, 1, 4)
|
||||
)
|
||||
val initialCollection = testSubject.getNext(initial)
|
||||
val next = emptyList<CallParticipant>()
|
||||
|
||||
// WHEN
|
||||
val result = initialCollection.getNext(next)
|
||||
|
||||
// THEN
|
||||
assertThat(result.gridParticipants).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACollection_whenIUpdateWithLatestSpeakerAndSpeakerIsAlreadyInGridSection_thenIExpectTheSameGridSectionOrder() {
|
||||
// GIVEN
|
||||
val initial = listOf(
|
||||
participant(1, 1, 2),
|
||||
participant(2, 1, 3),
|
||||
participant(3, 1, 4)
|
||||
)
|
||||
val initialCollection = testSubject.getNext(initial)
|
||||
val next = listOf(
|
||||
participant(1, 1, 2),
|
||||
participant(2, 2, 3),
|
||||
participant(3, 1, 4)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = initialCollection.getNext(next)
|
||||
|
||||
// THEN
|
||||
assertThat(result.gridParticipants).containsParticipantIds(1, 2, 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACollection_whenSomeoneLeaves_thenIDoNotExpectToSeeThemInTheNewList() {
|
||||
// GIVEN
|
||||
val initial = listOf(
|
||||
participant(1, 1, 2),
|
||||
participant(2, 1, 3),
|
||||
participant(3, 1, 4)
|
||||
)
|
||||
val initialCollection = testSubject.getNext(initial)
|
||||
val next = listOf(
|
||||
participant(2, 2, 3),
|
||||
participant(3, 1, 4)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = initialCollection.getNext(next)
|
||||
|
||||
// THEN
|
||||
assertThat(result.gridParticipants).containsParticipantIds(2, 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACollection_whenMultipleLeave_thenIDoNotExpectToSeeThemInTheNewList() {
|
||||
// GIVEN
|
||||
val testSubject = ParticipantCollection(4)
|
||||
val initial = listOf(
|
||||
participant(1, 1, 2),
|
||||
participant(2, 1, 3),
|
||||
participant(3, 1, 4),
|
||||
participant(4, 1, 5)
|
||||
)
|
||||
val initialCollection = testSubject.getNext(initial)
|
||||
val next = listOf(
|
||||
participant(3, 1, 4),
|
||||
participant(2, 1, 3)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = initialCollection.getNext(next)
|
||||
|
||||
// THEN
|
||||
assertThat(result.gridParticipants).containsParticipantIds(2, 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bigTest() {
|
||||
// Welcome to the Thunder dome. 10 people enter...
|
||||
|
||||
val testSubject = ParticipantCollection(6)
|
||||
val init = listOf(
|
||||
participant(1, 1, 1), // Alice
|
||||
participant(2, 1, 1), // Bob
|
||||
participant(3, 1, 1), // Charlie
|
||||
participant(4, 1, 1), // Diane
|
||||
participant(5, 1, 1), // Ethel
|
||||
participant(6, 1, 1), // Francis
|
||||
participant(7, 1, 1), // Georgina
|
||||
participant(8, 1, 1), // Henry
|
||||
participant(9, 1, 1), // Ignace
|
||||
participant(10, 1, 1) // Jericho
|
||||
)
|
||||
|
||||
val initialCollection = testSubject.getNext(init)
|
||||
|
||||
assertThat(initialCollection.gridParticipants).containsParticipantIds(1, 2, 3, 4, 5, 6)
|
||||
assertThat(initialCollection.listParticipants).containsParticipantIds(7, 8, 9, 10)
|
||||
|
||||
// Bob speaks about his trip to antigua...
|
||||
val bobSpoke = listOf(
|
||||
participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 1, 1),
|
||||
participant(9, 1, 1),
|
||||
participant(10, 1, 1)
|
||||
)
|
||||
|
||||
val afterBobSpoke = initialCollection.getNext(bobSpoke)
|
||||
|
||||
assertThat(afterBobSpoke.gridParticipants).containsParticipantIds(1, 2, 3, 4, 5, 6)
|
||||
assertThat(afterBobSpoke.listParticipants).containsParticipantIds(7, 8, 9, 10)
|
||||
|
||||
// Henry interjects and says now is not the time, this is the thunderdome.
|
||||
val henrySpoke = listOf(
|
||||
participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 1, 1),
|
||||
participant(10, 1, 1)
|
||||
)
|
||||
|
||||
val afterHenrySpoke = afterBobSpoke.getNext(henrySpoke)
|
||||
|
||||
assertThat(afterHenrySpoke.gridParticipants).containsParticipantIds(1, 2, 3, 4, 5, 8)
|
||||
assertThat(afterHenrySpoke.listParticipants).containsParticipantIds(6, 7, 9, 10)
|
||||
|
||||
// Ignace asks how everyone's holidays were
|
||||
val ignaceSpoke = listOf(
|
||||
participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1)
|
||||
)
|
||||
|
||||
val afterIgnaceSpoke = afterHenrySpoke.getNext(ignaceSpoke)
|
||||
|
||||
assertThat(afterIgnaceSpoke.gridParticipants).containsParticipantIds(1, 2, 3, 4, 9, 8)
|
||||
assertThat(afterIgnaceSpoke.listParticipants).containsParticipantIds(5, 6, 7, 10)
|
||||
|
||||
// Alice is the first to fall
|
||||
val aliceLeft = listOf(
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1)
|
||||
)
|
||||
|
||||
val afterAliceLeft = afterIgnaceSpoke.getNext(aliceLeft)
|
||||
|
||||
assertThat(afterAliceLeft.gridParticipants).containsParticipantIds(5, 2, 3, 4, 9, 8)
|
||||
assertThat(afterAliceLeft.listParticipants).containsParticipantIds(6, 7, 10)
|
||||
|
||||
// Just kidding, Alice is back. Georgina and Charlie gasp!
|
||||
val mixUp = listOf(
|
||||
participant(1, 1, 5),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 6, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 5, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1)
|
||||
)
|
||||
|
||||
val afterMixUp = afterAliceLeft.getNext(mixUp)
|
||||
|
||||
assertThat(afterMixUp.gridParticipants).containsParticipantIds(7, 2, 3, 4, 9, 8)
|
||||
assertThat(afterMixUp.listParticipants).containsParticipantIds(5, 6, 10, 1)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun Assert<List<CallParticipant>>.containsParticipantIds(vararg expectedParticipantIds: Long) {
|
||||
transform("Same sizes") { it.size }.isEqualTo(expectedParticipantIds.size)
|
||||
|
||||
transform { it.zip(expectedParticipantIds.asList()) }
|
||||
.each { assertionPair ->
|
||||
assertionPair.transform { (actualCallParticipant, expectedParticipantId) ->
|
||||
assertk.assertThat(actualCallParticipant.callParticipantId)
|
||||
.isEqualTo(CallParticipantId(expectedParticipantId, RecipientId.from(expectedParticipantId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun participant(serializedId: Long, lastSpoke: Long, added: Long): CallParticipant {
|
||||
return createRemote(
|
||||
callParticipantId = CallParticipantId(serializedId, RecipientId.from(serializedId)),
|
||||
recipient = Recipient.UNKNOWN,
|
||||
identityKey = null,
|
||||
renderer = BroadcastVideoSink(),
|
||||
isForwardingVideo = false,
|
||||
audioEnabled = false,
|
||||
videoEnabled = false,
|
||||
handRaisedTimestamp = CallParticipant.HAND_LOWERED,
|
||||
lastSpoke = lastSpoke,
|
||||
mediaKeysReceived = false,
|
||||
addedToCallTime = added,
|
||||
isScreenSharing = false,
|
||||
deviceOrdinal = CallParticipant.DeviceOrdinal.PRIMARY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
package org.thoughtcrime.securesms.testutil;
|
||||
|
||||
import org.hamcrest.BaseMatcher;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -93,46 +90,4 @@ public final class LogRecorder extends Log.Logger {
|
|||
return throwable;
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Matcher<T> hasMessages(T... messages) {
|
||||
return new BaseMatcher<T>() {
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendValueList("[", ", ", "]", messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeMismatch(Object item, Description description) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entry> list = (List<Entry>) item;
|
||||
ArrayList<String> messages = new ArrayList<>(list.size());
|
||||
|
||||
for (Entry e : list) {
|
||||
messages.add(e.message);
|
||||
}
|
||||
|
||||
description.appendText("was ").appendValueList("[", ", ", "]", messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Object item) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entry> list = (List<Entry>) item;
|
||||
|
||||
if (list.size() != messages.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
if (!list.get(i).message.equals(messages[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
public final class DecryptedGroupUtilTest {
|
||||
|
||||
@Test
|
||||
public void can_extract_editor_uuid_from_decrypted_group_change() {
|
||||
ACI aci = ACI.from(UUID.randomUUID());
|
||||
ByteString editor = aci.toByteString();
|
||||
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
|
||||
.editorServiceIdBytes(editor)
|
||||
.build();
|
||||
|
||||
ServiceId parsed = DecryptedGroupUtil.editorServiceId(groupChange).get();
|
||||
|
||||
assertEquals(aci, parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_extract_uuid_from_decrypted_pending_member() {
|
||||
ACI aci = ACI.from(UUID.randomUUID());
|
||||
DecryptedPendingMember decryptedMember = new DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(aci.toByteString())
|
||||
.build();
|
||||
|
||||
ServiceId parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes);
|
||||
|
||||
assertEquals(aci, parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_extract_uuid_from_bad_decrypted_pending_member() {
|
||||
DecryptedPendingMember decryptedMember = new DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(ByteString.of(Util.getSecretBytes(18)))
|
||||
.build();
|
||||
|
||||
ServiceId parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes);
|
||||
|
||||
assertNull(parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_extract_uuids_for_all_pending_including_bad_entries() {
|
||||
ACI aci1 = ACI.from(UUID.randomUUID());
|
||||
ACI aci2 = ACI.from(UUID.randomUUID());
|
||||
DecryptedPendingMember decryptedMember1 = new DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(aci1.toByteString())
|
||||
.build();
|
||||
DecryptedPendingMember decryptedMember2 = new DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(aci2.toByteString())
|
||||
.build();
|
||||
DecryptedPendingMember decryptedMember3 = new DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(ByteString.of(Util.getSecretBytes(18)))
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
|
||||
.newPendingMembers(asList(decryptedMember1, decryptedMember2, decryptedMember3))
|
||||
.build();
|
||||
|
||||
List<ServiceId> pendingUuids = DecryptedGroupUtil.pendingToServiceIdList(groupChange.newPendingMembers);
|
||||
|
||||
assertThat(pendingUuids, is(asList(aci1, aci2, ACI.UNKNOWN)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_extract_uuids_for_all_deleted_pending_excluding_bad_entries() {
|
||||
ACI aci1 = ACI.from(UUID.randomUUID());
|
||||
ACI aci2 = ACI.from(UUID.randomUUID());
|
||||
DecryptedPendingMemberRemoval decryptedMember1 = new DecryptedPendingMemberRemoval.Builder()
|
||||
.serviceIdBytes(aci1.toByteString())
|
||||
.build();
|
||||
DecryptedPendingMemberRemoval decryptedMember2 = new DecryptedPendingMemberRemoval.Builder()
|
||||
.serviceIdBytes(aci2.toByteString())
|
||||
.build();
|
||||
DecryptedPendingMemberRemoval decryptedMember3 = new DecryptedPendingMemberRemoval.Builder()
|
||||
.serviceIdBytes(ByteString.of(Util.getSecretBytes(18)))
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
|
||||
.deletePendingMembers(asList(decryptedMember1, decryptedMember2, decryptedMember3))
|
||||
.build();
|
||||
|
||||
List<ServiceId> removedUuids = DecryptedGroupUtil.removedPendingMembersServiceIdList(groupChange);
|
||||
|
||||
assertThat(removedUuids, is(asList(aci1, aci2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_extract_uuids_for_all_deleted_members_excluding_bad_entries() {
|
||||
ACI aci1 = ACI.from(UUID.randomUUID());
|
||||
ACI aci2 = ACI.from(UUID.randomUUID());
|
||||
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
|
||||
.deleteMembers(asList(aci1.toByteString(), aci2.toByteString(), ByteString.of(Util.getSecretBytes(18))))
|
||||
.build();
|
||||
|
||||
List<ServiceId> removedServiceIds = DecryptedGroupUtil.removedMembersServiceIdList(groupChange);
|
||||
|
||||
assertThat(removedServiceIds, is(asList(aci1, aci2)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import okio.ByteString
|
||||
import org.junit.Test
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.util.Util
|
||||
import java.util.UUID
|
||||
|
||||
class DecryptedGroupUtilTest {
|
||||
@Test
|
||||
fun can_extract_editor_uuid_from_decrypted_group_change() {
|
||||
val aci = randomACI()
|
||||
val editor = aci.toByteString()
|
||||
val groupChange = DecryptedGroupChange.Builder()
|
||||
.editorServiceIdBytes(editor)
|
||||
.build()
|
||||
|
||||
val parsed = DecryptedGroupUtil.editorServiceId(groupChange).get()
|
||||
|
||||
assertThat(parsed).isEqualTo(aci)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun can_extract_uuid_from_decrypted_pending_member() {
|
||||
val aci = randomACI()
|
||||
val decryptedMember = DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(aci.toByteString())
|
||||
.build()
|
||||
|
||||
val parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes)
|
||||
|
||||
assertThat(parsed).isEqualTo(aci)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun can_extract_uuid_from_bad_decrypted_pending_member() {
|
||||
val decryptedMember = DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(ByteString.of(*Util.getSecretBytes(18)))
|
||||
.build()
|
||||
|
||||
val parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes)
|
||||
|
||||
assertThat(parsed).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun can_extract_uuids_for_all_pending_including_bad_entries() {
|
||||
val aci1 = randomACI()
|
||||
val aci2 = randomACI()
|
||||
val decryptedMember1 = DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(aci1.toByteString())
|
||||
.build()
|
||||
val decryptedMember2 = DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(aci2.toByteString())
|
||||
.build()
|
||||
val decryptedMember3 = DecryptedPendingMember.Builder()
|
||||
.serviceIdBytes(ByteString.of(*Util.getSecretBytes(18)))
|
||||
.build()
|
||||
|
||||
val groupChange = DecryptedGroupChange.Builder()
|
||||
.newPendingMembers(listOf(decryptedMember1, decryptedMember2, decryptedMember3))
|
||||
.build()
|
||||
|
||||
val pendingUuids = DecryptedGroupUtil.pendingToServiceIdList(groupChange.newPendingMembers)
|
||||
|
||||
assertThat(pendingUuids).containsExactly(aci1, aci2, ServiceId.ACI.UNKNOWN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun can_extract_uuids_for_all_deleted_pending_excluding_bad_entries() {
|
||||
val aci1 = randomACI()
|
||||
val aci2 = randomACI()
|
||||
val decryptedMember1 = DecryptedPendingMemberRemoval.Builder()
|
||||
.serviceIdBytes(aci1.toByteString())
|
||||
.build()
|
||||
val decryptedMember2 = DecryptedPendingMemberRemoval.Builder()
|
||||
.serviceIdBytes(aci2.toByteString())
|
||||
.build()
|
||||
val decryptedMember3 = DecryptedPendingMemberRemoval.Builder()
|
||||
.serviceIdBytes(ByteString.of(*Util.getSecretBytes(18)))
|
||||
.build()
|
||||
|
||||
val groupChange = DecryptedGroupChange.Builder()
|
||||
.deletePendingMembers(listOf(decryptedMember1, decryptedMember2, decryptedMember3))
|
||||
.build()
|
||||
|
||||
val removedUuids = DecryptedGroupUtil.removedPendingMembersServiceIdList(groupChange)
|
||||
|
||||
assertThat(removedUuids).containsExactly(aci1, aci2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun can_extract_uuids_for_all_deleted_members_excluding_bad_entries() {
|
||||
val aci1 = randomACI()
|
||||
val aci2 = randomACI()
|
||||
val groupChange = DecryptedGroupChange.Builder()
|
||||
.deleteMembers(listOf(aci1.toByteString(), aci2.toByteString(), ByteString.of(*Util.getSecretBytes(18))))
|
||||
.build()
|
||||
|
||||
val removedServiceIds = DecryptedGroupUtil.removedMembersServiceIdList(groupChange)
|
||||
|
||||
assertThat(removedServiceIds).containsExactly(aci1, aci2)
|
||||
}
|
||||
|
||||
private fun randomACI() = ServiceId.ACI.from(UUID.randomUUID())
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.hasItems;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember;
|
||||
|
||||
public final class GroupsV2Operations_ban_Test {
|
||||
|
||||
private GroupsV2Operations.GroupOperations groupOperations;
|
||||
|
||||
@Before
|
||||
public void setup() throws InvalidInputException {
|
||||
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS();
|
||||
|
||||
TestZkGroupServer server = new TestZkGroupServer();
|
||||
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
|
||||
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
|
||||
|
||||
groupOperations = new GroupsV2Operations(clientZkOperations, 10).forGroup(groupSecretParams);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addBanToEmptyList() {
|
||||
ACI ban = ACI.from(UUID.randomUUID());
|
||||
|
||||
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(Collections.singleton(ban),
|
||||
false,
|
||||
Collections.emptyList());
|
||||
|
||||
assertThat(banUuidsChange.addBannedMembers.size(), is(1));
|
||||
assertThat(banUuidsChange.addBannedMembers.get(0).added.userId, is(groupOperations.encryptServiceId(ban)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addBanToPartialFullList() {
|
||||
ACI toBan = ACI.from(UUID.randomUUID());
|
||||
List<DecryptedBannedMember> alreadyBanned = new ArrayList<>(5);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
alreadyBanned.add(bannedMember(UUID.randomUUID()));
|
||||
}
|
||||
|
||||
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(Collections.singleton(toBan),
|
||||
false,
|
||||
alreadyBanned);
|
||||
|
||||
assertThat(banUuidsChange.addBannedMembers.size(), is(1));
|
||||
assertThat(banUuidsChange.addBannedMembers.get(0).added.userId, is(groupOperations.encryptServiceId(toBan)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addBanToFullList() {
|
||||
ACI toBan = ACI.from(UUID.randomUUID());
|
||||
List<DecryptedBannedMember> alreadyBanned = new ArrayList<>(10);
|
||||
DecryptedBannedMember oldest = null;
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
DecryptedBannedMember member = bannedMember(UUID.randomUUID()).newBuilder().timestamp(100 + i).build();
|
||||
if (oldest == null) {
|
||||
oldest = member;
|
||||
}
|
||||
alreadyBanned.add(member);
|
||||
}
|
||||
|
||||
Collections.shuffle(alreadyBanned);
|
||||
|
||||
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(Collections.singleton(toBan),
|
||||
false,
|
||||
alreadyBanned);
|
||||
|
||||
assertThat(banUuidsChange.deleteBannedMembers.size(), is(1));
|
||||
assertThat(banUuidsChange.deleteBannedMembers.get(0).deletedUserId, is(groupOperations.encryptServiceId(ServiceId.parseOrThrow(oldest.serviceIdBytes))));
|
||||
|
||||
|
||||
assertThat(banUuidsChange.addBannedMembers.size(), is(1));
|
||||
assertThat(banUuidsChange.addBannedMembers.get(0).added.userId, is(groupOperations.encryptServiceId(toBan)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addMultipleBanToFullList() {
|
||||
List<ACI> toBan = new ArrayList<>();
|
||||
toBan.add(ACI.from(UUID.randomUUID()));
|
||||
toBan.add(ACI.from(UUID.randomUUID()));
|
||||
|
||||
List<DecryptedBannedMember> alreadyBanned = new ArrayList<>(10);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
alreadyBanned.add(bannedMember(UUID.randomUUID()).newBuilder().timestamp(100 + i).build());
|
||||
}
|
||||
|
||||
List<ByteString> oldest = new ArrayList<>(2);
|
||||
oldest.add(groupOperations.encryptServiceId(ServiceId.parseOrThrow(alreadyBanned.get(0).serviceIdBytes)));
|
||||
oldest.add(groupOperations.encryptServiceId(ServiceId.parseOrThrow(alreadyBanned.get(1).serviceIdBytes)));
|
||||
|
||||
Collections.shuffle(alreadyBanned);
|
||||
|
||||
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(new HashSet<>(toBan),
|
||||
false,
|
||||
alreadyBanned);
|
||||
|
||||
assertThat(banUuidsChange.deleteBannedMembers.size(), is(2));
|
||||
assertThat(banUuidsChange.deleteBannedMembers
|
||||
.stream()
|
||||
.map(a -> a.deletedUserId)
|
||||
.collect(Collectors.toList()),
|
||||
hasItems(oldest.get(0), oldest.get(1)));
|
||||
|
||||
|
||||
assertThat(banUuidsChange.addBannedMembers.size(), is(2));
|
||||
assertThat(banUuidsChange.addBannedMembers
|
||||
.stream()
|
||||
.map(a -> a.added)
|
||||
.map(b -> b.userId)
|
||||
.collect(Collectors.toList()),
|
||||
hasItems(groupOperations.encryptServiceId(toBan.get(0)),
|
||||
groupOperations.encryptServiceId(toBan.get(1))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.single
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations.GroupOperations
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.util.Util
|
||||
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class GroupsV2Operations_ban_Test {
|
||||
private lateinit var groupOperations: GroupOperations
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
|
||||
|
||||
val server = TestZkGroupServer()
|
||||
val groupSecretParams = GroupSecretParams.deriveFromMasterKey(GroupMasterKey(Util.getSecretBytes(32)))
|
||||
val clientZkOperations = ClientZkOperations(server.serverPublicParams)
|
||||
|
||||
groupOperations = GroupsV2Operations(clientZkOperations, 10).forGroup(groupSecretParams)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addBanToEmptyList() {
|
||||
val ban = randomACI()
|
||||
|
||||
val banUuidsChange = groupOperations.createBanServiceIdsChange(
|
||||
/* banServiceIds = */
|
||||
setOf(ban),
|
||||
/* rejectJoinRequest = */
|
||||
false,
|
||||
/* bannedMembersList = */
|
||||
emptyList()
|
||||
)
|
||||
|
||||
assertThat(banUuidsChange.addBannedMembers)
|
||||
.single()
|
||||
.transform { it.added?.userId }
|
||||
.isEqualTo(groupOperations.encryptServiceId(ban))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addBanToPartialFullList() {
|
||||
val toBan = randomACI()
|
||||
val alreadyBanned = (0 until 5).map { ProtoTestUtils.bannedMember(UUID.randomUUID()) }
|
||||
|
||||
val banUuidsChange = groupOperations.createBanServiceIdsChange(
|
||||
/* banServiceIds = */
|
||||
setOf(toBan),
|
||||
/* rejectJoinRequest = */
|
||||
false,
|
||||
/* bannedMembersList = */
|
||||
alreadyBanned
|
||||
)
|
||||
|
||||
assertThat(banUuidsChange.addBannedMembers)
|
||||
.single()
|
||||
.transform { it.added?.userId }
|
||||
.isEqualTo(groupOperations.encryptServiceId(toBan))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addBanToFullList() {
|
||||
val toBan = ServiceId.ACI.from(UUID.randomUUID())
|
||||
|
||||
val alreadyBanned = (0 until 10).map { i ->
|
||||
ProtoTestUtils.bannedMember(UUID.randomUUID())
|
||||
.newBuilder()
|
||||
.timestamp(100L + i)
|
||||
.build()
|
||||
}.shuffled()
|
||||
|
||||
val banUuidsChange = groupOperations.createBanServiceIdsChange(
|
||||
/* banServiceIds = */
|
||||
setOf(toBan),
|
||||
/* rejectJoinRequest = */
|
||||
false,
|
||||
/* bannedMembersList = */
|
||||
alreadyBanned
|
||||
)
|
||||
|
||||
val oldest = alreadyBanned.minBy { it.timestamp }
|
||||
assertThat(banUuidsChange.deleteBannedMembers)
|
||||
.single()
|
||||
.transform { it.deletedUserId }
|
||||
.isEqualTo(groupOperations.encryptServiceId(ServiceId.parseOrThrow(oldest.serviceIdBytes)))
|
||||
|
||||
assertThat(banUuidsChange.addBannedMembers)
|
||||
.single()
|
||||
.transform { it.added?.userId }
|
||||
.isEqualTo(groupOperations.encryptServiceId(toBan))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addMultipleBanToFullList() {
|
||||
val toBan = (0 until 2).map { ServiceId.ACI.from(UUID.randomUUID()) }
|
||||
|
||||
val alreadyBanned = (0 until 10).map { i ->
|
||||
ProtoTestUtils.bannedMember(UUID.randomUUID())
|
||||
.newBuilder()
|
||||
.timestamp(100L + i)
|
||||
.build()
|
||||
}.shuffled()
|
||||
|
||||
val banUuidsChange = groupOperations.createBanServiceIdsChange(
|
||||
/* banServiceIds = */
|
||||
toBan.toMutableSet(),
|
||||
/* rejectJoinRequest = */
|
||||
false,
|
||||
/* bannedMembersList = */
|
||||
alreadyBanned
|
||||
)
|
||||
|
||||
val oldestTwo = alreadyBanned
|
||||
.sortedBy { it.timestamp }
|
||||
.subList(0, 2)
|
||||
.map { groupOperations.encryptServiceId(ServiceId.parseOrThrow(it.serviceIdBytes)) }
|
||||
.toTypedArray()
|
||||
assertThat(banUuidsChange.deleteBannedMembers)
|
||||
.transform { members ->
|
||||
members.map { member ->
|
||||
member.deletedUserId
|
||||
}
|
||||
}
|
||||
.containsExactly(*oldestTwo)
|
||||
|
||||
val newBans = (0..1).map { i ->
|
||||
groupOperations.encryptServiceId(toBan[i])
|
||||
}.toTypedArray()
|
||||
assertThat(banUuidsChange.addBannedMembers)
|
||||
.transform { members ->
|
||||
members.map { member ->
|
||||
member.added?.userId
|
||||
}
|
||||
}
|
||||
.containsExactly(*newBans)
|
||||
}
|
||||
|
||||
private fun randomACI(): ServiceId.ACI = ServiceId.ACI.from(UUID.randomUUID())
|
||||
}
|
Loading…
Add table
Reference in a new issue