Groups V2 state comparison and gap handling.
This commit is contained in:
parent
c9d2cef58d
commit
9ac9ace6b8
17 changed files with 990 additions and 113 deletions
|
@ -9,17 +9,17 @@ import java.util.Collection;
|
||||||
*/
|
*/
|
||||||
final class AdvanceGroupStateResult {
|
final class AdvanceGroupStateResult {
|
||||||
|
|
||||||
@NonNull private final Collection<GroupLogEntry> processedLogEntries;
|
@NonNull private final Collection<LocalGroupLogEntry> processedLogEntries;
|
||||||
@NonNull private final GlobalGroupState newGlobalGroupState;
|
@NonNull private final GlobalGroupState newGlobalGroupState;
|
||||||
|
|
||||||
AdvanceGroupStateResult(@NonNull Collection<GroupLogEntry> processedLogEntries,
|
AdvanceGroupStateResult(@NonNull Collection<LocalGroupLogEntry> processedLogEntries,
|
||||||
@NonNull GlobalGroupState newGlobalGroupState)
|
@NonNull GlobalGroupState newGlobalGroupState)
|
||||||
{
|
{
|
||||||
this.processedLogEntries = processedLogEntries;
|
this.processedLogEntries = processedLogEntries;
|
||||||
this.newGlobalGroupState = newGlobalGroupState;
|
this.newGlobalGroupState = newGlobalGroupState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull Collection<GroupLogEntry> getProcessedLogEntries() {
|
@NonNull Collection<LocalGroupLogEntry> getProcessedLogEntries() {
|
||||||
return processedLogEntries;
|
return processedLogEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,30 +14,41 @@ import java.util.List;
|
||||||
final class GlobalGroupState {
|
final class GlobalGroupState {
|
||||||
|
|
||||||
@Nullable private final DecryptedGroup localState;
|
@Nullable private final DecryptedGroup localState;
|
||||||
@NonNull private final List<GroupLogEntry> history;
|
@NonNull private final List<ServerGroupLogEntry> serverHistory;
|
||||||
|
|
||||||
GlobalGroupState(@Nullable DecryptedGroup localState,
|
GlobalGroupState(@Nullable DecryptedGroup localState,
|
||||||
@NonNull List<GroupLogEntry> serverStates)
|
@NonNull List<ServerGroupLogEntry> serverHistory)
|
||||||
{
|
{
|
||||||
this.localState = localState;
|
this.localState = localState;
|
||||||
this.history = serverStates;
|
this.serverHistory = serverHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable DecryptedGroup getLocalState() {
|
@Nullable DecryptedGroup getLocalState() {
|
||||||
return localState;
|
return localState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull Collection<GroupLogEntry> getHistory() {
|
@NonNull Collection<ServerGroupLogEntry> getServerHistory() {
|
||||||
return history;
|
return serverHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getEarliestRevisionNumber() {
|
||||||
|
if (localState != null) {
|
||||||
|
return localState.getRevision();
|
||||||
|
} else {
|
||||||
|
if (serverHistory.isEmpty()) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
return serverHistory.get(0).getRevision();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int getLatestRevisionNumber() {
|
int getLatestRevisionNumber() {
|
||||||
if (history.isEmpty()) {
|
if (serverHistory.isEmpty()) {
|
||||||
if (localState == null) {
|
if (localState == null) {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
return localState.getRevision();
|
return localState.getRevision();
|
||||||
}
|
}
|
||||||
return history.get(history.size() - 1).getGroup().getRevision();
|
return serverHistory.get(serverHistory.size() - 1).getRevision();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,24 @@ package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
final class GroupStateMapper {
|
final class GroupStateMapper {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(GroupStateMapper.class);
|
||||||
|
|
||||||
static final int LATEST = Integer.MAX_VALUE;
|
static final int LATEST = Integer.MAX_VALUE;
|
||||||
|
|
||||||
private static final Comparator<GroupLogEntry> BY_REVISION = (o1, o2) -> Integer.compare(o1.getGroup().getRevision(), o2.getGroup().getRevision());
|
private static final Comparator<ServerGroupLogEntry> BY_REVISION = (o1, o2) -> Integer.compare(o1.getRevision(), o2.getRevision());
|
||||||
|
|
||||||
private GroupStateMapper() {
|
private GroupStateMapper() {
|
||||||
}
|
}
|
||||||
|
@ -27,37 +35,88 @@ final class GroupStateMapper {
|
||||||
static @NonNull AdvanceGroupStateResult partiallyAdvanceGroupState(@NonNull GlobalGroupState inputState,
|
static @NonNull AdvanceGroupStateResult partiallyAdvanceGroupState(@NonNull GlobalGroupState inputState,
|
||||||
int maximumRevisionToApply)
|
int maximumRevisionToApply)
|
||||||
{
|
{
|
||||||
final ArrayList<GroupLogEntry> statesToApplyNow = new ArrayList<>(inputState.getHistory().size());
|
ArrayList<LocalGroupLogEntry> appliedChanges = new ArrayList<>(inputState.getServerHistory().size());
|
||||||
final ArrayList<GroupLogEntry> statesToApplyLater = new ArrayList<>(inputState.getHistory().size());
|
HashMap<Integer, ServerGroupLogEntry> statesToApplyNow = new HashMap<>(inputState.getServerHistory().size());
|
||||||
final DecryptedGroup newLocalState;
|
ArrayList<ServerGroupLogEntry> statesToApplyLater = new ArrayList<>(inputState.getServerHistory().size());
|
||||||
final GlobalGroupState newGlobalGroupState;
|
DecryptedGroup current = inputState.getLocalState();
|
||||||
|
|
||||||
for (GroupLogEntry entry : inputState.getHistory()) {
|
if (inputState.getServerHistory().isEmpty()) {
|
||||||
if (inputState.getLocalState() != null &&
|
return new AdvanceGroupStateResult(Collections.emptyList(), new GlobalGroupState(current, Collections.emptyList()));
|
||||||
inputState.getLocalState().getRevision() >= entry.getGroup().getRevision())
|
}
|
||||||
{
|
|
||||||
|
for (ServerGroupLogEntry entry : inputState.getServerHistory()) {
|
||||||
|
if (entry.getRevision() > maximumRevisionToApply) {
|
||||||
|
statesToApplyLater.add(entry);
|
||||||
|
} else {
|
||||||
|
statesToApplyNow.put(entry.getRevision(), entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(statesToApplyLater, BY_REVISION);
|
||||||
|
|
||||||
|
final int from = inputState.getEarliestRevisionNumber();
|
||||||
|
final int to = Math.min(inputState.getLatestRevisionNumber(), maximumRevisionToApply);
|
||||||
|
|
||||||
|
for (int revision = from; revision >= 0 && revision <= to; revision++) {
|
||||||
|
ServerGroupLogEntry entry = statesToApplyNow.get(revision);
|
||||||
|
if (entry == null) {
|
||||||
|
Log.w(TAG, "Could not find group log on server V" + revision);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.getGroup().getRevision() > maximumRevisionToApply) {
|
DecryptedGroup groupAtRevision = entry.getGroup();
|
||||||
statesToApplyLater.add(entry);
|
DecryptedGroupChange changeAtRevision = entry.getChange();
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
Log.w(TAG, "No local state, accepting server state for V" + revision);
|
||||||
|
current = groupAtRevision;
|
||||||
|
if (groupAtRevision != null) {
|
||||||
|
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, changeAtRevision));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.getRevision() + 1 != revision) {
|
||||||
|
Log.w(TAG, "Detected gap V" + revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeAtRevision == null) {
|
||||||
|
Log.w(TAG, "Reconstructing change for V" + revision);
|
||||||
|
changeAtRevision = GroupChangeReconstruct.reconstructGroupChange(current, Objects.requireNonNull(groupAtRevision));
|
||||||
|
}
|
||||||
|
|
||||||
|
DecryptedGroup groupWithChangeApplied;
|
||||||
|
try {
|
||||||
|
groupWithChangeApplied = DecryptedGroupUtil.applyWithoutRevisionCheck(current, changeAtRevision);
|
||||||
|
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
|
||||||
|
Log.w(TAG, "Unable to apply V" + revision, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupAtRevision == null) {
|
||||||
|
Log.w(TAG, "Reconstructing state for V" + revision);
|
||||||
|
groupAtRevision = groupWithChangeApplied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.getRevision() != groupAtRevision.getRevision()) {
|
||||||
|
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, changeAtRevision));
|
||||||
} else {
|
} else {
|
||||||
statesToApplyNow.add(entry);
|
DecryptedGroupChange sameRevisionDelta = GroupChangeReconstruct.reconstructGroupChange(current, groupAtRevision);
|
||||||
|
if (!DecryptedGroupUtil.changeIsEmpty(sameRevisionDelta)) {
|
||||||
|
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, sameRevisionDelta));
|
||||||
|
Log.w(TAG, "Inserted repair change for mismatch V" + revision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Collections.sort(statesToApplyNow, BY_REVISION);
|
DecryptedGroupChange missingChanges = GroupChangeReconstruct.reconstructGroupChange(groupWithChangeApplied, groupAtRevision);
|
||||||
Collections.sort(statesToApplyLater, BY_REVISION);
|
if (!DecryptedGroupUtil.changeIsEmpty(missingChanges)) {
|
||||||
|
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, missingChanges));
|
||||||
if (statesToApplyNow.size() > 0) {
|
Log.w(TAG, "Inserted repair change for gap V" + revision);
|
||||||
newLocalState = statesToApplyNow.get(statesToApplyNow.size() - 1)
|
|
||||||
.getGroup();
|
|
||||||
} else {
|
|
||||||
newLocalState = inputState.getLocalState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newGlobalGroupState = new GlobalGroupState(newLocalState, statesToApplyLater);
|
current = groupAtRevision;
|
||||||
|
}
|
||||||
|
|
||||||
return new AdvanceGroupStateResult(statesToApplyNow, newGlobalGroupState);
|
return new AdvanceGroupStateResult(appliedChanges, new GlobalGroupState(current, statesToApplyLater));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||||
|
@ -99,7 +100,7 @@ public final class GroupsV2StateProcessor {
|
||||||
|
|
||||||
public static class GroupUpdateResult {
|
public static class GroupUpdateResult {
|
||||||
private final GroupState groupState;
|
private final GroupState groupState;
|
||||||
@Nullable private DecryptedGroup latestServer;
|
@Nullable private final DecryptedGroup latestServer;
|
||||||
|
|
||||||
GroupUpdateResult(@NonNull GroupState groupState, @Nullable DecryptedGroup latestServer) {
|
GroupUpdateResult(@NonNull GroupState groupState, @Nullable DecryptedGroup latestServer) {
|
||||||
this.groupState = groupState;
|
this.groupState = groupState;
|
||||||
|
@ -152,15 +153,19 @@ public final class GroupsV2StateProcessor {
|
||||||
localState.getRevision() + 1 == signedGroupChange.getRevision() &&
|
localState.getRevision() + 1 == signedGroupChange.getRevision() &&
|
||||||
revision == signedGroupChange.getRevision())
|
revision == signedGroupChange.getRevision())
|
||||||
{
|
{
|
||||||
|
if (SignalStore.internalValues().gv2IgnoreP2PChanges()) {
|
||||||
|
Log.w(TAG, "Ignoring P2P group change by setting");
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
Log.i(TAG, "Applying P2P group change");
|
Log.i(TAG, "Applying P2P group change");
|
||||||
DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange);
|
DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange);
|
||||||
|
|
||||||
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new GroupLogEntry(newState, signedGroupChange)));
|
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(newState, signedGroupChange)));
|
||||||
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
|
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
|
||||||
Log.w(TAG, "Unable to apply P2P group change", e);
|
Log.w(TAG, "Unable to apply P2P group change", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (inputGroupState == null) {
|
if (inputGroupState == null) {
|
||||||
try {
|
try {
|
||||||
|
@ -186,7 +191,7 @@ public final class GroupsV2StateProcessor {
|
||||||
persistLearnedProfileKeys(inputGroupState);
|
persistLearnedProfileKeys(inputGroupState);
|
||||||
|
|
||||||
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
|
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
|
||||||
if (remainingWork.getHistory().size() > 0) {
|
if (remainingWork.getServerHistory().size() > 0) {
|
||||||
Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, not applying at this time, V[%d..%d]", newLocalState.getRevision() + 1, remainingWork.getLatestRevisionNumber()));
|
Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, not applying at this time, V[%d..%d]", newLocalState.getRevision() + 1, remainingWork.getLatestRevisionNumber()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,9 +275,9 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void insertUpdateMessages(long timestamp, Collection<GroupLogEntry> processedLogEntries) {
|
private void insertUpdateMessages(long timestamp, Collection<LocalGroupLogEntry> processedLogEntries) {
|
||||||
for (GroupLogEntry entry : processedLogEntries) {
|
for (LocalGroupLogEntry entry : processedLogEntries) {
|
||||||
if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(entry.getChange())) {
|
if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(entry.getChange()) && !DecryptedGroupUtil.changeIsEmpty(entry.getChange())) {
|
||||||
Log.d(TAG, "Skipping profile key changes only update message");
|
Log.d(TAG, "Skipping profile key changes only update message");
|
||||||
} else {
|
} else {
|
||||||
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp);
|
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp);
|
||||||
|
@ -283,9 +288,9 @@ public final class GroupsV2StateProcessor {
|
||||||
private void persistLearnedProfileKeys(@NonNull GlobalGroupState globalGroupState) {
|
private void persistLearnedProfileKeys(@NonNull GlobalGroupState globalGroupState) {
|
||||||
final ProfileKeySet profileKeys = new ProfileKeySet();
|
final ProfileKeySet profileKeys = new ProfileKeySet();
|
||||||
|
|
||||||
for (GroupLogEntry entry : globalGroupState.getHistory()) {
|
for (ServerGroupLogEntry entry : globalGroupState.getServerHistory()) {
|
||||||
Optional<UUID> editor = DecryptedGroupUtil.editorUuid(entry.getChange());
|
Optional<UUID> editor = DecryptedGroupUtil.editorUuid(entry.getChange());
|
||||||
if (editor.isPresent()) {
|
if (editor.isPresent() && entry.getGroup() != null) {
|
||||||
profileKeys.addKeysFromGroupState(entry.getGroup(), editor.get());
|
profileKeys.addKeysFromGroupState(entry.getGroup(), editor.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,9 +306,9 @@ public final class GroupsV2StateProcessor {
|
||||||
private @NonNull GlobalGroupState queryServer(@Nullable DecryptedGroup localState, boolean latestOnly)
|
private @NonNull GlobalGroupState queryServer(@Nullable DecryptedGroup localState, boolean latestOnly)
|
||||||
throws IOException, GroupNotAMemberException
|
throws IOException, GroupNotAMemberException
|
||||||
{
|
{
|
||||||
DecryptedGroup latestServerGroup;
|
|
||||||
List<GroupLogEntry> history;
|
|
||||||
UUID selfUuid = Recipient.self().getUuid().get();
|
UUID selfUuid = Recipient.self().getUuid().get();
|
||||||
|
DecryptedGroup latestServerGroup;
|
||||||
|
List<ServerGroupLogEntry> history;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||||
|
@ -314,7 +319,7 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latestOnly || !GroupProtoUtil.isMember(selfUuid, latestServerGroup.getMembersList())) {
|
if (latestOnly || !GroupProtoUtil.isMember(selfUuid, latestServerGroup.getMembersList())) {
|
||||||
history = Collections.singletonList(new GroupLogEntry(latestServerGroup, null));
|
history = Collections.singletonList(new ServerGroupLogEntry(latestServerGroup, null));
|
||||||
} else {
|
} else {
|
||||||
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfUuid);
|
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfUuid);
|
||||||
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
|
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
|
||||||
|
@ -325,13 +330,18 @@ public final class GroupsV2StateProcessor {
|
||||||
return new GlobalGroupState(localState, history);
|
return new GlobalGroupState(localState, history);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<GroupLogEntry> getFullMemberHistory(@NonNull UUID selfUuid, int logsNeededFromRevision) throws IOException {
|
private List<ServerGroupLogEntry> getFullMemberHistory(@NonNull UUID selfUuid, int logsNeededFromRevision) throws IOException {
|
||||||
try {
|
try {
|
||||||
Collection<DecryptedGroupHistoryEntry> groupStatesFromRevision = groupsV2Api.getGroupHistory(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
Collection<DecryptedGroupHistoryEntry> groupStatesFromRevision = groupsV2Api.getGroupHistory(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||||
ArrayList<GroupLogEntry> history = new ArrayList<>(groupStatesFromRevision.size());
|
ArrayList<ServerGroupLogEntry> history = new ArrayList<>(groupStatesFromRevision.size());
|
||||||
|
boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges();
|
||||||
|
|
||||||
|
if (ignoreServerChanges) {
|
||||||
|
Log.w(TAG, "Server change logs are ignored by setting");
|
||||||
|
}
|
||||||
|
|
||||||
for (DecryptedGroupHistoryEntry entry : groupStatesFromRevision) {
|
for (DecryptedGroupHistoryEntry entry : groupStatesFromRevision) {
|
||||||
history.add(new GroupLogEntry(entry.getGroup(), entry.getChange()));
|
history.add(new ServerGroupLogEntry(entry.getGroup(), ignoreServerChanges ? null : entry.getChange()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return history;
|
return history;
|
||||||
|
@ -343,12 +353,7 @@ public final class GroupsV2StateProcessor {
|
||||||
private void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
|
private void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
|
||||||
Optional<UUID> editor = getEditor(decryptedGroupV2Context);
|
Optional<UUID> editor = getEditor(decryptedGroupV2Context);
|
||||||
|
|
||||||
if (!editor.isPresent() || UuidUtil.UNKNOWN_UUID.equals(editor.get())) {
|
boolean outgoing = !editor.isPresent() || Recipient.self().requireUuid().equals(editor.get());
|
||||||
Log.w(TAG, "Cannot determine editor of change, can't insert message");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean outgoing = Recipient.self().requireUuid().equals(editor.get());
|
|
||||||
|
|
||||||
if (outgoing) {
|
if (outgoing) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -6,17 +6,21 @@ import androidx.annotation.Nullable;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pair of a group state and optionally the corresponding change.
|
* Pair of a group state and optionally the corresponding change.
|
||||||
* <p>
|
* <p>
|
||||||
|
* Similar to {@link ServerGroupLogEntry} but guaranteed to have a group state.
|
||||||
|
* <p>
|
||||||
* Changes are typically not available for pending members.
|
* Changes are typically not available for pending members.
|
||||||
*/
|
*/
|
||||||
final class GroupLogEntry {
|
final class LocalGroupLogEntry {
|
||||||
|
|
||||||
@NonNull private final DecryptedGroup group;
|
@NonNull private final DecryptedGroup group;
|
||||||
@Nullable private final DecryptedGroupChange change;
|
@Nullable private final DecryptedGroupChange change;
|
||||||
|
|
||||||
GroupLogEntry(@NonNull DecryptedGroup group, @Nullable DecryptedGroupChange change) {
|
LocalGroupLogEntry(@NonNull DecryptedGroup group, @Nullable DecryptedGroupChange change) {
|
||||||
if (change != null && group.getRevision() != change.getRevision()) {
|
if (change != null && group.getRevision() != change.getRevision()) {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
@ -32,4 +36,20 @@ final class GroupLogEntry {
|
||||||
@Nullable DecryptedGroupChange getChange() {
|
@Nullable DecryptedGroupChange getChange() {
|
||||||
return change;
|
return change;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof LocalGroupLogEntry)) return false;
|
||||||
|
|
||||||
|
LocalGroupLogEntry other = (LocalGroupLogEntry) o;
|
||||||
|
|
||||||
|
return group.equals(other.group) && Objects.equals(change, other.change);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = group.hashCode();
|
||||||
|
result = 31 * result + (change != null ? change.hashCode() : 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pair of a group state and optionally the corresponding change from the server.
|
||||||
|
* <p>
|
||||||
|
* Either the group or change may be empty.
|
||||||
|
* <p>
|
||||||
|
* Changes are typically not available for pending members.
|
||||||
|
*/
|
||||||
|
final class ServerGroupLogEntry {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(ServerGroupLogEntry.class);
|
||||||
|
|
||||||
|
@Nullable private final DecryptedGroup group;
|
||||||
|
@Nullable private final DecryptedGroupChange change;
|
||||||
|
|
||||||
|
ServerGroupLogEntry(@Nullable DecryptedGroup group, @Nullable DecryptedGroupChange change) {
|
||||||
|
if (change != null && group != null && group.getRevision() != change.getRevision()) {
|
||||||
|
Log.w(TAG, "Ignoring change with revision number not matching group");
|
||||||
|
change = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change == null && group == null) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.group = group;
|
||||||
|
this.change = change;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable DecryptedGroup getGroup() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable DecryptedGroupChange getChange() {
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getRevision() {
|
||||||
|
if (group != null) return group.getRevision();
|
||||||
|
else if (change != null) return change.getRevision();
|
||||||
|
else throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
public final class InternalValues extends SignalStoreValues {
|
public final class InternalValues extends SignalStoreValues {
|
||||||
|
|
||||||
public static final String GV2_FORCE_INVITES = "internal.gv2.force_invites";
|
public static final String GV2_FORCE_INVITES = "internal.gv2.force_invites";
|
||||||
|
public static final String GV2_IGNORE_SERVER_CHANGES = "internal.gv2.ignore_server_changes";
|
||||||
|
public static final String GV2_IGNORE_P2P_CHANGES = "internal.gv2.ignore_p2p_changes";
|
||||||
|
|
||||||
InternalValues(KeyValueStore store) {
|
InternalValues(KeyValueStore store) {
|
||||||
super(store);
|
super(store);
|
||||||
|
@ -17,4 +19,25 @@ public final class InternalValues extends SignalStoreValues {
|
||||||
public synchronized boolean forceGv2Invites() {
|
public synchronized boolean forceGv2Invites() {
|
||||||
return FeatureFlags.internalUser() && getBoolean(GV2_FORCE_INVITES, false);
|
return FeatureFlags.internalUser() && getBoolean(GV2_FORCE_INVITES, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Server will leave out changes that can only be described by a future protocol level that
|
||||||
|
* an older client cannot understand. Ignoring those changes by nulling them out simulates that
|
||||||
|
* scenario for testing.
|
||||||
|
* <p>
|
||||||
|
* In conjunction with {@link #gv2IgnoreP2PChanges()} it means no group changes are coming into
|
||||||
|
* the client and it will generate changes by group state comparison, and those changes will not
|
||||||
|
* have an editor and so will be in the passive voice.
|
||||||
|
*/
|
||||||
|
public synchronized boolean gv2IgnoreServerChanges() {
|
||||||
|
return FeatureFlags.internalUser() && getBoolean(GV2_IGNORE_SERVER_CHANGES, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signed group changes are sent P2P, if the client ignores them, it will then ask the server
|
||||||
|
* directly which allows testing of certain testing scenarios.
|
||||||
|
*/
|
||||||
|
public synchronized boolean gv2IgnoreP2PChanges() {
|
||||||
|
return FeatureFlags.internalUser() && getBoolean(GV2_IGNORE_P2P_CHANGES, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
package org.thoughtcrime.securesms.preferences;
|
package org.thoughtcrime.securesms.preferences;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.preference.PreferenceDataStore;
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.InternalValues;
|
import org.thoughtcrime.securesms.keyvalue.InternalValues;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
@ -26,14 +32,39 @@ public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragme
|
||||||
|
|
||||||
PreferenceDataStore preferenceDataStore = SignalStore.getPreferenceDataStore();
|
PreferenceDataStore preferenceDataStore = SignalStore.getPreferenceDataStore();
|
||||||
|
|
||||||
SwitchPreferenceCompat forceGv2Preference = (SwitchPreferenceCompat) this.findPreference(InternalValues.GV2_FORCE_INVITES);
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_FORCE_INVITES, SignalStore.internalValues().forceGv2Invites());
|
||||||
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_SERVER_CHANGES, SignalStore.internalValues().gv2IgnoreServerChanges());
|
||||||
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_P2P_CHANGES, SignalStore.internalValues().gv2IgnoreP2PChanges());
|
||||||
|
|
||||||
|
findPreference("pref_refresh_attributes").setOnPreferenceClickListener(preference -> {
|
||||||
|
ApplicationDependencies.getJobManager()
|
||||||
|
.startChain(new RefreshAttributesJob())
|
||||||
|
.then(new RefreshOwnProfileJob())
|
||||||
|
.enqueue();
|
||||||
|
Toast.makeText(getContext(), "Scheduled attribute refresh", Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
findPreference("pref_rotate_profile_key").setOnPreferenceClickListener(preference -> {
|
||||||
|
ApplicationDependencies.getJobManager().add(new RotateProfileKeyJob());
|
||||||
|
Toast.makeText(getContext(), "Scheduled profile key rotation", Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeSwitchPreference(@NonNull PreferenceDataStore preferenceDataStore,
|
||||||
|
@NonNull String key,
|
||||||
|
boolean checked)
|
||||||
|
{
|
||||||
|
SwitchPreferenceCompat forceGv2Preference = (SwitchPreferenceCompat) findPreference(key);
|
||||||
forceGv2Preference.setPreferenceDataStore(preferenceDataStore);
|
forceGv2Preference.setPreferenceDataStore(preferenceDataStore);
|
||||||
forceGv2Preference.setChecked(SignalStore.internalValues().forceGv2Invites());
|
forceGv2Preference.setChecked(checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
//noinspection ConstantConditions
|
||||||
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__internal_preferences);
|
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__internal_preferences);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1947,6 +1947,15 @@
|
||||||
<string name="preferences__internal_preferences_groups_v2" translatable="false">Groups V2</string>
|
<string name="preferences__internal_preferences_groups_v2" translatable="false">Groups V2</string>
|
||||||
<string name="preferences__internal_force_gv2_invites" translatable="false">Force Invites</string>
|
<string name="preferences__internal_force_gv2_invites" translatable="false">Force Invites</string>
|
||||||
<string name="preferences__internal_force_gv2_invites_description" translatable="false">Members will not be added directly to a GV2 even if they could be.</string>
|
<string name="preferences__internal_force_gv2_invites_description" translatable="false">Members will not be added directly to a GV2 even if they could be.</string>
|
||||||
|
<string name="preferences__internal_ignore_gv2_server_changes" translatable="false">Ignore server changes</string>
|
||||||
|
<string name="preferences__internal_ignore_gv2_server_changes_description" translatable="false">Changes in server\'s response will be ignored, causing passive voice update messages if P2P is also ignored.</string>
|
||||||
|
<string name="preferences__internal_ignore_gv2_p2p_changes" translatable="false">Ignore P2P changes</string>
|
||||||
|
<string name="preferences__internal_ignore_gv2_p2p_changes_description" translatable="false">Changes sent P2P will be ignored. In conjunction with ignoring server changes, will cause passive voice.</string>
|
||||||
|
<string name="preferences__internal_account" translatable="false">Account</string>
|
||||||
|
<string name="preferences__internal_refresh_attributes" translatable="false">Refresh attributes</string>
|
||||||
|
<string name="preferences__internal_refresh_attributes_description" translatable="false">Forces a write of capabilities on to the server followed by a read.</string>
|
||||||
|
<string name="preferences__internal_rotate_profile_key" translatable="false">Rotate profile key</string>
|
||||||
|
<string name="preferences__internal_rotate_profile_key_description" translatable="false">Creates a new versioned profile, and triggers an update of any GV2 group you belong to.</string>
|
||||||
|
|
||||||
<!-- **************************************** -->
|
<!-- **************************************** -->
|
||||||
<!-- menus -->
|
<!-- menus -->
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="internal_account"
|
||||||
|
android:title="@string/preferences__internal_account">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="pref_refresh_attributes"
|
||||||
|
android:summary="@string/preferences__internal_refresh_attributes_description"
|
||||||
|
android:title="@string/preferences__internal_refresh_attributes" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="pref_rotate_profile_key"
|
||||||
|
android:summary="@string/preferences__internal_rotate_profile_key_description"
|
||||||
|
android:title="@string/preferences__internal_rotate_profile_key" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:key="internal_groupsV2"
|
android:key="internal_groupsV2"
|
||||||
android:title="@string/preferences__internal_preferences_groups_v2">
|
android:title="@string/preferences__internal_preferences_groups_v2">
|
||||||
|
@ -11,6 +27,18 @@
|
||||||
android:summary="@string/preferences__internal_force_gv2_invites_description"
|
android:summary="@string/preferences__internal_force_gv2_invites_description"
|
||||||
android:title="@string/preferences__internal_force_gv2_invites" />
|
android:title="@string/preferences__internal_force_gv2_invites" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="internal.gv2.ignore_server_changes"
|
||||||
|
android:summary="@string/preferences__internal_ignore_gv2_server_changes_description"
|
||||||
|
android:title="@string/preferences__internal_ignore_gv2_server_changes" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="internal.gv2.ignore_p2p_changes"
|
||||||
|
android:summary="@string/preferences__internal_ignore_gv2_p2p_changes_description"
|
||||||
|
android:title="@string/preferences__internal_ignore_gv2_p2p_changes" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
|
@ -31,8 +31,8 @@ public final class GlobalGroupStateTest {
|
||||||
assertEquals(4, emptyState.getLatestRevisionNumber());
|
assertEquals(4, emptyState.getLatestRevisionNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GroupLogEntry logEntry(int revision) {
|
private static ServerGroupLogEntry logEntry(int revision) {
|
||||||
return new GroupLogEntry(state(revision), change(revision));
|
return new ServerGroupLogEntry(state(revision), change(revision));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DecryptedGroup state(int revision) {
|
private static DecryptedGroup state(int revision) {
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
package org.thoughtcrime.securesms.groups.v2.processing;
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
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.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.testutil.LogRecorder;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
@ -17,12 +26,19 @@ import static org.thoughtcrime.securesms.groups.v2.processing.GroupStateMapper.L
|
||||||
|
|
||||||
public final class GroupStateMapperTest {
|
public final class GroupStateMapperTest {
|
||||||
|
|
||||||
|
private static final UUID KNOWN_EDITOR = UUID.randomUUID();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
Log.initialize(new LogRecorder());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void unknown_group_with_no_states_to_update() {
|
public void unknown_group_with_no_states_to_update() {
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, emptyList()), 10);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, emptyList()), 10);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
assertNull(advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertNull(advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,69 +49,69 @@ public final class GroupStateMapperTest {
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, emptyList()), 10);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, emptyList()), 10);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
assertSame(currentState, advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertSame(currentState, advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void unknown_group_single_state_to_update() {
|
public void unknown_group_single_state_to_update() {
|
||||||
GroupLogEntry log0 = logEntry(0);
|
ServerGroupLogEntry log0 = serverLogEntry(0);
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, singletonList(log0)), 10);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, singletonList(log0)), 10);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(log0)));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
|
||||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
assertEquals(log0.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertEquals(log0.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void known_group_single_state_to_update() {
|
public void known_group_single_state_to_update() {
|
||||||
DecryptedGroup currentState = state(0);
|
DecryptedGroup currentState = state(0);
|
||||||
GroupLogEntry log1 = logEntry(1);
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log1)), 1);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log1)), 1);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(log1)));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log1))));
|
||||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
assertEquals(log1.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertEquals(log1.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void known_group_two_states_to_update() {
|
public void known_group_two_states_to_update() {
|
||||||
DecryptedGroup currentState = state(0);
|
DecryptedGroup currentState = state(0);
|
||||||
GroupLogEntry log1 = logEntry(1);
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
GroupLogEntry log2 = logEntry(2);
|
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2)));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void known_group_two_states_to_update_already_on_one() {
|
public void known_group_two_states_to_update_already_on_one() {
|
||||||
DecryptedGroup currentState = state(1);
|
DecryptedGroup currentState = state(1);
|
||||||
GroupLogEntry log1 = logEntry(1);
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
GroupLogEntry log2 = logEntry(2);
|
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(log2)));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log2))));
|
||||||
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void known_group_three_states_to_update_stop_at_2() {
|
public void known_group_three_states_to_update_stop_at_2() {
|
||||||
DecryptedGroup currentState = state(0);
|
DecryptedGroup currentState = state(0);
|
||||||
GroupLogEntry log1 = logEntry(1);
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
GroupLogEntry log2 = logEntry(2);
|
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||||
GroupLogEntry log3 = logEntry(3);
|
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), 2);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), 2);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2)));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||||
assertNewState(new GlobalGroupState(log2.getGroup(), singletonList(log3)), advanceGroupStateResult.getNewGlobalGroupState());
|
assertNewState(new GlobalGroupState(log2.getGroup(), singletonList(log3)), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
@ -103,13 +119,13 @@ public final class GroupStateMapperTest {
|
||||||
@Test
|
@Test
|
||||||
public void known_group_three_states_to_update_update_latest() {
|
public void known_group_three_states_to_update_update_latest() {
|
||||||
DecryptedGroup currentState = state(0);
|
DecryptedGroup currentState = state(0);
|
||||||
GroupLogEntry log1 = logEntry(1);
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
GroupLogEntry log2 = logEntry(2);
|
ServerGroupLogEntry log2 = serverLogEntry(2);
|
||||||
GroupLogEntry log3 = logEntry(3);
|
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), LATEST);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), LATEST);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2, log3)));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2), asLocal(log3))));
|
||||||
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
@ -117,30 +133,230 @@ public final class GroupStateMapperTest {
|
||||||
@Test
|
@Test
|
||||||
public void apply_maximum_group_revisions() {
|
public void apply_maximum_group_revisions() {
|
||||||
DecryptedGroup currentState = state(Integer.MAX_VALUE - 2);
|
DecryptedGroup currentState = state(Integer.MAX_VALUE - 2);
|
||||||
GroupLogEntry log1 = logEntry(Integer.MAX_VALUE - 1);
|
ServerGroupLogEntry log1 = serverLogEntry(Integer.MAX_VALUE - 1);
|
||||||
GroupLogEntry log2 = logEntry(Integer.MAX_VALUE);
|
ServerGroupLogEntry log2 = serverLogEntry(Integer.MAX_VALUE);
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), LATEST);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), LATEST);
|
||||||
|
|
||||||
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2)));
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
|
||||||
assertNewState(new GlobalGroupState(log2.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
assertNewState(new GlobalGroupState(log2.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertNewState(GlobalGroupState expected, GlobalGroupState actual) {
|
@Test
|
||||||
assertEquals(expected.getLocalState(), actual.getLocalState());
|
public void unknown_group_single_state_to_update_with_missing_change() {
|
||||||
assertThat(actual.getHistory(), is(expected.getHistory()));
|
ServerGroupLogEntry log0 = serverLogEntryWholeStateOnly(0);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, singletonList(log0)), 10);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
|
assertEquals(log0.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GroupLogEntry logEntry(int revision) {
|
@Test
|
||||||
return new GroupLogEntry(state(revision), change(revision));
|
public void known_group_single_state_to_update_with_missing_change() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
ServerGroupLogEntry log1 = serverLogEntryWholeStateOnly(1);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log1)), 1);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(localLogEntryNoEditor(1))));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getServerHistory().isEmpty());
|
||||||
|
assertEquals(log1.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_three_states_to_update_update_latest_handle_missing_change() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
|
ServerGroupLogEntry log2 = serverLogEntryWholeStateOnly(2);
|
||||||
|
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), localLogEntryNoEditor(2), asLocal(log3))));
|
||||||
|
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_three_states_to_update_update_latest_handle_gap_with_no_changes() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
|
ServerGroupLogEntry log3 = serverLogEntry(3);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log3)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log3))));
|
||||||
|
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_three_states_to_update_update_latest_handle_gap_with_changes() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
ServerGroupLogEntry log1 = serverLogEntry(1);
|
||||||
|
DecryptedGroup state3 = DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(3)
|
||||||
|
.setTitle("Group Revision " + 3)
|
||||||
|
.setAvatar("Lost Avatar Update")
|
||||||
|
.build();
|
||||||
|
ServerGroupLogEntry log3 = new ServerGroupLogEntry(state3, change(3));
|
||||||
|
DecryptedGroup state4 = DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(4)
|
||||||
|
.setTitle("Group Revision " + 4)
|
||||||
|
.setAvatar("Lost Avatar Update")
|
||||||
|
.build();
|
||||||
|
ServerGroupLogEntry log4 = new ServerGroupLogEntry(state4, change(4));
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log3, log4)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
|
||||||
|
asLocal(log3),
|
||||||
|
new LocalGroupLogEntry(state3, DecryptedGroupChange.newBuilder()
|
||||||
|
.setRevision(3)
|
||||||
|
.setNewAvatar(DecryptedString.newBuilder().setValue("Lost Avatar Update"))
|
||||||
|
.build()),
|
||||||
|
asLocal(log4))));
|
||||||
|
|
||||||
|
assertNewState(new GlobalGroupState(log4.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log4.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updates_with_all_changes_missing() {
|
||||||
|
DecryptedGroup currentState = state(5);
|
||||||
|
ServerGroupLogEntry log6 = serverLogEntryWholeStateOnly(6);
|
||||||
|
ServerGroupLogEntry log7 = serverLogEntryWholeStateOnly(7);
|
||||||
|
ServerGroupLogEntry log8 = serverLogEntryWholeStateOnly(8);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log6, log7, log8)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(localLogEntryNoEditor(6), localLogEntryNoEditor(7), localLogEntryNoEditor(8))));
|
||||||
|
assertNewState(new GlobalGroupState(log8.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log8.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updates_with_all_group_states_missing() {
|
||||||
|
DecryptedGroup currentState = state(6);
|
||||||
|
ServerGroupLogEntry log7 = logEntryMissingState(7);
|
||||||
|
ServerGroupLogEntry log8 = logEntryMissingState(8);
|
||||||
|
ServerGroupLogEntry log9 = logEntryMissingState(9);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log7, log8, log9)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(serverLogEntry(7)), asLocal(serverLogEntry(8)), asLocal(serverLogEntry(9)))));
|
||||||
|
assertNewState(new GlobalGroupState(state(9), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(state(9), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updates_with_a_server_mismatch_inserts_additional_update() {
|
||||||
|
DecryptedGroup currentState = state(6);
|
||||||
|
ServerGroupLogEntry log7 = serverLogEntry(7);
|
||||||
|
DecryptedMember newMember = DecryptedMember.newBuilder()
|
||||||
|
.setUuid(UuidUtil.toByteString(UUID.randomUUID()))
|
||||||
|
.build();
|
||||||
|
ServerGroupLogEntry log8 = new ServerGroupLogEntry(DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(8)
|
||||||
|
.addMembers(newMember)
|
||||||
|
.setTitle("Group Revision " + 8)
|
||||||
|
.build(),
|
||||||
|
change(8) );
|
||||||
|
ServerGroupLogEntry log9 = new ServerGroupLogEntry(DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(9)
|
||||||
|
.addMembers(newMember)
|
||||||
|
.setTitle("Group Revision " + 9)
|
||||||
|
.build(),
|
||||||
|
change(9) );
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log7, log8, log9)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log7),
|
||||||
|
asLocal(log8),
|
||||||
|
asLocal(new ServerGroupLogEntry(log8.getGroup(), DecryptedGroupChange.newBuilder()
|
||||||
|
.setRevision(8)
|
||||||
|
.addNewMembers(newMember)
|
||||||
|
.build())),
|
||||||
|
asLocal(log9))));
|
||||||
|
assertNewState(new GlobalGroupState(log9.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log9.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void local_up_to_date_no_repair_necessary() {
|
||||||
|
DecryptedGroup currentState = state(6);
|
||||||
|
ServerGroupLogEntry log6 = serverLogEntryWholeStateOnly(6);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log6)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||||
|
assertNewState(new GlobalGroupState(state(6), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(state(6), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void local_on_same_revision_but_incorrect_repair_necessary() {
|
||||||
|
DecryptedGroup currentState = DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(6)
|
||||||
|
.setTitle("Incorrect group title, Revision " + 6)
|
||||||
|
.build();
|
||||||
|
ServerGroupLogEntry log6 = serverLogEntryWholeStateOnly(6);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log6)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(localLogEntryNoEditor(6))));
|
||||||
|
assertNewState(new GlobalGroupState(state(6), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(state(6), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertNewState(GlobalGroupState expected, GlobalGroupState actual) {
|
||||||
|
assertEquals(expected.getLocalState(), actual.getLocalState());
|
||||||
|
assertThat(actual.getServerHistory(), is(expected.getServerHistory()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerGroupLogEntry serverLogEntry(int revision) {
|
||||||
|
return new ServerGroupLogEntry(state(revision), change(revision));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalGroupLogEntry localLogEntryNoEditor(int revision) {
|
||||||
|
return new LocalGroupLogEntry(state(revision), changeNoEditor(revision));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerGroupLogEntry serverLogEntryWholeStateOnly(int revision) {
|
||||||
|
return new ServerGroupLogEntry(state(revision), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerGroupLogEntry logEntryMissingState(int revision) {
|
||||||
|
return new ServerGroupLogEntry(null, change(revision));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DecryptedGroup state(int revision) {
|
private static DecryptedGroup state(int revision) {
|
||||||
return DecryptedGroup.newBuilder().setRevision(revision).build();
|
return DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(revision)
|
||||||
|
.setTitle("Group Revision " + revision)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DecryptedGroupChange change(int revision) {
|
private static DecryptedGroupChange change(int revision) {
|
||||||
return DecryptedGroupChange.newBuilder().setRevision(revision).build();
|
return DecryptedGroupChange.newBuilder()
|
||||||
|
.setRevision(revision)
|
||||||
|
.setEditor(UuidUtil.toByteString(KNOWN_EDITOR))
|
||||||
|
.setNewTitle(DecryptedString.newBuilder().setValue("Group Revision " + revision))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecryptedGroupChange changeNoEditor(int revision) {
|
||||||
|
return DecryptedGroupChange.newBuilder()
|
||||||
|
.setRevision(revision)
|
||||||
|
.setNewTitle(DecryptedString.newBuilder().setValue("Group Revision " + revision))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalGroupLogEntry asLocal(ServerGroupLogEntry logEntry) {
|
||||||
|
assertNotNull(logEntry.getGroup());
|
||||||
|
return new LocalGroupLogEntry(logEntry.getGroup(), logEntry.getChange());
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -197,6 +197,12 @@ public final class DecryptedGroupUtil {
|
||||||
throw new NotAbleToApplyChangeException();
|
throw new NotAbleToApplyChangeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return applyWithoutRevisionCheck(group, change);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DecryptedGroup applyWithoutRevisionCheck(DecryptedGroup group, DecryptedGroupChange change)
|
||||||
|
throws NotAbleToApplyChangeException
|
||||||
|
{
|
||||||
DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group);
|
DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group);
|
||||||
|
|
||||||
builder.addAllMembers(change.getNewMembersList());
|
builder.addAllMembers(change.getNewMembersList());
|
||||||
|
@ -228,7 +234,7 @@ public final class DecryptedGroupUtil {
|
||||||
throw new NotAbleToApplyChangeException();
|
throw new NotAbleToApplyChangeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setMembers(index, modifyProfileKey);
|
builder.setMembers(index, DecryptedMember.newBuilder(builder.getMembers(index)).setProfileKey(modifyProfileKey.getProfileKey()).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (DecryptedPendingMemberRemoval removedMember : change.getDeletePendingMembersList()) {
|
for (DecryptedPendingMemberRemoval removedMember : change.getDeletePendingMembersList()) {
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
package org.whispersystems.signalservice.api.groupsv2;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class GroupChangeReconstruct {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a {@param fromState} and a {@param toState} creates a {@link DecryptedGroupChange} that would take the {@param fromState} to the {@param toState}.
|
||||||
|
*/
|
||||||
|
public static DecryptedGroupChange reconstructGroupChange(DecryptedGroup fromState, DecryptedGroup toState) {
|
||||||
|
DecryptedGroupChange.Builder builder = DecryptedGroupChange.newBuilder()
|
||||||
|
.setRevision(toState.getRevision());
|
||||||
|
|
||||||
|
if (!fromState.getTitle().equals(toState.getTitle())) {
|
||||||
|
builder.setNewTitle(DecryptedString.newBuilder().setValue(toState.getTitle()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fromState.getAvatar().equals(toState.getAvatar())) {
|
||||||
|
builder.setNewAvatar(DecryptedString.newBuilder().setValue(toState.getAvatar()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fromState.getDisappearingMessagesTimer().equals(toState.getDisappearingMessagesTimer())) {
|
||||||
|
builder.setNewTimer(toState.getDisappearingMessagesTimer());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fromState.getAccessControl().getAttributes().equals(toState.getAccessControl().getAttributes())) {
|
||||||
|
builder.setNewAttributeAccess(toState.getAccessControl().getAttributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fromState.getAccessControl().getMembers().equals(toState.getAccessControl().getMembers())) {
|
||||||
|
builder.setNewMemberAccess(toState.getAccessControl().getMembers());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ByteString> fromStateMemberUuids = membersToSetOfUuids(fromState.getMembersList());
|
||||||
|
Set<ByteString> toStateMemberUuids = membersToSetOfUuids(toState.getMembersList());
|
||||||
|
|
||||||
|
Set<ByteString> pendingMembersListA = pendingMembersToSetOfUuids(fromState.getPendingMembersList());
|
||||||
|
Set<ByteString> pendingMembersListB = pendingMembersToSetOfUuids(toState.getPendingMembersList());
|
||||||
|
|
||||||
|
Set<ByteString> removedPendingMemberUuids = subtract(pendingMembersListA, pendingMembersListB);
|
||||||
|
Set<ByteString> newPendingMemberUuids = subtract(pendingMembersListB, pendingMembersListA);
|
||||||
|
Set<ByteString> removedMemberUuids = subtract(fromStateMemberUuids, toStateMemberUuids);
|
||||||
|
Set<ByteString> newMemberUuids = subtract(toStateMemberUuids, fromStateMemberUuids);
|
||||||
|
|
||||||
|
Set<ByteString> addedByInvitationUuids = intersect(newMemberUuids, removedPendingMemberUuids);
|
||||||
|
Set<DecryptedMember> addedMembersByInvitation = intersectByUUID(toState.getMembersList(), addedByInvitationUuids);
|
||||||
|
Set<DecryptedMember> addedMembers = intersectByUUID(toState.getMembersList(), subtract(newMemberUuids, addedByInvitationUuids));
|
||||||
|
Set<DecryptedPendingMember> uninvitedMembers = intersectPendingByUUID(fromState.getPendingMembersList(), subtract(removedPendingMemberUuids, addedByInvitationUuids));
|
||||||
|
|
||||||
|
for (DecryptedMember member : intersectByUUID(fromState.getMembersList(), removedMemberUuids)) {
|
||||||
|
builder.addDeleteMembers(member.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DecryptedMember member : addedMembers) {
|
||||||
|
builder.addNewMembers(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DecryptedMember member : addedMembersByInvitation) {
|
||||||
|
builder.addPromotePendingMembers(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DecryptedPendingMember uninvitedMember : uninvitedMembers) {
|
||||||
|
builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
|
||||||
|
.setUuid(uninvitedMember.getUuid())
|
||||||
|
.setUuidCipherText(uninvitedMember.getUuidCipherText()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DecryptedPendingMember invitedMember : intersectPendingByUUID(toState.getPendingMembersList(), newPendingMemberUuids)) {
|
||||||
|
builder.addNewPendingMembers(invitedMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ByteString> consistentMemberUuids = intersect(fromStateMemberUuids, toStateMemberUuids);
|
||||||
|
Set<DecryptedMember> changedMembers = intersectByUUID(subtract(toState.getMembersList(), fromState.getMembersList()), consistentMemberUuids);
|
||||||
|
Map<ByteString, DecryptedMember> membersUuidMap = uuidMap(fromState.getMembersList());
|
||||||
|
|
||||||
|
for (DecryptedMember newState : changedMembers) {
|
||||||
|
DecryptedMember oldState = membersUuidMap.get(newState.getUuid());
|
||||||
|
if (oldState.getRole() != newState.getRole()) {
|
||||||
|
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
|
||||||
|
.setUuid(newState.getUuid())
|
||||||
|
.setRole(newState.getRole()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldState.getProfileKey() != newState.getProfileKey()) {
|
||||||
|
builder.addModifiedProfileKeys(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<ByteString, DecryptedMember> uuidMap(List<DecryptedMember> membersList) {
|
||||||
|
HashMap<ByteString, DecryptedMember> map = new HashMap<>(membersList.size());
|
||||||
|
for (DecryptedMember member : membersList) {
|
||||||
|
map.put(member.getUuid(), member);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<DecryptedMember> intersectByUUID(Collection<DecryptedMember> members, Set<ByteString> uuids) {
|
||||||
|
Set<DecryptedMember> result = new HashSet<>(members.size());
|
||||||
|
for (DecryptedMember member : members) {
|
||||||
|
if (uuids.contains(member.getUuid()))
|
||||||
|
result.add(member);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<DecryptedPendingMember> intersectPendingByUUID(Collection<DecryptedPendingMember> members, Set<ByteString> uuids) {
|
||||||
|
Set<DecryptedPendingMember> result = new HashSet<>(members.size());
|
||||||
|
for (DecryptedPendingMember member : members) {
|
||||||
|
if (uuids.contains(member.getUuid()))
|
||||||
|
result.add(member);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<ByteString> pendingMembersToSetOfUuids(Collection<DecryptedPendingMember> pendingMembers) {
|
||||||
|
HashSet<ByteString> uuids = new HashSet<>(pendingMembers.size());
|
||||||
|
for (DecryptedPendingMember pendingMember : pendingMembers) {
|
||||||
|
uuids.add(pendingMember.getUuid());
|
||||||
|
}
|
||||||
|
return uuids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<ByteString> membersToSetOfUuids(Collection<DecryptedMember> members) {
|
||||||
|
HashSet<ByteString> uuids = new HashSet<>(members.size());
|
||||||
|
for (DecryptedMember member : members) {
|
||||||
|
uuids.add(member.getUuid());
|
||||||
|
}
|
||||||
|
return uuids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Set<T> subtract(Collection<T> a, Collection<T> b) {
|
||||||
|
Set<T> result = new HashSet<>(a);
|
||||||
|
result.removeAll(b);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Set<T> intersect(Collection<T> a, Collection<T> b) {
|
||||||
|
Set<T> result = new HashSet<>(a);
|
||||||
|
result.retainAll(b);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package org.whispersystems.signalservice.api.groupsv2;
|
package org.whispersystems.signalservice.api.groupsv2;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.signal.storageservice.protos.groups.AccessControl;
|
import org.signal.storageservice.protos.groups.AccessControl;
|
||||||
import org.signal.storageservice.protos.groups.Member;
|
import org.signal.storageservice.protos.groups.Member;
|
||||||
|
@ -12,6 +14,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -170,6 +173,36 @@ public final class DecryptedGroupUtil_apply_Test {
|
||||||
newGroup);
|
newGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void apply_modify_admin_profile_keys() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||||
|
UUID adminUuid = UUID.randomUUID();
|
||||||
|
ProfileKey profileKey1 = randomProfileKey();
|
||||||
|
ProfileKey profileKey2a = randomProfileKey();
|
||||||
|
ProfileKey profileKey2b = randomProfileKey();
|
||||||
|
DecryptedMember member1 = member(UUID.randomUUID(), profileKey1);
|
||||||
|
DecryptedMember admin2a = admin(adminUuid, profileKey2a);
|
||||||
|
|
||||||
|
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(13)
|
||||||
|
.addMembers(member1)
|
||||||
|
.addMembers(admin2a)
|
||||||
|
.build(),
|
||||||
|
DecryptedGroupChange.newBuilder()
|
||||||
|
.setRevision(14)
|
||||||
|
.addModifiedProfileKeys(DecryptedMember.newBuilder(DecryptedMember.newBuilder()
|
||||||
|
.setUuid(UuidUtil.toByteString(adminUuid))
|
||||||
|
.build())
|
||||||
|
.setProfileKey(ByteString.copyFrom(profileKey2b.serialize())))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroup.newBuilder()
|
||||||
|
.setRevision(14)
|
||||||
|
.addMembers(member1)
|
||||||
|
.addMembers(admin(adminUuid, profileKey2b))
|
||||||
|
.build(),
|
||||||
|
newGroup);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void apply_new_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
public void apply_new_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
|
||||||
DecryptedMember member1 = member(UUID.randomUUID());
|
DecryptedMember member1 = member(UUID.randomUUID());
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
package org.whispersystems.signalservice.api.groupsv2;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.signal.storageservice.protos.groups.AccessControl;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
|
||||||
|
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
|
||||||
|
|
||||||
|
public final class GroupChangeReconstructTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void empty_to_empty() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void revision_set_to_the_target() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().setRevision(10).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().setRevision(20).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(20, decryptedGroupChange.getRevision());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void title_change() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().setTitle("A").build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().setTitle("B").build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().setNewTitle(DecryptedString.newBuilder().setValue("B")).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void avatar_change() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().setAvatar("A").build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().setAvatar("B").build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().setNewAvatar(DecryptedString.newBuilder().setValue("B")).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void timer_change() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(100)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(200)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().setNewTimer(DecryptedTimer.newBuilder().setDuration(200)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void access_control_change_attributes() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().setAccessControl(AccessControl.newBuilder().setAttributes(AccessControl.AccessRequired.MEMBER)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().setAccessControl(AccessControl.newBuilder().setAttributes(AccessControl.AccessRequired.ADMINISTRATOR)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().setNewAttributeAccess(AccessControl.AccessRequired.ADMINISTRATOR).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void access_control_change_membership() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().setAccessControl(AccessControl.newBuilder().setMembers(AccessControl.AccessRequired.ADMINISTRATOR)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().setAccessControl(AccessControl.newBuilder().setMembers(AccessControl.AccessRequired.MEMBER)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().setNewMemberAccess(AccessControl.AccessRequired.MEMBER).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void access_control_change_membership_and_attributes() {
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().setAccessControl(AccessControl.newBuilder().setMembers(AccessControl.AccessRequired.MEMBER)
|
||||||
|
.setAttributes(AccessControl.AccessRequired.ADMINISTRATOR)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().setAccessControl(AccessControl.newBuilder().setMembers(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||||
|
.setAttributes(AccessControl.AccessRequired.MEMBER)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||||
|
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void new_member() {
|
||||||
|
UUID uuidNew = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addMembers(member(uuidNew)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addNewMembers(member(uuidNew)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removed_member() {
|
||||||
|
UUID uuidOld = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addMembers(member(uuidOld)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addDeleteMembers(UuidUtil.toByteString(uuidOld)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void new_member_and_existing_member() {
|
||||||
|
UUID uuidOld = UUID.randomUUID();
|
||||||
|
UUID uuidNew = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addMembers(member(uuidOld)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addMembers(member(uuidOld)).addMembers(member(uuidNew)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addNewMembers(member(uuidNew)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removed_member_and_remaining_member() {
|
||||||
|
UUID uuidOld = UUID.randomUUID();
|
||||||
|
UUID uuidRemaining = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addMembers(member(uuidOld)).addMembers(member(uuidRemaining)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addMembers(member(uuidRemaining)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addDeleteMembers(UuidUtil.toByteString(uuidOld)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void new_member_by_invite() {
|
||||||
|
UUID uuidNew = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addPendingMembers(pendingMember(uuidNew)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addMembers(member(uuidNew)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addPromotePendingMembers(member(uuidNew)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uninvited_member_by_invite() {
|
||||||
|
UUID uuidNew = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addPendingMembers(pendingMember(uuidNew)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addDeletePendingMembers(pendingMemberRemoval(uuidNew)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void new_invite() {
|
||||||
|
UUID uuidNew = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addPendingMembers(pendingMember(uuidNew)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addNewPendingMembers(pendingMember(uuidNew)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void to_admin() {
|
||||||
|
UUID uuid = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addMembers(member(uuid)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addMembers(admin(uuid)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addModifyMemberRoles(promoteAdmin(uuid)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void to_member() {
|
||||||
|
UUID uuid = UUID.randomUUID();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addMembers(admin(uuid)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addMembers(member(uuid)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addModifyMemberRoles(demoteAdmin(uuid)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void profile_key_change_member() {
|
||||||
|
UUID uuid = UUID.randomUUID();
|
||||||
|
ProfileKey profileKey1 = randomProfileKey();
|
||||||
|
ProfileKey profileKey2 = randomProfileKey();
|
||||||
|
DecryptedGroup from = DecryptedGroup.newBuilder().addMembers(withProfileKey(admin(uuid),profileKey1)).build();
|
||||||
|
DecryptedGroup to = DecryptedGroup.newBuilder().addMembers(withProfileKey(admin(uuid),profileKey2)).build();
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||||
|
|
||||||
|
assertEquals(DecryptedGroupChange.newBuilder().addModifiedProfileKeys(withProfileKey(admin(uuid),profileKey2)).build(), decryptedGroupChange);
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,6 +96,10 @@ final class ProtoTestUtils {
|
||||||
return withProfileKey(member(uuid), profileKey);
|
return withProfileKey(member(uuid), profileKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static DecryptedMember admin(UUID uuid, ProfileKey profileKey) {
|
||||||
|
return withProfileKey(admin(uuid), profileKey);
|
||||||
|
}
|
||||||
|
|
||||||
static DecryptedMember admin(UUID uuid) {
|
static DecryptedMember admin(UUID uuid) {
|
||||||
return DecryptedMember.newBuilder()
|
return DecryptedMember.newBuilder()
|
||||||
.setUuid(UuidUtil.toByteString(uuid))
|
.setUuid(UuidUtil.toByteString(uuid))
|
||||||
|
|
Loading…
Add table
Reference in a new issue