From 26868ae668ef2c6e43bc7040bff845aebb4d08a0 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Mon, 3 Aug 2020 16:12:02 -0300 Subject: [PATCH] Get authoritative profile keys from group changes only. --- .../securesms/groups/v2/ProfileKeySet.java | 77 +++++--- .../v2/processing/GroupsV2StateProcessor.java | 12 +- .../GroupsV2UpdateMessageProducerTest.java | 104 +--------- .../securesms/groups/v2/ChangeBuilder.java | 140 ++++++++++++++ .../groups/v2/ProfileKeySetTest.java | 182 ++++++++++++++++++ .../securesms/testutil/LogRecorder.java | 45 +++++ 6 files changed, 425 insertions(+), 135 deletions(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/groups/v2/ChangeBuilder.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySet.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySet.java index e5536484a8..a0381f5588 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySet.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySet.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; @@ -30,38 +31,62 @@ public final class ProfileKeySet { private final Map authoritativeProfileKeys = new LinkedHashMap<>(); /** - * Add new profile keys from the group state. + * Add new profile keys from a group change. + *

+ * If the change came from the member whose profile key is changing then it is regarded as + * authoritative. */ - public void addKeysFromGroupState(@NonNull DecryptedGroup group, - @Nullable UUID changeSource) - { + public void addKeysFromGroupChange(@NonNull DecryptedGroupChange change) { + UUID editor = UuidUtil.fromByteStringOrNull(change.getEditor()); + + for (DecryptedMember member : change.getNewMembersList()) { + addMemberKey(member, editor); + } + + for (DecryptedMember member : change.getPromotePendingMembersList()) { + addMemberKey(member, editor); + } + + for (DecryptedMember member : change.getModifiedProfileKeysList()) { + addMemberKey(member, editor); + } + } + + /** + * Add new profile keys from the group state. + *

+ * Profile keys found in group state are never authoritative as the change cannot be easily + * attributed to a member and it's possible that the group is out of date. So profile keys + * gathered from a group state can only be used to fill in gaps in knowledge. + */ + public void addKeysFromGroupState(@NonNull DecryptedGroup group) { for (DecryptedMember member : group.getMembersList()) { - UUID memberUuid = UuidUtil.fromByteString(member.getUuid()); + addMemberKey(member, null); + } + } - if (UuidUtil.UNKNOWN_UUID.equals(memberUuid)) { - Log.w(TAG, "Seen unknown member UUID"); - continue; - } + private void addMemberKey(@NonNull DecryptedMember member, @Nullable UUID changeSource) { + UUID memberUuid = UuidUtil.fromByteString(member.getUuid()); - ProfileKey profileKey; - try { - profileKey = new ProfileKey(member.getProfileKey().toByteArray()); - } catch (InvalidInputException e) { - Log.w(TAG, "Bad profile key in group"); - continue; - } + if (UuidUtil.UNKNOWN_UUID.equals(memberUuid)) { + Log.w(TAG, "Seen unknown member UUID"); + return; + } - if (changeSource != null) { - Log.d(TAG, String.format("Change %s by %s", memberUuid, changeSource)); + ProfileKey profileKey; + try { + profileKey = new ProfileKey(member.getProfileKey().toByteArray()); + } catch (InvalidInputException e) { + Log.w(TAG, "Bad profile key in group"); + return; + } - if (changeSource.equals(memberUuid)) { - authoritativeProfileKeys.put(memberUuid, profileKey); - profileKeys.remove(memberUuid); - } else { - if (!authoritativeProfileKeys.containsKey(memberUuid)) { - profileKeys.put(memberUuid, profileKey); - } - } + if (memberUuid.equals(changeSource)) { + authoritativeProfileKeys.put(memberUuid, profileKey); + profileKeys.remove(memberUuid); + } else { + if (!authoritativeProfileKeys.containsKey(memberUuid)) { + profileKeys.put(memberUuid, profileKey); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index bb20b00a13..16781b9e01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -7,8 +7,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import com.google.protobuf.ByteString; - import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; @@ -16,8 +14,6 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; -import org.signal.zkgroup.util.UUIDUtil; -import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -320,9 +316,11 @@ public final class GroupsV2StateProcessor { final ProfileKeySet profileKeys = new ProfileKeySet(); for (ServerGroupLogEntry entry : globalGroupState.getServerHistory()) { - Optional editor = DecryptedGroupUtil.editorUuid(entry.getChange()); - if (editor.isPresent() && entry.getGroup() != null) { - profileKeys.addKeysFromGroupState(entry.getGroup(), editor.get()); + if (entry.getGroup() != null) { + profileKeys.addKeysFromGroupState(entry.getGroup()); + } + if (entry.getChange() != null) { + profileKeys.addKeysFromGroupChange(entry.getChange()); } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java index 4136951624..f6fd9d18bc 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java @@ -18,17 +18,11 @@ import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.signal.storageservice.protos.groups.AccessControl; -import org.signal.storageservice.protos.groups.Member; 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.DecryptedModifyMemberRole; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; -import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval; -import org.signal.storageservice.protos.groups.local.DecryptedString; -import org.signal.storageservice.protos.groups.local.DecryptedTimer; import org.thoughtcrime.securesms.testutil.MainThreadUtil; -import org.thoughtcrime.securesms.util.StringUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -44,6 +38,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; 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.util.StringUtil.isolateBidi; @RunWith(RobolectricTestRunner.class) @@ -932,102 +928,6 @@ public final class GroupsV2UpdateMessageProducerTest { } } - private static class ChangeBuilder { - - private final DecryptedGroupChange.Builder builder; - - ChangeBuilder(@NonNull UUID editor) { - builder = DecryptedGroupChange.newBuilder() - .setEditor(UuidUtil.toByteString(editor)); - } - - ChangeBuilder() { - builder = DecryptedGroupChange.newBuilder(); - } - - ChangeBuilder addMember(@NonNull UUID newMember) { - builder.addNewMembers(DecryptedMember.newBuilder() - .setUuid(UuidUtil.toByteString(newMember))); - return this; - } - - ChangeBuilder deleteMember(@NonNull UUID removedMember) { - builder.addDeleteMembers(UuidUtil.toByteString(removedMember)); - return this; - } - - ChangeBuilder promoteToAdmin(@NonNull UUID member) { - builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder() - .setRole(Member.Role.ADMINISTRATOR) - .setUuid(UuidUtil.toByteString(member))); - return this; - } - - ChangeBuilder demoteToMember(@NonNull UUID member) { - builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder() - .setRole(Member.Role.DEFAULT) - .setUuid(UuidUtil.toByteString(member))); - return this; - } - - ChangeBuilder invite(@NonNull UUID potentialMember) { - builder.addNewPendingMembers(DecryptedPendingMember.newBuilder() - .setUuid(UuidUtil.toByteString(potentialMember))); - return this; - } - - ChangeBuilder uninvite(@NonNull UUID pendingMember) { - builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder() - .setUuid(UuidUtil.toByteString(pendingMember))); - return this; - } - - ChangeBuilder promote(@NonNull UUID pendingMember) { - builder.addPromotePendingMembers(DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(pendingMember))); - return this; - } - - ChangeBuilder title(@NonNull String newTitle) { - builder.setNewTitle(DecryptedString.newBuilder() - .setValue(newTitle)); - return this; - } - - ChangeBuilder avatar(@NonNull String newAvatar) { - builder.setNewAvatar(DecryptedString.newBuilder() - .setValue(newAvatar)); - return this; - } - - ChangeBuilder timer(int duration) { - builder.setNewTimer(DecryptedTimer.newBuilder() - .setDuration(duration)); - return this; - } - - ChangeBuilder attributeAccess(@NonNull AccessControl.AccessRequired accessRequired) { - builder.setNewAttributeAccess(accessRequired); - return this; - } - - ChangeBuilder membershipAccess(@NonNull AccessControl.AccessRequired accessRequired) { - builder.setNewMemberAccess(accessRequired); - return this; - } - - DecryptedGroupChange build() { - return builder.build(); - } - } - - private static ChangeBuilder changeBy(@NonNull UUID groupEditor) { - return new ChangeBuilder(groupEditor); - } - - private static ChangeBuilder changeByUnknown() { - return new ChangeBuilder(); - } - private static @NonNull GroupsV2UpdateMessageProducer.DescribeMemberStrategy createDescriber(@NonNull Map map) { return uuid -> { String name = map.get(uuid); diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ChangeBuilder.java b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ChangeBuilder.java new file mode 100644 index 0000000000..06c9cc776b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ChangeBuilder.java @@ -0,0 +1,140 @@ +package org.thoughtcrime.securesms.groups.v2; + +import androidx.annotation.NonNull; + +import com.google.protobuf.ByteString; + +import org.signal.storageservice.protos.groups.AccessControl; +import org.signal.storageservice.protos.groups.Member; +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedMember; +import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole; +import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; +import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval; +import org.signal.storageservice.protos.groups.local.DecryptedString; +import org.signal.storageservice.protos.groups.local.DecryptedTimer; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.UUID; + +public final class ChangeBuilder { + + private final DecryptedGroupChange.Builder builder; + + public static ChangeBuilder changeBy(@NonNull UUID editor) { + return new ChangeBuilder(editor); + } + + public static ChangeBuilder changeByUnknown() { + return new ChangeBuilder(); + } + + ChangeBuilder(@NonNull UUID editor) { + builder = DecryptedGroupChange.newBuilder() + .setEditor(UuidUtil.toByteString(editor)); + } + + ChangeBuilder() { + builder = DecryptedGroupChange.newBuilder(); + } + + public ChangeBuilder addMember(@NonNull UUID newMember) { + builder.addNewMembers(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(newMember))); + return this; + } + + public ChangeBuilder addMember(@NonNull UUID newMember, @NonNull ProfileKey profileKey) { + builder.addNewMembers(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(newMember)) + .setProfileKey(ByteString.copyFrom(profileKey.serialize()))); + return this; + } + + public ChangeBuilder deleteMember(@NonNull UUID removedMember) { + builder.addDeleteMembers(UuidUtil.toByteString(removedMember)); + return this; + } + + public ChangeBuilder promoteToAdmin(@NonNull UUID member) { + builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder() + .setRole(Member.Role.ADMINISTRATOR) + .setUuid(UuidUtil.toByteString(member))); + return this; + } + + public ChangeBuilder demoteToMember(@NonNull UUID member) { + builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder() + .setRole(Member.Role.DEFAULT) + .setUuid(UuidUtil.toByteString(member))); + return this; + } + + public ChangeBuilder invite(@NonNull UUID potentialMember) { + builder.addNewPendingMembers(DecryptedPendingMember.newBuilder() + .setUuid(UuidUtil.toByteString(potentialMember))); + return this; + } + + public ChangeBuilder uninvite(@NonNull UUID pendingMember) { + builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder() + .setUuid(UuidUtil.toByteString(pendingMember))); + return this; + } + + public ChangeBuilder promote(@NonNull UUID pendingMember) { + builder.addPromotePendingMembers(DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(pendingMember))); + return this; + } + + public ChangeBuilder profileKeyUpdate(@NonNull UUID member, @NonNull ProfileKey profileKey) { + return profileKeyUpdate(member, profileKey.serialize()); + } + + public ChangeBuilder profileKeyUpdate(@NonNull UUID member, @NonNull byte[] profileKey) { + builder.addModifiedProfileKeys(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(member)) + .setProfileKey(ByteString.copyFrom(profileKey))); + return this; + } + + public ChangeBuilder promote(@NonNull UUID pendingMember, @NonNull ProfileKey profileKey) { + builder.addPromotePendingMembers(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(pendingMember)) + .setProfileKey(ByteString.copyFrom(profileKey.serialize()))); + return this; + } + + public ChangeBuilder title(@NonNull String newTitle) { + builder.setNewTitle(DecryptedString.newBuilder() + .setValue(newTitle)); + return this; + } + + public ChangeBuilder avatar(@NonNull String newAvatar) { + builder.setNewAvatar(DecryptedString.newBuilder() + .setValue(newAvatar)); + return this; + } + + public ChangeBuilder timer(int duration) { + builder.setNewTimer(DecryptedTimer.newBuilder() + .setDuration(duration)); + return this; + } + + public ChangeBuilder attributeAccess(@NonNull AccessControl.AccessRequired accessRequired) { + builder.setNewAttributeAccess(accessRequired); + return this; + } + + public ChangeBuilder membershipAccess(@NonNull AccessControl.AccessRequired accessRequired) { + builder.setNewMemberAccess(accessRequired); + return this; + } + + public DecryptedGroupChange build() { + return builder.build(); + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.java new file mode 100644 index 0000000000..3481b999ed --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/ProfileKeySetTest.java @@ -0,0 +1,182 @@ +package org.thoughtcrime.securesms.groups.v2; + +import org.junit.Test; +import org.signal.zkgroup.profiles.ProfileKey; +import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.testutil.LogRecorder; + +import java.util.Collection; +import java.util.UUID; + +import edu.emory.mathcs.backport.java.util.Collections; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +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() { + UUID editor = 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() { + UUID editor = UUID.randomUUID(); + UUID newMember = 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() { + UUID newMember = 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() { + UUID newMember = 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() { + UUID editor = UUID.randomUUID(); + UUID newMember = 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() { + UUID newMember = 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() { + UUID member = 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() { + UUID editor = UUID.randomUUID(); + UUID member = 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() { + UUID editor = UUID.randomUUID(); + UUID member = 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() { + UUID editor = UUID.randomUUID(); + UUID member = 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() { + UUID editor = UUID.randomUUID(); + UUID member = 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(); + UUID editor = UUID.randomUUID(); + UUID member = 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")); + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java index 895836875f..9345dc4248 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java @@ -1,5 +1,8 @@ package org.thoughtcrime.securesms.testutil; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.thoughtcrime.securesms.logging.Log; import java.util.ArrayList; @@ -95,4 +98,46 @@ public final class LogRecorder extends Log.Logger { return throwable; } } + + @SafeVarargs + public static Matcher hasMessages(T... messages) { + return new BaseMatcher() { + + @Override + public void describeTo(Description description) { + description.appendValueList("[", ", ", "]", messages); + } + + @Override + public void describeMismatch(Object item, Description description) { + @SuppressWarnings("unchecked") + List list = (List) item; + ArrayList messages = new ArrayList<>(list.size()); + + for (Entry e : list) { + messages.add(e.message); + } + + description.appendText("was ").appendValueList("[", ", ", "]", messages); + } + + @Override + public boolean matches(Object item) { + @SuppressWarnings("unchecked") + List list = (List) item; + + if (list.size() != messages.length) { + return false; + } + + for (int i = 0; i < messages.length; i++) { + if (!list.get(i).message.equals(messages[i])) { + return false; + } + } + + return true; + } + }; + } }