mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Groups V2 protobufs and local conflict resolution.
This commit is contained in:
committed by
Greyson Parrelli
parent
f449a45912
commit
0269a3eb6f
@@ -1,6 +1,9 @@
|
||||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.whispersystems.libsignal.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -46,4 +49,31 @@ public final class UuidUtilTest {
|
||||
|
||||
assertEquals("b83dfb0b-67f1-41aa-992e-030c167cd011", uuid.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void byte_array_compatibility_with_zk_group_uuid_util() {
|
||||
UUID uuid = UUID.fromString("67dfd496-ea02-4720-b13d-83a462168b1d");
|
||||
|
||||
UUID result = UUIDUtil.deserialize(UuidUtil.toByteArray(uuid));
|
||||
|
||||
assertEquals(uuid, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void byte_string_compatibility_with_zk_group_uuid_util() {
|
||||
UUID uuid = UUID.fromString("67dfd496-ea02-4720-b13d-83a462168b1d");
|
||||
|
||||
UUID result = UuidUtil.fromByteString(ByteString.copyFrom(UUIDUtil.serialize(uuid)));
|
||||
|
||||
assertEquals(uuid, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void byte_string_round_trip() {
|
||||
UUID uuid = UUID.fromString("67dfd496-ea02-4720-b13d-83a462168b1d");
|
||||
|
||||
UUID result = UuidUtil.fromByteString(ByteString.copyFrom(UuidUtil.toByteArray(uuid)));
|
||||
|
||||
assertEquals(uuid, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class GroupChangeUtilTest {
|
||||
|
||||
/**
|
||||
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
|
||||
* <p>
|
||||
* If we didn't, newly added fields would easily affect {@link GroupChangeUtil}'s ability to detect empty change states and resolve conflicts.
|
||||
*/
|
||||
@Test
|
||||
public void ensure_GroupChangeUtil_knows_about_all_fields_of_GroupChange_Actions() {
|
||||
int maxFieldFound = Stream.of(GroupChange.Actions.class.getFields())
|
||||
.filter(f -> f.getType() == int.class)
|
||||
.mapToInt(f -> {
|
||||
try {
|
||||
return (int) f.get(null);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
})
|
||||
.max()
|
||||
.orElse(0);
|
||||
|
||||
assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(),
|
||||
GroupChangeUtil.CHANGE_ACTION_MAX_FIELD, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void empty_change_set() {
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(GroupChange.Actions.newBuilder().build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_add_member_field_3() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_delete_member_field_4() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_member_roles_field_5() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_profile_keys_field_6() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_add_pending_members_field_7() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_delete_pending_members_field_8() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_promote_delete_pending_members_field_9() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_title_field_10() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_avatar_field_11() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_disappearing_message_timer_field_12() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_attributes_field_13() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_member_access_field_14() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder().getDefaultInstanceForType())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.DisappearingMessagesTimer;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.PendingMember;
|
||||
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.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class GroupChangeUtil_resolveConflict_Test {
|
||||
|
||||
@Test
|
||||
public void empty_actions() {
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(DecryptedGroup.newBuilder().build(),
|
||||
DecryptedGroupChange.newBuilder().build(),
|
||||
GroupChange.Actions.newBuilder().build())
|
||||
.build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_3__changes_to_add_existing_members_are_excluded() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1))
|
||||
.addMembers(member(member3))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addNewMembers(member(member1))
|
||||
.addNewMembers(member(member2))
|
||||
.addNewMembers(member(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().setAdded(encryptedMember(member1, randomProfileKey())))
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().setAdded(encryptedMember(member2, profileKey2)))
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().setAdded(encryptedMember(member3, randomProfileKey())))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().setAdded(encryptedMember(member2, profileKey2)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_4__changes_to_remove_missing_members_are_excluded() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member2))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addDeleteMembers(UuidUtil.toByteString(member1))
|
||||
.addDeleteMembers(UuidUtil.toByteString(member2))
|
||||
.addDeleteMembers(UuidUtil.toByteString(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().setDeletedUserId(encrypt(member1)))
|
||||
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
|
||||
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().setDeletedUserId(encrypt(member3)))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_5__role_change_is_preserved() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(admin(member1))
|
||||
.addMembers(member(member2))
|
||||
.addMembers(member(member3))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addModifyMemberRoles(demoteAdmin(member1))
|
||||
.addModifyMemberRoles(promoteAdmin(member2))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(encrypt(member1)).setRole(Member.Role.DEFAULT))
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(encrypt(member2)).setRole(Member.Role.ADMINISTRATOR))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertEquals(change, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_5__unnecessary_role_changes_removed() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
UUID memberNotInGroup = UUID.randomUUID();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(admin(member1))
|
||||
.addMembers(member(member2))
|
||||
.addMembers(member(member3))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addModifyMemberRoles(promoteAdmin(member1))
|
||||
.addModifyMemberRoles(promoteAdmin(member2))
|
||||
.addModifyMemberRoles(demoteAdmin(member3))
|
||||
.addModifyMemberRoles(promoteAdmin(memberNotInGroup))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(encrypt(member1)).setRole(Member.Role.ADMINISTRATOR))
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(encrypt(member2)).setRole(Member.Role.ADMINISTRATOR))
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(encrypt(member3)).setRole(Member.Role.DEFAULT))
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(encrypt(memberNotInGroup)).setRole(Member.Role.ADMINISTRATOR))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().setUserId(encrypt(member2)).setRole(Member.Role.ADMINISTRATOR))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_6__profile_key_changes() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
UUID memberNotInGroup = UUID.randomUUID();
|
||||
ProfileKey profileKey1 = randomProfileKey();
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
ProfileKey profileKey3 = randomProfileKey();
|
||||
ProfileKey profileKey4 = randomProfileKey();
|
||||
ProfileKey profileKey2b = randomProfileKey();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1, profileKey1))
|
||||
.addMembers(member(member2, profileKey2))
|
||||
.addMembers(member(member3, profileKey3))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addModifiedProfileKeys(member(member1, profileKey1))
|
||||
.addModifiedProfileKeys(member(member2, profileKey2b))
|
||||
.addModifiedProfileKeys(member(member3, profileKey3))
|
||||
.addModifiedProfileKeys(member(memberNotInGroup, profileKey4))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().setPresentation(presentation(member1, profileKey1)))
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().setPresentation(presentation(member2, profileKey2b)))
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().setPresentation(presentation(member3, profileKey3)))
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().setPresentation(presentation(memberNotInGroup, profileKey4)))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().setPresentation(presentation(member2, profileKey2b)))
|
||||
.build();
|
||||
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_7__add_pending_members() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1))
|
||||
.addPendingMembers(pendingMember(member3))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addNewPendingMembers(pendingMember(member1))
|
||||
.addNewPendingMembers(pendingMember(member2))
|
||||
.addNewPendingMembers(pendingMember(member3))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder().setAdded(PendingMember.newBuilder().setMember(encryptedMember(member1, randomProfileKey()))))
|
||||
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder().setAdded(PendingMember.newBuilder().setMember(encryptedMember(member2, profileKey2))))
|
||||
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder().setAdded(PendingMember.newBuilder().setMember(encryptedMember(member3, randomProfileKey()))))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder().setAdded(PendingMember.newBuilder().setMember(encryptedMember(member2, profileKey2))))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_8__delete_pending_members() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1))
|
||||
.addPendingMembers(pendingMember(member2))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addDeletePendingMembers(pendingMemberRemoval(member1))
|
||||
.addDeletePendingMembers(pendingMemberRemoval(member2))
|
||||
.addDeletePendingMembers(pendingMemberRemoval(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().setDeletedUserId(encrypt(member1)))
|
||||
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
|
||||
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().setDeletedUserId(encrypt(member3)))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_9__promote_pending_members() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
UUID member2 = UUID.randomUUID();
|
||||
UUID member3 = UUID.randomUUID();
|
||||
ProfileKey profileKey2 = randomProfileKey();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addMembers(member(member1))
|
||||
.addPendingMembers(pendingMember(member2))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addPromotePendingMembers(UuidUtil.toByteString(member1))
|
||||
.addPromotePendingMembers(UuidUtil.toByteString(member2))
|
||||
.addPromotePendingMembers(UuidUtil.toByteString(member3))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member1, randomProfileKey())))
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member2, profileKey2)))
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member3, randomProfileKey())))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member2, profileKey2)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_3_to_9__add_of_pending_member_converted_to_a_promote() {
|
||||
UUID member1 = UUID.randomUUID();
|
||||
ProfileKey profileKey1 = randomProfileKey();
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.addPendingMembers(pendingMember(member1))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.addNewMembers(member(member1))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().setAdded(encryptedMember(member1, profileKey1)))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
|
||||
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member1, profileKey1)))
|
||||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_10__title_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setTitle("Existing title")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewTitle(DecryptedString.newBuilder().setValue("New title").build())
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder().setTitle(ByteString.copyFrom("New title encrypted".getBytes())))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertEquals(change, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_10__no_title_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setTitle("Existing title")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewTitle(DecryptedString.newBuilder().setValue("Existing title").build())
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder().setTitle(ByteString.copyFrom("Existing title encrypted".getBytes())))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_11__avatar_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setAvatar("Existing avatar")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewAvatar(DecryptedString.newBuilder().setValue("New avatar").build())
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar("New avatar possibly encrypted"))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertEquals(change, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_11__no_avatar_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setAvatar("Existing avatar")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewAvatar(DecryptedString.newBuilder().setValue("Existing avatar").build())
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar("Existing avatar possibly encrypted"))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_12__timer_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDisappearingMessagesTimer(DisappearingMessagesTimer.newBuilder().setDuration(123))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewTimer(DisappearingMessagesTimer.newBuilder().setDuration(456))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder().setTimer(ByteString.EMPTY))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertEquals(change, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_12__no_timer_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDisappearingMessagesTimer(DisappearingMessagesTimer.newBuilder().setDuration(123))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewTimer(DisappearingMessagesTimer.newBuilder().setDuration(123))
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder().setTimer(ByteString.EMPTY))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_13__attribute_access_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setAccessControl(AccessControl.newBuilder().setAttributes(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER)
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder().setAttributesAccess(AccessControl.AccessRequired.MEMBER))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertEquals(change, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_13__no_attribute_access_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setAccessControl(AccessControl.newBuilder().setAttributes(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewAttributeAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder().setAttributesAccess(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_14__membership_access_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setAccessControl(AccessControl.newBuilder().setMembers(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewMemberAccess(AccessControl.AccessRequired.MEMBER)
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder().setMembersAccess(AccessControl.AccessRequired.MEMBER))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertEquals(change, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_14__no_membership_access_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setAccessControl(AccessControl.newBuilder().setMembers(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder().setMembersAccess(AccessControl.AccessRequired.ADMINISTRATOR))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
|
||||
private static ProfileKey randomProfileKey() {
|
||||
byte[] contents = new byte[32];
|
||||
new SecureRandom().nextBytes(contents);
|
||||
try {
|
||||
return new ProfileKey(contents);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates encryption by creating a unique {@link ByteString} that won't equal a byte string created from the {@link UUID}.
|
||||
*/
|
||||
private static ByteString encrypt(UUID uuid) {
|
||||
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
|
||||
return ByteString.copyFrom(Arrays.copyOf(uuidBytes, uuidBytes.length + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates a presentation by concatenating the uuid and profile key which makes it suitable for
|
||||
* equality assertions in these tests.
|
||||
*/
|
||||
private static ByteString presentation(UUID uuid, ProfileKey profileKey) {
|
||||
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
|
||||
byte[] profileKeyBytes = profileKey.serialize();
|
||||
byte[] concat = new byte[uuidBytes.length + profileKeyBytes.length];
|
||||
|
||||
System.arraycopy(uuidBytes, 0, concat, 0, uuidBytes.length);
|
||||
System.arraycopy(profileKeyBytes, 0, concat, uuidBytes.length, profileKeyBytes.length);
|
||||
|
||||
return ByteString.copyFrom(concat);
|
||||
}
|
||||
|
||||
private static DecryptedModifyMemberRole promoteAdmin(UUID member) {
|
||||
return DecryptedModifyMemberRole.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member))
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedModifyMemberRole demoteAdmin(UUID member) {
|
||||
return DecryptedModifyMemberRole.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(member))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Member encryptedMember(UUID uuid, ProfileKey profileKey) {
|
||||
return Member.newBuilder()
|
||||
.setPresentation(presentation(uuid, profileKey))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedMember member(UUID uuid) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedPendingMemberRemoval pendingMemberRemoval(UUID uuid) {
|
||||
return DecryptedPendingMemberRemoval.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedPendingMember pendingMember(UUID uuid) {
|
||||
return DecryptedPendingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedMember member(UUID uuid, ProfileKey profileKey) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DecryptedMember admin(UUID uuid) {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(uuid))
|
||||
.setRole(Member.Role.ADMINISTRATOR)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user