Convert tests to Kotlin, remove Hamcrest.

Resolves #13884
This commit is contained in:
Jameson Williams 2025-01-07 18:20:37 -06:00 committed by Greyson Parrelli
parent 2a2a6e6a0d
commit 33c918defd
19 changed files with 2001 additions and 1703 deletions

View file

@ -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)));
}
}

View file

@ -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)
}
}

View file

@ -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)));
}
}

View file

@ -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())
}

View file

@ -1,515 +0,0 @@
package org.thoughtcrime.securesms.groups.v2.processing;
import org.junit.Before;
import org.junit.Test;
import org.signal.core.util.logging.Log;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.thoughtcrime.securesms.testutil.LogRecorder;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import kotlin.collections.CollectionsKt;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.thoughtcrime.securesms.groups.v2.processing.GroupStatePatcher.LATEST;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public final class GroupStatePatcherTest {
private static final UUID KNOWN_EDITOR = UUID.randomUUID();
@Before
public void setup() {
Log.initialize(new LogRecorder());
}
@Test
public void unknown_group_with_no_states_to_update() {
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, emptyList(), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertNull(advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_with_no_states_to_update() {
DecryptedGroup currentState = state(0);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, emptyList(), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertSame(currentState, advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void unknown_group_single_state_to_update() {
DecryptedGroupChangeLog log0 = serverLogEntry(0);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log0.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_single_state_to_update() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1), null), 1);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log1))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log1.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_two_states_to_update() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = serverLogEntry(2);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_two_states_to_update_already_on_one() {
DecryptedGroup currentState = state(1);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = serverLogEntry(2);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log2))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_three_states_to_update_stop_at_2() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = serverLogEntry(2);
DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
assertNewState(log2.getGroup(), singletonList(log3), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_three_states_to_update_update_latest() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = serverLogEntry(2);
DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2), asLocal(log3))));
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log3.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void apply_maximum_group_revisions() {
DecryptedGroup currentState = state(Integer.MAX_VALUE - 2);
DecryptedGroupChangeLog log1 = serverLogEntry(Integer.MAX_VALUE - 1);
DecryptedGroupChangeLog log2 = serverLogEntry(Integer.MAX_VALUE);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
assertNewState(log2.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void unknown_group_single_state_to_update_with_missing_change() {
DecryptedGroupChangeLog log0 = serverLogEntryWholeStateOnly(0);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log0.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_single_state_to_update_with_missing_change() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntryWholeStateOnly(1);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1), null), 1);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(localLogEntryNoEditor(1))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log1.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_three_states_to_update_update_latest_handle_missing_change() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = serverLogEntryWholeStateOnly(2);
DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), localLogEntryNoEditor(2), asLocal(log3))));
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log3.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_three_states_to_update_update_latest_handle_gap_with_no_changes() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log3))));
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log3.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void known_group_three_states_to_update_update_latest_handle_gap_with_changes() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroup state3a = new DecryptedGroup.Builder()
.revision(3)
.title("Group Revision " + 3)
.build();
DecryptedGroup state3 = new DecryptedGroup.Builder()
.revision(3)
.title("Group Revision " + 3)
.avatar("Lost Avatar Update")
.build();
DecryptedGroupChangeLog log3 = new DecryptedGroupChangeLog(state3, change(3));
DecryptedGroup state4 = new DecryptedGroup.Builder()
.revision(4)
.title("Group Revision " + 4)
.avatar("Lost Avatar Update")
.build();
DecryptedGroupChangeLog log4 = new DecryptedGroupChangeLog(state4, change(4));
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3, log4), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
new AppliedGroupChangeLog(state3a, log3.getChange()),
new AppliedGroupChangeLog(state3, new DecryptedGroupChange.Builder()
.revision(3)
.newAvatar(new DecryptedString.Builder().value_("Lost Avatar Update").build())
.build()),
asLocal(log4))));
assertNewState(log4.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log4.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void updates_with_all_changes_missing() {
DecryptedGroup currentState = state(5);
DecryptedGroupChangeLog log6 = serverLogEntryWholeStateOnly(6);
DecryptedGroupChangeLog log7 = serverLogEntryWholeStateOnly(7);
DecryptedGroupChangeLog log8 = serverLogEntryWholeStateOnly(8);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log6, log7, log8), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(localLogEntryNoEditor(6), localLogEntryNoEditor(7), localLogEntryNoEditor(8))));
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void updates_with_all_group_states_missing() {
DecryptedGroup currentState = state(6);
DecryptedGroupChangeLog log7 = logEntryMissingState(7);
DecryptedGroupChangeLog log8 = logEntryMissingState(8);
DecryptedGroupChangeLog log9 = logEntryMissingState(9);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(serverLogEntry(7)), asLocal(serverLogEntry(8)), asLocal(serverLogEntry(9)))));
assertNewState(state(9), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(state(9), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void updates_with_a_server_mismatch_inserts_additional_update() {
DecryptedGroup currentState = state(6);
DecryptedGroupChangeLog log7 = serverLogEntry(7);
DecryptedMember newMember = new DecryptedMember.Builder()
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
.build();
DecryptedGroup state7b = new DecryptedGroup.Builder()
.revision(8)
.title("Group Revision " + 8)
.build();
DecryptedGroup state8 = new DecryptedGroup.Builder()
.revision(8)
.title("Group Revision " + 8)
.members(Collections.singletonList(newMember))
.build();
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(state8,
change(8));
DecryptedGroupChangeLog log9 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
.revision(9)
.members(Collections.singletonList(newMember))
.title("Group Revision " + 9)
.build(),
change(9));
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log7),
new AppliedGroupChangeLog(state7b, log8.getChange()),
new AppliedGroupChangeLog(state8, new DecryptedGroupChange.Builder()
.revision(8)
.newMembers(Collections.singletonList(newMember))
.build()),
asLocal(log9))));
assertNewState(log9.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log9.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void local_up_to_date_no_repair_necessary() {
DecryptedGroup currentState = state(6);
DecryptedGroupChangeLog log6 = serverLogEntryWholeStateOnly(6);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
assertNewState(state(6), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(state(6), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void no_repair_change_is_posted_if_the_local_state_is_a_placeholder() {
DecryptedGroup currentState = new DecryptedGroup.Builder()
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
.title("Incorrect group title, Revision " + 6)
.build();
DecryptedGroupChangeLog log6 = serverLogEntry(6);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log6))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log6.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void clears_changes_duplicated_in_the_placeholder() {
ACI newMemberAci = ACI.from(UUID.randomUUID());
DecryptedMember newMember = new DecryptedMember.Builder()
.aciBytes(newMemberAci.toByteString())
.build();
DecryptedMember existingMember = new DecryptedMember.Builder()
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
.build();
DecryptedGroup currentState = new DecryptedGroup.Builder()
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
.title("Group Revision " + 8)
.members(Collections.singletonList(newMember))
.build();
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
.revision(8)
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
.title("Group Revision " + 8)
.build(),
new DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(newMemberAci.toByteString())
.newMembers(Collections.singletonList(newMember))
.build());
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8), null), LATEST);
assertNotNull(log8.getGroup());
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void clears_changes_duplicated_in_a_non_placeholder() {
ACI editorAci = ACI.from(UUID.randomUUID());
ACI newMemberAci = ACI.from(UUID.randomUUID());
DecryptedMember newMember = new DecryptedMember.Builder()
.aciBytes(newMemberAci.toByteString())
.build();
DecryptedMember existingMember = new DecryptedMember.Builder()
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
.build();
DecryptedGroup currentState = new DecryptedGroup.Builder()
.revision(8)
.title("Group Revision " + 8)
.members(Collections.singletonList(existingMember))
.build();
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
.revision(8)
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
.title("Group Revision " + 8)
.build(),
new DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(editorAci.toByteString())
.newMembers(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
.build());
DecryptedGroupChange expectedChange = new DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(editorAci.toByteString())
.newMembers(Collections.singletonList(newMember))
.build();
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8), null), LATEST);
assertNotNull(log8.getGroup());
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange))));
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void notices_changes_in_avatar_and_title_but_not_members_in_placeholder() {
ACI newMemberAci = ACI.from(UUID.randomUUID());
DecryptedMember newMember = new DecryptedMember.Builder()
.aciBytes(newMemberAci.toByteString())
.build();
DecryptedMember existingMember = new DecryptedMember.Builder()
.aciBytes(ACI.from(UUID.randomUUID()).toByteString())
.build();
DecryptedGroup currentState = new DecryptedGroup.Builder()
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
.title("Incorrect group title")
.avatar("Incorrect group avatar")
.members(Collections.singletonList(newMember))
.build();
DecryptedGroupChangeLog log8 = new DecryptedGroupChangeLog(new DecryptedGroup.Builder()
.revision(8)
.members(CollectionsKt.plus(Collections.singletonList(existingMember), newMember))
.title("Group Revision " + 8)
.avatar("Group Avatar " + 8)
.build(),
new DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(newMemberAci.toByteString())
.newMembers(Collections.singletonList(newMember))
.build());
DecryptedGroupChange expectedChange = new DecryptedGroupChange.Builder()
.revision(8)
.newTitle(new DecryptedString.Builder().value_("Group Revision " + 8).build())
.newAvatar(new DecryptedString.Builder().value_("Group Avatar " + 8).build())
.build();
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8), null), LATEST);
assertNotNull(log8.getGroup());
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange))));
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
assertEquals(log8.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
@Test
public void no_actual_change() {
DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = new DecryptedGroupChangeLog(log1.getGroup().newBuilder()
.revision(2)
.build(),
new DecryptedGroupChange.Builder()
.revision(2)
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
.newTitle(new DecryptedString.Builder().value_(log1.getGroup().title).build())
.build());
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
new AppliedGroupChangeLog(log2.getGroup(), new DecryptedGroupChange.Builder()
.revision(2)
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
.build()))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
assertEquals(log2.getGroup(), advanceGroupStateResult.getUpdatedGroupState());
}
private static void assertNewState(DecryptedGroup expectedUpdatedGroupState, List<DecryptedGroupChangeLog> expectedRemainingLogs, DecryptedGroup updatedGroupState, List<DecryptedGroupChangeLog> remainingLogs) {
assertEquals(expectedUpdatedGroupState, updatedGroupState);
assertThat(remainingLogs, is(expectedRemainingLogs));
}
private static DecryptedGroupChangeLog serverLogEntry(int revision) {
return new DecryptedGroupChangeLog(state(revision), change(revision));
}
private static AppliedGroupChangeLog localLogEntryNoEditor(int revision) {
return new AppliedGroupChangeLog(state(revision), changeNoEditor(revision));
}
private static DecryptedGroupChangeLog serverLogEntryWholeStateOnly(int revision) {
return new DecryptedGroupChangeLog(state(revision), null);
}
private static DecryptedGroupChangeLog logEntryMissingState(int revision) {
return new DecryptedGroupChangeLog(null, change(revision));
}
private static DecryptedGroup state(int revision) {
return new DecryptedGroup.Builder()
.revision(revision)
.title("Group Revision " + revision)
.build();
}
private static DecryptedGroupChange change(int revision) {
return new DecryptedGroupChange.Builder()
.revision(revision)
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
.newTitle(new DecryptedString.Builder().value_("Group Revision " + revision).build())
.build();
}
private static DecryptedGroupChange changeNoEditor(int revision) {
return new DecryptedGroupChange.Builder()
.revision(revision)
.newTitle(new DecryptedString.Builder().value_("Group Revision " + revision).build())
.build();
}
private static AppliedGroupChangeLog asLocal(DecryptedGroupChangeLog logEntry) {
assertNotNull(logEntry.getGroup());
return new AppliedGroupChangeLog(logEntry.getGroup(), logEntry.getChange());
}
}

View file

@ -0,0 +1,830 @@
package org.thoughtcrime.securesms.groups.v2.processing
import assertk.assertThat
import assertk.assertions.containsOnly
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.isSameInstanceAs
import org.junit.Before
import org.junit.Test
import org.signal.core.util.logging.Log
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.storageservice.protos.groups.local.DecryptedString
import org.thoughtcrime.securesms.testutil.LogRecorder
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
class GroupStatePatcherTest {
@Before
fun setup() {
Log.initialize(LogRecorder())
}
@Test
fun unknown_group_with_no_states_to_update() {
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = null,
serverHistory = emptyList(),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
10
)
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isNull()
}
@Test
fun known_group_with_no_states_to_update() {
val currentState = state(0)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = emptyList(),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
10
)
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(currentState).isSameInstanceAs(advanceGroupStateResult.updatedGroupState)
}
@Test
fun unknown_group_single_state_to_update() {
val log0 = serverLogEntry(0)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = null,
serverHistory = listOf(log0),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
10
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log0))
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log0.group)
}
@Test
fun known_group_single_state_to_update() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
1
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1))
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log1.group)
}
@Test
fun known_group_two_states_to_update() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val log2 = serverLogEntry(2)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log2),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
2
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log2))
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
}
@Test
fun known_group_two_states_to_update_already_on_one() {
val currentState = state(1)
val log1 = serverLogEntry(1)
val log2 = serverLogEntry(2)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log2),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
2
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log2))
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
}
@Test
fun known_group_three_states_to_update_stop_at_2() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val log2 = serverLogEntry(2)
val log3 = serverLogEntry(3)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log2, log3),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
2
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log2))
assertNewState(
expectedUpdatedGroupState = log2.group,
expectedRemainingLogs = listOf(log3),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
}
@Test
fun known_group_three_states_to_update_update_latest() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val log2 = serverLogEntry(2)
val log3 = serverLogEntry(3)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log2, log3),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
asLocal(log1),
asLocal(log2),
asLocal(log3)
)
assertNewState(
expectedUpdatedGroupState = log3.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log3.group)
}
@Test
fun apply_maximum_group_revisions() {
val currentState = state(Int.MAX_VALUE - 2)
val log1 = serverLogEntry(Int.MAX_VALUE - 1)
val log2 = serverLogEntry(Int.MAX_VALUE)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log2),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log2))
assertNewState(
expectedUpdatedGroupState = log2.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
}
@Test
fun unknown_group_single_state_to_update_with_missing_change() {
val log0 = serverLogEntryWholeStateOnly(0)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = null,
serverHistory = listOf(log0),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
10
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log0))
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log0.group)
}
@Test
fun known_group_single_state_to_update_with_missing_change() {
val currentState = state(0)
val log1 = serverLogEntryWholeStateOnly(1)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
1
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(localLogEntryNoEditor(1))
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log1.group)
}
@Test
fun known_group_three_states_to_update_update_latest_handle_missing_change() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val log2 = serverLogEntryWholeStateOnly(2)
val log3 = serverLogEntry(3)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log2, log3),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
asLocal(log1),
localLogEntryNoEditor(2),
asLocal(log3)
)
assertNewState(
expectedUpdatedGroupState = log3.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log3.group)
}
@Test
fun known_group_three_states_to_update_update_latest_handle_gap_with_no_changes() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val log3 = serverLogEntry(3)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log3),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log1), asLocal(log3))
assertNewState(
expectedUpdatedGroupState = log3.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log3.group)
}
@Test
fun known_group_three_states_to_update_update_latest_handle_gap_with_changes() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val state3a = DecryptedGroup.Builder()
.revision(3)
.title("Group Revision " + 3)
.build()
val state3 = DecryptedGroup.Builder()
.revision(3)
.title("Group Revision " + 3)
.avatar("Lost Avatar Update")
.build()
val log3 = DecryptedGroupChangeLog(state3, change(3))
val state4 = DecryptedGroup.Builder()
.revision(4)
.title("Group Revision " + 4)
.avatar("Lost Avatar Update")
.build()
val log4 = DecryptedGroupChangeLog(state4, change(4))
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log3, log4),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
asLocal(log1),
AppliedGroupChangeLog(state3a, log3.change),
AppliedGroupChangeLog(
state3,
DecryptedGroupChange.Builder()
.revision(3)
.newAvatar(DecryptedString.Builder().value_("Lost Avatar Update").build())
.build()
),
asLocal(log4)
)
assertNewState(
expectedUpdatedGroupState = log4.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log4.group)
}
@Test
fun updates_with_all_changes_missing() {
val currentState = state(5)
val log6 = serverLogEntryWholeStateOnly(6)
val log7 = serverLogEntryWholeStateOnly(7)
val log8 = serverLogEntryWholeStateOnly(8)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log6, log7, log8),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
localLogEntryNoEditor(6),
localLogEntryNoEditor(7),
localLogEntryNoEditor(8)
)
assertNewState(
expectedUpdatedGroupState = log8.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
}
@Test
fun updates_with_all_group_states_missing() {
val currentState = state(6)
val log7 = logEntryMissingState(7)
val log8 = logEntryMissingState(8)
val log9 = logEntryMissingState(9)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log7, log8, log9),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
asLocal(serverLogEntry(7)),
asLocal(serverLogEntry(8)),
asLocal(serverLogEntry(9))
)
assertNewState(
expectedUpdatedGroupState = state(9),
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(state(9))
}
@Test
fun updates_with_a_server_mismatch_inserts_additional_update() {
val currentState = state(6)
val log7 = serverLogEntry(7)
val newMember = DecryptedMember.Builder()
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
.build()
val state7b = DecryptedGroup.Builder()
.revision(8)
.title("Group Revision " + 8)
.build()
val state8 = DecryptedGroup.Builder()
.revision(8)
.title("Group Revision " + 8)
.members(listOf(newMember))
.build()
val log8 = DecryptedGroupChangeLog(
state8,
change(8)
)
val log9 = DecryptedGroupChangeLog(
DecryptedGroup.Builder()
.revision(9)
.members(listOf(newMember))
.title("Group Revision " + 9)
.build(),
change(9)
)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log7, log8, log9),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
asLocal(log7),
AppliedGroupChangeLog(state7b, log8.change),
AppliedGroupChangeLog(
state8,
DecryptedGroupChange.Builder()
.revision(8)
.newMembers(listOf(newMember))
.build()
),
asLocal(log9)
)
assertNewState(
expectedUpdatedGroupState = log9.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log9.group)
}
@Test
fun local_up_to_date_no_repair_necessary() {
val currentState = state(6)
val log6 = serverLogEntryWholeStateOnly(6)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log6),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
assertNewState(
expectedUpdatedGroupState = state(6),
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(state(6))
}
@Test
fun no_repair_change_is_posted_if_the_local_state_is_a_placeholder() {
val currentState = DecryptedGroup.Builder()
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
.title("Incorrect group title, Revision " + 6)
.build()
val log6 = serverLogEntry(6)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log6),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(asLocal(log6))
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log6.group)
}
@Test
fun clears_changes_duplicated_in_the_placeholder() {
val newMemberAci = ServiceId.ACI.from(UUID.randomUUID())
val newMember = DecryptedMember.Builder()
.aciBytes(newMemberAci.toByteString())
.build()
val existingMember = DecryptedMember.Builder()
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
.build()
val currentState = DecryptedGroup.Builder()
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
.title("Group Revision " + 8)
.members(listOf(newMember))
.build()
val log8 = DecryptedGroupChangeLog(
DecryptedGroup.Builder()
.revision(8)
.members(listOf(existingMember).plus(newMember))
.title("Group Revision " + 8)
.build(),
DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(newMemberAci.toByteString())
.newMembers(listOf(newMember))
.build()
)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log8),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(log8.group).isNotNull()
assertThat(advanceGroupStateResult.processedLogEntries).isEmpty()
assertNewState(
expectedUpdatedGroupState = log8.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
}
@Test
fun clears_changes_duplicated_in_a_non_placeholder() {
val editorAci = ServiceId.ACI.from(UUID.randomUUID())
val newMemberAci = ServiceId.ACI.from(UUID.randomUUID())
val newMember = DecryptedMember.Builder()
.aciBytes(newMemberAci.toByteString())
.build()
val existingMember = DecryptedMember.Builder()
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
.build()
val currentState = DecryptedGroup.Builder()
.revision(8)
.title("Group Revision " + 8)
.members(listOf(existingMember))
.build()
val log8 = DecryptedGroupChangeLog(
DecryptedGroup.Builder()
.revision(8)
.members(listOf(existingMember).plus(newMember))
.title("Group Revision " + 8)
.build(),
DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(editorAci.toByteString())
.newMembers(listOf(existingMember).plus(newMember))
.build()
)
val expectedChange = DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(editorAci.toByteString())
.newMembers(listOf(newMember))
.build()
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log8),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(log8.group).isNotNull()
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
AppliedGroupChangeLog(
log8.group!!,
expectedChange
)
)
assertNewState(
expectedUpdatedGroupState = log8.group,
expectedRemainingLogs = emptyList(),
updatedGroupState = advanceGroupStateResult.updatedGroupState,
remainingLogs = advanceGroupStateResult.remainingRemoteGroupChanges
)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
}
@Test
fun notices_changes_in_avatar_and_title_but_not_members_in_placeholder() {
val newMemberAci = ServiceId.ACI.from(UUID.randomUUID())
val newMember = DecryptedMember.Builder()
.aciBytes(newMemberAci.toByteString())
.build()
val existingMember = DecryptedMember.Builder()
.aciBytes(ServiceId.ACI.from(UUID.randomUUID()).toByteString())
.build()
val currentState = DecryptedGroup.Builder()
.revision(GroupStatePatcher.PLACEHOLDER_REVISION)
.title("Incorrect group title")
.avatar("Incorrect group avatar")
.members(listOf(newMember))
.build()
val log8 = DecryptedGroupChangeLog(
DecryptedGroup.Builder()
.revision(8)
.members(listOf(existingMember).plus(newMember))
.title("Group Revision " + 8)
.avatar("Group Avatar " + 8)
.build(),
DecryptedGroupChange.Builder()
.revision(8)
.editorServiceIdBytes(newMemberAci.toByteString())
.newMembers(listOf(newMember))
.build()
)
val expectedChange = DecryptedGroupChange.Builder()
.revision(8)
.newTitle(DecryptedString.Builder().value_("Group Revision " + 8).build())
.newAvatar(DecryptedString.Builder().value_("Group Avatar " + 8).build())
.build()
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log8),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
GroupStatePatcher.LATEST
)
assertThat(log8.group).isNotNull()
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
AppliedGroupChangeLog(
log8.group!!,
expectedChange
)
)
assertNewState(log8.group, emptyList(), advanceGroupStateResult.updatedGroupState, advanceGroupStateResult.remainingRemoteGroupChanges)
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log8.group)
}
@Test
fun no_actual_change() {
val currentState = state(0)
val log1 = serverLogEntry(1)
val log2 = DecryptedGroupChangeLog(
log1.group!!.newBuilder()
.revision(2)
.build(),
DecryptedGroupChange.Builder()
.revision(2)
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
.newTitle(DecryptedString.Builder().value_(log1.group!!.title).build())
.build()
)
val advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(
/* inputState = */
GroupStateDiff(
previousGroupState = currentState,
serverHistory = listOf(log1, log2),
groupSendEndorsementsResponse = null
),
/* maximumRevisionToApply = */
2
)
assertThat(advanceGroupStateResult.processedLogEntries).containsOnly(
asLocal(log1),
AppliedGroupChangeLog(
log2.group!!,
DecryptedGroupChange.Builder()
.revision(2)
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
.build()
)
)
assertThat(advanceGroupStateResult.remainingRemoteGroupChanges).isEmpty()
assertThat(advanceGroupStateResult.updatedGroupState).isEqualTo(log2.group)
}
companion object {
private val KNOWN_EDITOR = UUID.randomUUID()
private fun assertNewState(
expectedUpdatedGroupState: DecryptedGroup?,
expectedRemainingLogs: List<DecryptedGroupChangeLog>,
updatedGroupState: DecryptedGroup?,
remainingLogs: List<DecryptedGroupChangeLog>
) {
assertThat(updatedGroupState).isEqualTo(expectedUpdatedGroupState)
assertThat(remainingLogs).isEqualTo(expectedRemainingLogs)
}
private fun serverLogEntry(revision: Int): DecryptedGroupChangeLog {
return DecryptedGroupChangeLog(state(revision), change(revision))
}
private fun localLogEntryNoEditor(revision: Int): AppliedGroupChangeLog {
return AppliedGroupChangeLog(state(revision), changeNoEditor(revision))
}
private fun serverLogEntryWholeStateOnly(revision: Int): DecryptedGroupChangeLog {
return DecryptedGroupChangeLog(state(revision), null)
}
private fun logEntryMissingState(revision: Int): DecryptedGroupChangeLog {
return DecryptedGroupChangeLog(null, change(revision))
}
private fun state(revision: Int): DecryptedGroup {
return DecryptedGroup.Builder()
.revision(revision)
.title("Group Revision $revision")
.build()
}
private fun change(revision: Int): DecryptedGroupChange {
return DecryptedGroupChange.Builder()
.revision(revision)
.editorServiceIdBytes(UuidUtil.toByteString(KNOWN_EDITOR))
.newTitle(DecryptedString.Builder().value_("Group Revision $revision").build())
.build()
}
private fun changeNoEditor(revision: Int): DecryptedGroupChange {
return DecryptedGroupChange.Builder()
.revision(revision)
.newTitle(DecryptedString.Builder().value_("Group Revision $revision").build())
.build()
}
private fun asLocal(logEntry: DecryptedGroupChangeLog): AppliedGroupChangeLog {
assertThat(logEntry.group).isNotNull()
return AppliedGroupChangeLog(logEntry.group!!, logEntry.change)
}
}
}

View file

@ -1,133 +0,0 @@
package org.thoughtcrime.securesms.groups.v2.processing;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public final class StateChainTest {
private static final int BAD_DELTA = 256;
private final StateChain<Character, Integer> stateChain = new StateChain<>(
(c, d) -> {
if (d == BAD_DELTA) return null;
return (char) (c + d);
},
(a, b) -> a - b,
(a, b)->a==b);
@Test
public void push_one_state_pair() {
stateChain.push('A', 0);
assertThat(stateChain.getList(), is(singletonList(pair('A', 0))));
}
@Test
public void push_two_state_pairs() {
stateChain.push('A', 0);
stateChain.push('B', 1);
assertThat(stateChain.getList(), is(asList(pair('A', 0),
pair('B', 1))));
}
@Test
public void push_two_state_pairs_null_first_delta() {
stateChain.push('A', null);
stateChain.push('B', 1);
assertThat(stateChain.getList(), is(asList(pair('A', null),
pair('B', 1))));
}
@Test
public void push_two_state_pairs_with_missing_delta() {
stateChain.push('A', 0);
stateChain.push('B', null);
assertThat(stateChain.getList(), is(asList(pair('A', 0),
pair('B', 1))));
}
@Test
public void push_two_state_pairs_with_missing_state() {
stateChain.push('A', 0);
stateChain.push(null, 1);
assertThat(stateChain.getList(), is(asList(pair('A', 0),
pair('B', 1))));
}
@Test
public void push_one_state_pairs_with_missing_state_and_delta() {
stateChain.push(null, null);
assertThat(stateChain.getList(), is(emptyList()));
}
@Test
public void push_two_state_pairs_with_missing_state_and_delta() {
stateChain.push('A', 0);
stateChain.push(null, null);
assertThat(stateChain.getList(), is(singletonList(pair('A', 0))));
}
@Test
public void push_two_state_pairs_that_do_not_match() {
stateChain.push('D', 0);
stateChain.push('E', 2);
assertThat(stateChain.getList(), is(asList(pair('D', 0),
pair('F', 2),
pair('E', -1))));
}
@Test
public void push_one_state_pair_null_delta() {
stateChain.push('A', null);
assertThat(stateChain.getList(), is(singletonList(pair('A', null))));
}
@Test
public void push_two_state_pairs_with_no_diff() {
stateChain.push('Z', null);
stateChain.push('Z', 0);
assertThat(stateChain.getList(), is(singletonList(pair('Z', null))));
}
@Test
public void push_one_state_pair_null_state() {
stateChain.push(null, 1);
assertThat(stateChain.getList(), is(emptyList()));
}
@Test
public void bad_delta_results_in_reconstruction() {
stateChain.push('C', 0);
stateChain.push('F', BAD_DELTA);
assertThat(stateChain.getList(), is(asList(pair('C', 0),
pair('F', 3))));
}
@Test
public void bad_delta_and_no_state_results_in_change_ignore() {
stateChain.push('C', 0);
stateChain.push(null, BAD_DELTA);
assertThat(stateChain.getList(), is(singletonList(pair('C', 0))));
}
private static StateChain.Pair<Character, Integer> pair(char c, Integer i) {
return new StateChain.Pair<>(c, i);
}
}

View file

@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.groups.v2.processing
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.isEmpty
import org.junit.Test
class StateChainTest {
private val stateChain = StateChain<Char, Int>(
{ state, delta ->
if (delta == BAD_DELTA) return@StateChain null
(state.code + delta).toChar()
},
{ state1, state2 -> state1.code - state2.code },
{ state1, state2 -> state1 == state2 }
)
@Test
fun push_one_state_pair() {
stateChain.push('A', 0)
assertThat(stateChain.list).containsExactly(stateChainPair('A', 0))
}
@Test
fun push_two_state_pairs() {
stateChain.push('A', 0)
stateChain.push('B', 1)
assertThat(stateChain.list)
.containsExactly(stateChainPair('A', 0), stateChainPair('B', 1))
}
@Test
fun push_two_state_pairs_null_first_delta() {
stateChain.push('A', null)
stateChain.push('B', 1)
assertThat(stateChain.list)
.containsExactly(stateChainPair('A', null), stateChainPair('B', 1))
}
@Test
fun push_two_state_pairs_with_missing_delta() {
stateChain.push('A', 0)
stateChain.push('B', null)
assertThat(stateChain.list)
.containsExactly(stateChainPair('A', 0), stateChainPair('B', 1))
}
@Test
fun push_two_state_pairs_with_missing_state() {
stateChain.push('A', 0)
stateChain.push(null, 1)
assertThat(stateChain.list)
.containsExactly(stateChainPair('A', 0), stateChainPair('B', 1))
}
@Test
fun push_one_state_pairs_with_missing_state_and_delta() {
stateChain.push(null, null)
assertThat(stateChain.list).isEmpty()
}
@Test
fun push_two_state_pairs_with_missing_state_and_delta() {
stateChain.push('A', 0)
stateChain.push(null, null)
assertThat(stateChain.list).containsExactly(stateChainPair('A', 0))
}
@Test
fun push_two_state_pairs_that_do_not_match() {
stateChain.push('D', 0)
stateChain.push('E', 2)
assertThat(stateChain.list).containsExactly(
stateChainPair('D', 0),
stateChainPair('F', 2),
stateChainPair('E', -1)
)
}
@Test
fun push_one_state_pair_null_delta() {
stateChain.push('A', null)
assertThat(stateChain.list).containsExactly(stateChainPair('A', null))
}
@Test
fun push_two_state_pairs_with_no_diff() {
stateChain.push('Z', null)
stateChain.push('Z', 0)
assertThat(stateChain.list).containsExactly(stateChainPair('Z', null))
}
@Test
fun push_one_state_pair_null_state() {
stateChain.push(null, 1)
assertThat(stateChain.list).isEmpty()
}
@Test
fun bad_delta_results_in_reconstruction() {
stateChain.push('C', 0)
stateChain.push('F', BAD_DELTA)
assertThat(stateChain.list).containsExactly(stateChainPair('C', 0), stateChainPair('F', 3))
}
@Test
fun bad_delta_and_no_state_results_in_change_ignore() {
stateChain.push('C', 0)
stateChain.push(null, BAD_DELTA)
assertThat(stateChain.list).containsExactly(stateChainPair('C', 0))
}
companion object {
private const val BAD_DELTA = 256
private fun <A, B> stateChainPair(first: A & Any, second: B): StateChain.Pair<A & Any, B> {
return StateChain.Pair<A & Any, B>(first, second)
}
}
}

View file

@ -1,84 +0,0 @@
package org.thoughtcrime.securesms.l10n;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
public final class SupportArticleTest {
private static final File MAIN_STRINGS = new File("src/main/res/values/strings.xml");
private static final Pattern SUPPORT_ARTICLE = Pattern.compile(".*:\\/\\/support.signal.org\\/.*articles\\/.*");
private static final Pattern CORRECT_SUPPORT_ARTICLE = Pattern.compile("https:\\/\\/support.signal.org\\/hc\\/articles\\/\\d+(#[a-z_]+)?");
/**
* Tests that support articles found in strings.xml:
* <p>
* - Do not have a locale mentioned in the url.
* - Only have an article number, i.e. no trailing text.
* - Are https.
* - Are marked as translatable="false".
*/
@Test
public void ensure_format_and_translatable_state_of_all_support_article_urls() throws Exception {
assertTrue(MAIN_STRINGS.exists());
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
List<String> errors = new LinkedList<>();
int seen = 0;
try (InputStream fileStream = new FileInputStream(MAIN_STRINGS)) {
Document doc = builder.parse(fileStream);
NodeList strings = doc.getElementsByTagName("string");
for (int i = 0; i < strings.getLength(); i++) {
Node stringNode = strings.item(i);
String string = stringNode.getTextContent();
String stringName = stringName(stringNode);
if (SUPPORT_ARTICLE.matcher(string).matches()) {
seen++;
if (!CORRECT_SUPPORT_ARTICLE.matcher(string).matches()) {
errors.add(String.format("Article url format is not correct [%s] url: %s", stringName, string));
}
if (isTranslatable(stringNode)) {
errors.add(String.format("Article string is translatable [%s], add translatable=\"false\"", stringName));
}
}
}
}
assertThat(seen, greaterThan(0));
assertThat(errors, is(Collections.emptyList()));
}
private static boolean isTranslatable(Node item) {
if (item.hasAttributes()) {
Node translatableAttribute = item.getAttributes().getNamedItem("translatable");
return translatableAttribute == null || !"false".equals(translatableAttribute.getTextContent());
}
return true;
}
private static String stringName(Node item) {
return item.getAttributes().getNamedItem("name").getTextContent();
}
}

View file

@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.l10n
import assertk.assertThat
import assertk.assertions.isEmpty
import assertk.assertions.isGreaterThan
import org.junit.Test
import org.w3c.dom.Node
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.io.path.Path
import kotlin.io.path.inputStream
class SupportArticleTest {
/**
* Tests that support articles found in strings.xml:
* <p>
* - Do not have a locale mentioned in the URL.
* - Only have an article number, i.e. no trailing text.
* - Are https.
* - Are marked as translatable="false".
*/
@Test
fun ensure_format_and_translatable_state_of_all_support_article_urls() {
val errors = mutableListOf<String>()
var seen = 0
val strings = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(MAIN_STRINGS_PATH.inputStream())
.getElementsByTagName("string")
for (i in 0 until strings.length) {
val stringNode = strings.item(i)
val string = stringNode.textContent
val stringName = stringName(stringNode)
if (SUPPORT_ARTICLE.matches(string)) {
seen++
if (!CORRECT_SUPPORT_ARTICLE.matches(string)) {
errors.add("Article URL format is not correct [$stringName] URL: $string")
}
if (isTranslatable(stringNode)) {
errors.add("Article string is translatable [$stringName], add translatable=\"false\"")
}
}
}
assertThat(seen).isGreaterThan(0)
assertThat(errors).isEmpty()
}
private fun isTranslatable(item: Node): Boolean {
val translatableAttribute = item.attributes.getNamedItem("translatable")
return translatableAttribute == null || translatableAttribute.textContent != "false"
}
private fun stringName(item: Node): String {
return item.attributes.getNamedItem("name").textContent
}
companion object {
private val MAIN_STRINGS_PATH = Path("src/main/res/values/strings.xml")
private val SUPPORT_ARTICLE = Regex(".*://support.signal.org/.*articles/.*")
private val CORRECT_SUPPORT_ARTICLE = Regex("https://support.signal.org/hc/articles/\\d+(#[a-z_]+)?")
}
}

View file

@ -1,107 +0,0 @@
package org.thoughtcrime.securesms.recipients;
import org.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;
import org.junit.Test;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public final class RecipientIdSerializationTest {
@Test
public void toSerializedList_empty() {
assertEquals("", RecipientId.toSerializedList(emptyList()));
}
@Test
public void toSerializedList_one_item() {
assertEquals("123", RecipientId.toSerializedList(singletonList(RecipientId.from(123))));
}
@Test
public void toSerializedList_two_items() {
assertEquals("123,987", RecipientId.toSerializedList(asList(RecipientId.from(123), RecipientId.from("987"))));
}
@Test
public void fromSerializedList_empty() {
assertThat(RecipientId.fromSerializedList(""), is(emptyList()));
}
@Test
public void fromSerializedList_one_item() {
assertThat(RecipientId.fromSerializedList("123"), is(singletonList(RecipientId.from(123))));
}
@Test
public void fromSerializedList_two_items() {
assertThat(RecipientId.fromSerializedList("123,456"), is(asList(RecipientId.from(123), RecipientId.from(456))));
}
@Test
public void fromSerializedList_recipient_serialize() {
List<RecipientId> recipientIds = RecipientId.fromSerializedList(RecipientId.from(123).serialize());
assertThat(recipientIds, hasSize(1));
assertThat(recipientIds, contains(RecipientId.from(123)));
}
@Test
public void serializedListContains_empty_list_does_not_contain_item() {
assertFalse(RecipientId.serializedListContains("", RecipientId.from(456)));
}
@Test
public void serializedListContains_single_list_does_not_contain_item() {
assertFalse(RecipientId.serializedListContains("123", RecipientId.from(456)));
}
@Test
public void serializedListContains_single_list_does_contain_item() {
assertTrue(RecipientId.serializedListContains("456", RecipientId.from(456)));
}
@Test
public void serializedListContains_double_list_does_contain_item_in_first_position() {
assertTrue(RecipientId.serializedListContains("456,123", RecipientId.from(456)));
}
@Test
public void serializedListContains_double_list_does_contain_item_in_second_position() {
assertTrue(RecipientId.serializedListContains("123,456", RecipientId.from(456)));
}
@Test
public void serializedListContains_single_list_does_not_contain_item_due_to_extra_digit_at_start() {
assertFalse(RecipientId.serializedListContains("1456", RecipientId.from(456)));
}
@Test
public void serializedListContains_single_list_does_not_contain_item_due_to_extra_digit_at_end() {
assertFalse(RecipientId.serializedListContains("4561", RecipientId.from(456)));
}
@Test
public void serializedListContains_find_all_items_in_triple_list() {
assertTrue(RecipientId.serializedListContains("11,12,13", RecipientId.from(11)));
assertTrue(RecipientId.serializedListContains("11,12,13", RecipientId.from(12)));
assertTrue(RecipientId.serializedListContains("11,12,13", RecipientId.from(13)));
}
@Test
public void serializedListContains_cant_find_similar_items_in_triple_list() {
assertFalse(RecipientId.serializedListContains("11,12,13", RecipientId.from(1)));
assertFalse(RecipientId.serializedListContains("11,12,13", RecipientId.from(2)));
assertFalse(RecipientId.serializedListContains("11,12,13", RecipientId.from(3)));
}
}

View file

@ -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()
}
}

View file

@ -1,253 +0,0 @@
package org.thoughtcrime.securesms.service.webrtc.collections;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class ParticipantCollectionTest {
private final ParticipantCollection testSubject = new ParticipantCollection(3);
@Test
public void givenAnEmptyCollection_whenIAdd3Participants_thenIExpectThemToBeOrderedByAddedToCallTime() {
// GIVEN
List<CallParticipant> input = Arrays.asList(participant(1, 1, 4), participant(2, 1, 2), participant(3, 1, 3));
// WHEN
ParticipantCollection result = testSubject.getNext(input);
// THEN
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(3), id(1)));
}
@Test
public void givenAnEmptyCollection_whenIAdd3Participants_thenIExpectNoListParticipants() {
// GIVEN
List<CallParticipant> input = Arrays.asList(participant(1, 1, 4), participant(2, 1, 2), participant(3, 1, 3));
// WHEN
ParticipantCollection result = testSubject.getNext(input);
// THEN
assertEquals(result.getListParticipants().size(), 0);
}
@Test
public void givenAnEmptyColletion_whenIAdd4Participants_thenIExpectThemToBeOrderedByLastSpokenThenAddedToCallTime() {
// GIVEN
List<CallParticipant> input = Arrays.asList(participant(1, 1, 2),
participant(2, 5, 2),
participant(3, 1, 1),
participant(4, 1, 0));
// WHEN
ParticipantCollection result = testSubject.getNext(input);
// THEN
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(4), id(3)));
}
@Test
public void givenACollection_whenIUpdateWithEmptyList_thenIExpectEmptyList() {
// GIVEN
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
ParticipantCollection initialCollection = testSubject.getNext(initial);
List<CallParticipant> next = Collections.emptyList();
// WHEN
ParticipantCollection result = initialCollection.getNext(next);
// THEN
assertEquals(0, result.getGridParticipants().size());
}
@Test
public void givenACollection_whenIUpdateWithLatestSpeakerAndSpeakerIsAlreadyInGridSection_thenIExpectTheSameGridSectionOrder() {
// GIVEN
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
ParticipantCollection initialCollection = testSubject.getNext(initial);
List<CallParticipant> next = Arrays.asList(participant(1, 1, 2), participant(2, 2, 3), participant(3, 1, 4));
// WHEN
ParticipantCollection result = initialCollection.getNext(next);
// THEN
assertThat(result.getGridParticipants(), Matchers.contains(id(1), id(2), id(3)));
}
@Test
public void givenACollection_whenSomeoneLeaves_thenIDoNotExpectToSeeThemInTheNewList() {
// GIVEN
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
ParticipantCollection initialCollection = testSubject.getNext(initial);
List<CallParticipant> next = Arrays.asList(participant(2, 2, 3), participant(3, 1, 4));
// WHEN
ParticipantCollection result = initialCollection.getNext(next);
// THEN
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(3)));
}
@Test
public void givenACollection_whenMultipleLeave_thenIDoNotExpectToSeeThemInTheNewList() {
// GIVEN
ParticipantCollection testSubject = new ParticipantCollection(4);
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4), participant(4, 1, 5));
ParticipantCollection initialCollection = testSubject.getNext(initial);
List<CallParticipant> next = Arrays.asList(participant(3, 1, 4), participant(2, 1, 3));
// WHEN
ParticipantCollection result = initialCollection.getNext(next);
// THEN
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(3)));
}
@Test
public void bigTest() {
// Welcome to the Thunder dome. 10 people enter...
ParticipantCollection testSubject = new ParticipantCollection(6);
List<CallParticipant> init = Arrays.asList(participant(1, 1, 1), // Alice
participant(2, 1, 1), // Bob
participant(3, 1, 1), // Charlie
participant(4, 1, 1), // Diane
participant(5, 1, 1), // Ethel
participant(6, 1, 1), // Francis
participant(7, 1, 1), // Georgina
participant(8, 1, 1), // Henry
participant(9, 1, 1), // Ignace
participant(10, 1, 1)); // Jericho
ParticipantCollection initialCollection = testSubject.getNext(init);
assertThat(initialCollection.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(6)));
assertThat(initialCollection.getListParticipants(), Matchers.contains(id(7), id(8), id(9), id(10)));
// Bob speaks about his trip to antigua...
List<CallParticipant> bobSpoke = Arrays.asList(participant(1, 1, 1),
participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 1, 1),
participant(9, 1, 1),
participant(10, 1, 1));
ParticipantCollection afterBobSpoke = initialCollection.getNext(bobSpoke);
assertThat(afterBobSpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(6)));
assertThat(afterBobSpoke.getListParticipants(), Matchers.contains(id(7), id(8), id(9), id(10)));
// Henry interjects and says now is not the time, this is the thunderdome.
List<CallParticipant> henrySpoke = Arrays.asList(participant(1, 1, 1),
participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 3, 1),
participant(9, 1, 1),
participant(10, 1, 1));
ParticipantCollection afterHenrySpoke = afterBobSpoke.getNext(henrySpoke);
assertThat(afterHenrySpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(8)));
assertThat(afterHenrySpoke.getListParticipants(), Matchers.contains(id(6), id(7), id(9), id(10)));
// Ignace asks how everone's holidays were
List<CallParticipant> ignaceSpoke = Arrays.asList(participant(1, 1, 1),
participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 3, 1),
participant(9, 4, 1),
participant(10, 1, 1));
ParticipantCollection afterIgnaceSpoke = afterHenrySpoke.getNext(ignaceSpoke);
assertThat(afterIgnaceSpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(9), id(8)));
assertThat(afterIgnaceSpoke.getListParticipants(), Matchers.contains(id(5), id(6), id(7), id(10)));
// Alice is the first to fall
List<CallParticipant> aliceLeft = Arrays.asList(participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 3, 1),
participant(9, 4, 1),
participant(10, 1, 1));
ParticipantCollection afterAliceLeft = afterIgnaceSpoke.getNext(aliceLeft);
assertThat(afterAliceLeft.getGridParticipants(), Matchers.contains(id(5), id(2), id(3), id(4), id(9), id(8)));
assertThat(afterAliceLeft.getListParticipants(), Matchers.contains(id(6), id(7), id(10)));
// Just kidding, Alice is back. Georgina and Charlie gasp!
List<CallParticipant> mixUp = Arrays.asList(participant(1, 1, 5),
participant(2, 2, 1),
participant(3, 6, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 5, 1),
participant(8, 3, 1),
participant(9, 4, 1),
participant(10, 1, 1));
ParticipantCollection afterMixUp = afterAliceLeft.getNext(mixUp);
assertThat(afterMixUp.getGridParticipants(), Matchers.contains(id(7), id(2), id(3), id(4), id(9), id(8)));
assertThat(afterMixUp.getListParticipants(), Matchers.contains(id(5), id(6), id(10), id(1)));
}
private Matcher<CallParticipant> id(long serializedId) {
return Matchers.hasProperty("callParticipantId", Matchers.equalTo(new CallParticipantId(serializedId, RecipientId.from(serializedId))));
}
private static CallParticipant participant(long serializedId,long lastSpoke, long added) {
return CallParticipant.createRemote(
new CallParticipantId(serializedId, RecipientId.from(serializedId)),
Recipient.UNKNOWN,
null,
new BroadcastVideoSink(),
false,
false,
false,
CallParticipant.HAND_LOWERED,
lastSpoke,
false,
added,
false,
CallParticipant.DeviceOrdinal.PRIMARY);
}
}

View file

@ -0,0 +1,301 @@
package org.thoughtcrime.securesms.service.webrtc.collections
import assertk.Assert
import assertk.assertThat
import assertk.assertions.each
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import org.junit.Test
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.events.CallParticipant.Companion.createRemote
import org.thoughtcrime.securesms.events.CallParticipantId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class ParticipantCollectionTest {
private val testSubject = ParticipantCollection(3)
@Test
fun givenAnEmptyCollection_whenIAdd3Participants_thenIExpectThemToBeOrderedByAddedToCallTime() {
// GIVEN
val input = listOf(
participant(1, 1, 4),
participant(2, 1, 2),
participant(3, 1, 3)
)
// WHEN
val result = testSubject.getNext(input)
// THEN
assertThat(result.gridParticipants).containsParticipantIds(2, 3, 1)
}
@Test
fun givenAnEmptyCollection_whenIAdd3Participants_thenIExpectNoListParticipants() {
// GIVEN
val input = listOf(
participant(1, 1, 4),
participant(2, 1, 2),
participant(3, 1, 3)
)
// WHEN
val result = testSubject.getNext(input)
// THEN
assertThat(result.listParticipants).isEmpty()
}
@Test
fun givenAnEmptyColletion_whenIAdd4Participants_thenIExpectThemToBeOrderedByLastSpokenThenAddedToCallTime() {
// GIVEN
val input = listOf(
participant(1, 1, 2),
participant(2, 5, 2),
participant(3, 1, 1),
participant(4, 1, 0)
)
// WHEN
val result = testSubject.getNext(input)
// THEN
assertThat(result.gridParticipants).containsParticipantIds(2, 4, 3)
}
@Test
fun givenACollection_whenIUpdateWithEmptyList_thenIExpectEmptyList() {
// GIVEN
val initial = listOf(
participant(1, 1, 2),
participant(2, 1, 3),
participant(3, 1, 4)
)
val initialCollection = testSubject.getNext(initial)
val next = emptyList<CallParticipant>()
// WHEN
val result = initialCollection.getNext(next)
// THEN
assertThat(result.gridParticipants).isEmpty()
}
@Test
fun givenACollection_whenIUpdateWithLatestSpeakerAndSpeakerIsAlreadyInGridSection_thenIExpectTheSameGridSectionOrder() {
// GIVEN
val initial = listOf(
participant(1, 1, 2),
participant(2, 1, 3),
participant(3, 1, 4)
)
val initialCollection = testSubject.getNext(initial)
val next = listOf(
participant(1, 1, 2),
participant(2, 2, 3),
participant(3, 1, 4)
)
// WHEN
val result = initialCollection.getNext(next)
// THEN
assertThat(result.gridParticipants).containsParticipantIds(1, 2, 3)
}
@Test
fun givenACollection_whenSomeoneLeaves_thenIDoNotExpectToSeeThemInTheNewList() {
// GIVEN
val initial = listOf(
participant(1, 1, 2),
participant(2, 1, 3),
participant(3, 1, 4)
)
val initialCollection = testSubject.getNext(initial)
val next = listOf(
participant(2, 2, 3),
participant(3, 1, 4)
)
// WHEN
val result = initialCollection.getNext(next)
// THEN
assertThat(result.gridParticipants).containsParticipantIds(2, 3)
}
@Test
fun givenACollection_whenMultipleLeave_thenIDoNotExpectToSeeThemInTheNewList() {
// GIVEN
val testSubject = ParticipantCollection(4)
val initial = listOf(
participant(1, 1, 2),
participant(2, 1, 3),
participant(3, 1, 4),
participant(4, 1, 5)
)
val initialCollection = testSubject.getNext(initial)
val next = listOf(
participant(3, 1, 4),
participant(2, 1, 3)
)
// WHEN
val result = initialCollection.getNext(next)
// THEN
assertThat(result.gridParticipants).containsParticipantIds(2, 3)
}
@Test
fun bigTest() {
// Welcome to the Thunder dome. 10 people enter...
val testSubject = ParticipantCollection(6)
val init = listOf(
participant(1, 1, 1), // Alice
participant(2, 1, 1), // Bob
participant(3, 1, 1), // Charlie
participant(4, 1, 1), // Diane
participant(5, 1, 1), // Ethel
participant(6, 1, 1), // Francis
participant(7, 1, 1), // Georgina
participant(8, 1, 1), // Henry
participant(9, 1, 1), // Ignace
participant(10, 1, 1) // Jericho
)
val initialCollection = testSubject.getNext(init)
assertThat(initialCollection.gridParticipants).containsParticipantIds(1, 2, 3, 4, 5, 6)
assertThat(initialCollection.listParticipants).containsParticipantIds(7, 8, 9, 10)
// Bob speaks about his trip to antigua...
val bobSpoke = listOf(
participant(1, 1, 1),
participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 1, 1),
participant(9, 1, 1),
participant(10, 1, 1)
)
val afterBobSpoke = initialCollection.getNext(bobSpoke)
assertThat(afterBobSpoke.gridParticipants).containsParticipantIds(1, 2, 3, 4, 5, 6)
assertThat(afterBobSpoke.listParticipants).containsParticipantIds(7, 8, 9, 10)
// Henry interjects and says now is not the time, this is the thunderdome.
val henrySpoke = listOf(
participant(1, 1, 1),
participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 3, 1),
participant(9, 1, 1),
participant(10, 1, 1)
)
val afterHenrySpoke = afterBobSpoke.getNext(henrySpoke)
assertThat(afterHenrySpoke.gridParticipants).containsParticipantIds(1, 2, 3, 4, 5, 8)
assertThat(afterHenrySpoke.listParticipants).containsParticipantIds(6, 7, 9, 10)
// Ignace asks how everyone's holidays were
val ignaceSpoke = listOf(
participant(1, 1, 1),
participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 3, 1),
participant(9, 4, 1),
participant(10, 1, 1)
)
val afterIgnaceSpoke = afterHenrySpoke.getNext(ignaceSpoke)
assertThat(afterIgnaceSpoke.gridParticipants).containsParticipantIds(1, 2, 3, 4, 9, 8)
assertThat(afterIgnaceSpoke.listParticipants).containsParticipantIds(5, 6, 7, 10)
// Alice is the first to fall
val aliceLeft = listOf(
participant(2, 2, 1),
participant(3, 1, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 1, 1),
participant(8, 3, 1),
participant(9, 4, 1),
participant(10, 1, 1)
)
val afterAliceLeft = afterIgnaceSpoke.getNext(aliceLeft)
assertThat(afterAliceLeft.gridParticipants).containsParticipantIds(5, 2, 3, 4, 9, 8)
assertThat(afterAliceLeft.listParticipants).containsParticipantIds(6, 7, 10)
// Just kidding, Alice is back. Georgina and Charlie gasp!
val mixUp = listOf(
participant(1, 1, 5),
participant(2, 2, 1),
participant(3, 6, 1),
participant(4, 1, 1),
participant(5, 1, 1),
participant(6, 1, 1),
participant(7, 5, 1),
participant(8, 3, 1),
participant(9, 4, 1),
participant(10, 1, 1)
)
val afterMixUp = afterAliceLeft.getNext(mixUp)
assertThat(afterMixUp.gridParticipants).containsParticipantIds(7, 2, 3, 4, 9, 8)
assertThat(afterMixUp.listParticipants).containsParticipantIds(5, 6, 10, 1)
}
companion object {
private fun Assert<List<CallParticipant>>.containsParticipantIds(vararg expectedParticipantIds: Long) {
transform("Same sizes") { it.size }.isEqualTo(expectedParticipantIds.size)
transform { it.zip(expectedParticipantIds.asList()) }
.each { assertionPair ->
assertionPair.transform { (actualCallParticipant, expectedParticipantId) ->
assertk.assertThat(actualCallParticipant.callParticipantId)
.isEqualTo(CallParticipantId(expectedParticipantId, RecipientId.from(expectedParticipantId)))
}
}
}
private fun participant(serializedId: Long, lastSpoke: Long, added: Long): CallParticipant {
return createRemote(
callParticipantId = CallParticipantId(serializedId, RecipientId.from(serializedId)),
recipient = Recipient.UNKNOWN,
identityKey = null,
renderer = BroadcastVideoSink(),
isForwardingVideo = false,
audioEnabled = false,
videoEnabled = false,
handRaisedTimestamp = CallParticipant.HAND_LOWERED,
lastSpoke = lastSpoke,
mediaKeysReceived = false,
addedToCallTime = added,
isScreenSharing = false,
deviceOrdinal = CallParticipant.DeviceOrdinal.PRIMARY
)
}
}
}

View file

@ -1,8 +1,5 @@
package org.thoughtcrime.securesms.testutil;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.signal.core.util.logging.Log;
import java.util.ArrayList;
@ -93,46 +90,4 @@ public final class LogRecorder extends Log.Logger {
return throwable;
}
}
@SafeVarargs
public static <T> Matcher<T> hasMessages(T... messages) {
return new BaseMatcher<T>() {
@Override
public void describeTo(Description description) {
description.appendValueList("[", ", ", "]", messages);
}
@Override
public void describeMismatch(Object item, Description description) {
@SuppressWarnings("unchecked")
List<Entry> list = (List<Entry>) item;
ArrayList<String> messages = new ArrayList<>(list.size());
for (Entry e : list) {
messages.add(e.message);
}
description.appendText("was ").appendValueList("[", ", ", "]", messages);
}
@Override
public boolean matches(Object item) {
@SuppressWarnings("unchecked")
List<Entry> list = (List<Entry>) item;
if (list.size() != messages.length) {
return false;
}
for (int i = 0; i < messages.length; i++) {
if (!list.get(i).message.equals(messages[i])) {
return false;
}
}
return true;
}
};
}
}

View file

@ -1,118 +0,0 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.List;
import java.util.UUID;
import okio.ByteString;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static java.util.Arrays.asList;
public final class DecryptedGroupUtilTest {
@Test
public void can_extract_editor_uuid_from_decrypted_group_change() {
ACI aci = ACI.from(UUID.randomUUID());
ByteString editor = aci.toByteString();
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
.editorServiceIdBytes(editor)
.build();
ServiceId parsed = DecryptedGroupUtil.editorServiceId(groupChange).get();
assertEquals(aci, parsed);
}
@Test
public void can_extract_uuid_from_decrypted_pending_member() {
ACI aci = ACI.from(UUID.randomUUID());
DecryptedPendingMember decryptedMember = new DecryptedPendingMember.Builder()
.serviceIdBytes(aci.toByteString())
.build();
ServiceId parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes);
assertEquals(aci, parsed);
}
@Test
public void can_extract_uuid_from_bad_decrypted_pending_member() {
DecryptedPendingMember decryptedMember = new DecryptedPendingMember.Builder()
.serviceIdBytes(ByteString.of(Util.getSecretBytes(18)))
.build();
ServiceId parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes);
assertNull(parsed);
}
@Test
public void can_extract_uuids_for_all_pending_including_bad_entries() {
ACI aci1 = ACI.from(UUID.randomUUID());
ACI aci2 = ACI.from(UUID.randomUUID());
DecryptedPendingMember decryptedMember1 = new DecryptedPendingMember.Builder()
.serviceIdBytes(aci1.toByteString())
.build();
DecryptedPendingMember decryptedMember2 = new DecryptedPendingMember.Builder()
.serviceIdBytes(aci2.toByteString())
.build();
DecryptedPendingMember decryptedMember3 = new DecryptedPendingMember.Builder()
.serviceIdBytes(ByteString.of(Util.getSecretBytes(18)))
.build();
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
.newPendingMembers(asList(decryptedMember1, decryptedMember2, decryptedMember3))
.build();
List<ServiceId> pendingUuids = DecryptedGroupUtil.pendingToServiceIdList(groupChange.newPendingMembers);
assertThat(pendingUuids, is(asList(aci1, aci2, ACI.UNKNOWN)));
}
@Test
public void can_extract_uuids_for_all_deleted_pending_excluding_bad_entries() {
ACI aci1 = ACI.from(UUID.randomUUID());
ACI aci2 = ACI.from(UUID.randomUUID());
DecryptedPendingMemberRemoval decryptedMember1 = new DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(aci1.toByteString())
.build();
DecryptedPendingMemberRemoval decryptedMember2 = new DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(aci2.toByteString())
.build();
DecryptedPendingMemberRemoval decryptedMember3 = new DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(ByteString.of(Util.getSecretBytes(18)))
.build();
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
.deletePendingMembers(asList(decryptedMember1, decryptedMember2, decryptedMember3))
.build();
List<ServiceId> removedUuids = DecryptedGroupUtil.removedPendingMembersServiceIdList(groupChange);
assertThat(removedUuids, is(asList(aci1, aci2)));
}
@Test
public void can_extract_uuids_for_all_deleted_members_excluding_bad_entries() {
ACI aci1 = ACI.from(UUID.randomUUID());
ACI aci2 = ACI.from(UUID.randomUUID());
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
.deleteMembers(asList(aci1.toByteString(), aci2.toByteString(), ByteString.of(Util.getSecretBytes(18))))
.build();
List<ServiceId> removedServiceIds = DecryptedGroupUtil.removedMembersServiceIdList(groupChange);
assertThat(removedServiceIds, is(asList(aci1, aci2)));
}
}

View file

@ -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())
}

View file

@ -1,139 +0,0 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Before;
import org.junit.Test;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import okio.ByteString;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember;
public final class GroupsV2Operations_ban_Test {
private GroupsV2Operations.GroupOperations groupOperations;
@Before
public void setup() throws InvalidInputException {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS();
TestZkGroupServer server = new TestZkGroupServer();
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
groupOperations = new GroupsV2Operations(clientZkOperations, 10).forGroup(groupSecretParams);
}
@Test
public void addBanToEmptyList() {
ACI ban = ACI.from(UUID.randomUUID());
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(Collections.singleton(ban),
false,
Collections.emptyList());
assertThat(banUuidsChange.addBannedMembers.size(), is(1));
assertThat(banUuidsChange.addBannedMembers.get(0).added.userId, is(groupOperations.encryptServiceId(ban)));
}
@Test
public void addBanToPartialFullList() {
ACI toBan = ACI.from(UUID.randomUUID());
List<DecryptedBannedMember> alreadyBanned = new ArrayList<>(5);
for (int i = 0; i < 5; i++) {
alreadyBanned.add(bannedMember(UUID.randomUUID()));
}
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(Collections.singleton(toBan),
false,
alreadyBanned);
assertThat(banUuidsChange.addBannedMembers.size(), is(1));
assertThat(banUuidsChange.addBannedMembers.get(0).added.userId, is(groupOperations.encryptServiceId(toBan)));
}
@Test
public void addBanToFullList() {
ACI toBan = ACI.from(UUID.randomUUID());
List<DecryptedBannedMember> alreadyBanned = new ArrayList<>(10);
DecryptedBannedMember oldest = null;
for (int i = 0; i < 10; i++) {
DecryptedBannedMember member = bannedMember(UUID.randomUUID()).newBuilder().timestamp(100 + i).build();
if (oldest == null) {
oldest = member;
}
alreadyBanned.add(member);
}
Collections.shuffle(alreadyBanned);
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(Collections.singleton(toBan),
false,
alreadyBanned);
assertThat(banUuidsChange.deleteBannedMembers.size(), is(1));
assertThat(banUuidsChange.deleteBannedMembers.get(0).deletedUserId, is(groupOperations.encryptServiceId(ServiceId.parseOrThrow(oldest.serviceIdBytes))));
assertThat(banUuidsChange.addBannedMembers.size(), is(1));
assertThat(banUuidsChange.addBannedMembers.get(0).added.userId, is(groupOperations.encryptServiceId(toBan)));
}
@Test
public void addMultipleBanToFullList() {
List<ACI> toBan = new ArrayList<>();
toBan.add(ACI.from(UUID.randomUUID()));
toBan.add(ACI.from(UUID.randomUUID()));
List<DecryptedBannedMember> alreadyBanned = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
alreadyBanned.add(bannedMember(UUID.randomUUID()).newBuilder().timestamp(100 + i).build());
}
List<ByteString> oldest = new ArrayList<>(2);
oldest.add(groupOperations.encryptServiceId(ServiceId.parseOrThrow(alreadyBanned.get(0).serviceIdBytes)));
oldest.add(groupOperations.encryptServiceId(ServiceId.parseOrThrow(alreadyBanned.get(1).serviceIdBytes)));
Collections.shuffle(alreadyBanned);
GroupChange.Actions.Builder banUuidsChange = groupOperations.createBanServiceIdsChange(new HashSet<>(toBan),
false,
alreadyBanned);
assertThat(banUuidsChange.deleteBannedMembers.size(), is(2));
assertThat(banUuidsChange.deleteBannedMembers
.stream()
.map(a -> a.deletedUserId)
.collect(Collectors.toList()),
hasItems(oldest.get(0), oldest.get(1)));
assertThat(banUuidsChange.addBannedMembers.size(), is(2));
assertThat(banUuidsChange.addBannedMembers
.stream()
.map(a -> a.added)
.map(b -> b.userId)
.collect(Collectors.toList()),
hasItems(groupOperations.encryptServiceId(toBan.get(0)),
groupOperations.encryptServiceId(toBan.get(1))));
}
}

View file

@ -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())
}