diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/SelectedContactSetTest.java b/app/src/test/java/org/thoughtcrime/securesms/contacts/SelectedContactSetTest.java deleted file mode 100644 index 10d3a15a70..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/contacts/SelectedContactSetTest.java +++ /dev/null @@ -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))); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/SelectedContactSetTest.kt b/app/src/test/java/org/thoughtcrime/securesms/contacts/SelectedContactSetTest.kt new file mode 100644 index 0000000000..27bcede34b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/contacts/SelectedContactSetTest.kt @@ -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) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.java deleted file mode 100644 index 9d21f52597..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.java +++ /dev/null @@ -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))); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.kt new file mode 100644 index 0000000000..b41d19d8f7 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.kt @@ -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()) +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.java deleted file mode 100644 index 3053d99f92..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.java +++ /dev/null @@ -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 expectedRemainingLogs, DecryptedGroup updatedGroupState, List 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()); - } -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.kt new file mode 100644 index 0000000000..c0b46662cc --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.kt @@ -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, + updatedGroupState: DecryptedGroup?, + remainingLogs: List + ) { + 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) + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/StateChainTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/StateChainTest.java deleted file mode 100644 index fbeaf90933..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/StateChainTest.java +++ /dev/null @@ -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 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 pair(char c, Integer i) { - return new StateChain.Pair<>(c, i); - } -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/StateChainTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/StateChainTest.kt new file mode 100644 index 0000000000..6f7128bf0b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/StateChainTest.kt @@ -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( + { 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 stateChainPair(first: A & Any, second: B): StateChain.Pair { + return StateChain.Pair(first, second) + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/l10n/SupportArticleTest.java b/app/src/test/java/org/thoughtcrime/securesms/l10n/SupportArticleTest.java deleted file mode 100644 index 6568a046ff..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/l10n/SupportArticleTest.java +++ /dev/null @@ -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: - *

- * - 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 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(); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/l10n/SupportArticleTest.kt b/app/src/test/java/org/thoughtcrime/securesms/l10n/SupportArticleTest.kt new file mode 100644 index 0000000000..385921b73b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/l10n/SupportArticleTest.kt @@ -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: + *

+ * - 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() + 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_]+)?") + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientIdSerializationTest.java b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientIdSerializationTest.java deleted file mode 100644 index b4b1f41576..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientIdSerializationTest.java +++ /dev/null @@ -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 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))); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientIdSerializationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientIdSerializationTest.kt new file mode 100644 index 0000000000..e4bab8fc03 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientIdSerializationTest.kt @@ -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() + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java b/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java deleted file mode 100644 index 664bdea89b..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java +++ /dev/null @@ -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 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 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 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 initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4)); - ParticipantCollection initialCollection = testSubject.getNext(initial); - List next = Collections.emptyList(); - - // WHEN - ParticipantCollection result = initialCollection.getNext(next); - - // THEN - assertEquals(0, result.getGridParticipants().size()); - } - - @Test - public void givenACollection_whenIUpdateWithLatestSpeakerAndSpeakerIsAlreadyInGridSection_thenIExpectTheSameGridSectionOrder() { - // GIVEN - List initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4)); - ParticipantCollection initialCollection = testSubject.getNext(initial); - List 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 initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4)); - ParticipantCollection initialCollection = testSubject.getNext(initial); - List 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 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 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 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 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 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 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 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 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 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); - } -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.kt b/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.kt new file mode 100644 index 0000000000..668a14785e --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.kt @@ -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() + + // 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>.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 + ) + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java index 4860be1b1e..b800384f93 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java @@ -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 Matcher hasMessages(T... messages) { - return new BaseMatcher() { - - @Override - public void describeTo(Description description) { - description.appendValueList("[", ", ", "]", messages); - } - - @Override - public void describeMismatch(Object item, Description description) { - @SuppressWarnings("unchecked") - List list = (List) item; - ArrayList 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 list = (List) 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; - } - }; - } } diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.java b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.java deleted file mode 100644 index 0c8fcb9efc..0000000000 --- a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.java +++ /dev/null @@ -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 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 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 removedServiceIds = DecryptedGroupUtil.removedMembersServiceIdList(groupChange); - - assertThat(removedServiceIds, is(asList(aci1, aci2))); - } -} diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.kt b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.kt new file mode 100644 index 0000000000..772d329b16 --- /dev/null +++ b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.kt @@ -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()) +} diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_ban_Test.java b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_ban_Test.java deleted file mode 100644 index df23698390..0000000000 --- a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_ban_Test.java +++ /dev/null @@ -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 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 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 toBan = new ArrayList<>(); - toBan.add(ACI.from(UUID.randomUUID())); - toBan.add(ACI.from(UUID.randomUUID())); - - List alreadyBanned = new ArrayList<>(10); - for (int i = 0; i < 10; i++) { - alreadyBanned.add(bannedMember(UUID.randomUUID()).newBuilder().timestamp(100 + i).build()); - } - - List 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)))); - } -} diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_ban_Test.kt b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_ban_Test.kt new file mode 100644 index 0000000000..af6b88e812 --- /dev/null +++ b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_ban_Test.kt @@ -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()) +}