Unit tests for the distributed group store.

Change-Id: Ie8f00b9bbc1ba46a6f80e70f63d1fd853d64154b
diff --git a/core/api/src/test/java/org/onosproject/store/cluster/messaging/ClusterCommunicationServiceAdapter.java b/core/api/src/test/java/org/onosproject/store/cluster/messaging/ClusterCommunicationServiceAdapter.java
new file mode 100644
index 0000000..04f890c
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/store/cluster/messaging/ClusterCommunicationServiceAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.store.cluster.messaging;
+
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.onosproject.cluster.NodeId;
+
+/**
+ * Testing adapter for the cluster communication service.
+ */
+public class ClusterCommunicationServiceAdapter
+        implements ClusterCommunicationService {
+
+    @Override
+    public void addSubscriber(MessageSubject subject,
+                              ClusterMessageHandler subscriber,
+                              ExecutorService executor) {
+    }
+
+    @Override
+    public void removeSubscriber(MessageSubject subject) {}
+
+    @Override
+    public <M> void broadcast(M message, MessageSubject subject,
+                              Function<M, byte[]> encoder) {
+    }
+
+    @Override
+    public <M> void broadcastIncludeSelf(M message,
+                                         MessageSubject subject, Function<M, byte[]> encoder) {
+    }
+
+    @Override
+    public <M> CompletableFuture<Void> unicast(M message, MessageSubject subject,
+                                               Function<M, byte[]> encoder, NodeId toNodeId) {
+        return null;
+    }
+
+    @Override
+    public <M> void multicast(M message, MessageSubject subject,
+                              Function<M, byte[]> encoder, Set<NodeId> nodes) {
+    }
+
+    @Override
+    public <M, R> CompletableFuture<R> sendAndReceive(M message,
+                                                      MessageSubject subject, Function<M, byte[]> encoder,
+                                                      Function<byte[], R> decoder, NodeId toNodeId) {
+        return null;
+    }
+
+    @Override
+    public <M, R> void addSubscriber(MessageSubject subject,
+                                     Function<byte[], M> decoder, Function<M, R> handler,
+                                     Function<R, byte[]> encoder, Executor executor) {
+    }
+
+    @Override
+    public <M, R> void addSubscriber(MessageSubject subject,
+                                     Function<byte[], M> decoder, Function<M, CompletableFuture<R>> handler,
+                                     Function<R, byte[]> encoder) {
+    }
+
+    @Override
+    public <M> void addSubscriber(MessageSubject subject,
+                                  Function<byte[], M> decoder, Consumer<M> handler,
+                                  Executor executor) {
+
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/store/service/TestEventuallyConsistentMap.java b/core/api/src/test/java/org/onosproject/store/service/TestEventuallyConsistentMap.java
index 5ee44c4..4f612de 100644
--- a/core/api/src/test/java/org/onosproject/store/service/TestEventuallyConsistentMap.java
+++ b/core/api/src/test/java/org/onosproject/store/service/TestEventuallyConsistentMap.java
@@ -91,7 +91,9 @@
         EventuallyConsistentMapEvent<K, V> addEvent =
                 new EventuallyConsistentMapEvent<>(mapName, PUT, key, value);
         notifyListeners(addEvent);
-        peerUpdateFunction.apply(key, value);
+        if (peerUpdateFunction != null) {
+            peerUpdateFunction.apply(key, value);
+        }
     }
 
     @Override
diff --git a/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java b/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java
index a0cb189..cd42b17 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java
@@ -1014,7 +1014,7 @@
     /**
      * Flattened map key to be used to store group entries.
      */
-    private class GroupStoreMapKey {
+    protected static class GroupStoreMapKey {
         private final DeviceId deviceId;
 
         public GroupStoreMapKey(DeviceId deviceId) {
@@ -1047,7 +1047,7 @@
         }
     }
 
-    private class GroupStoreKeyMapKey extends GroupStoreMapKey {
+    protected static class GroupStoreKeyMapKey extends GroupStoreMapKey {
         private final GroupKey appCookie;
         public GroupStoreKeyMapKey(DeviceId deviceId,
                                    GroupKey appCookie) {
@@ -1078,7 +1078,7 @@
         }
     }
 
-    private class GroupStoreIdMapKey extends GroupStoreMapKey {
+    protected static class GroupStoreIdMapKey extends GroupStoreMapKey {
         private final GroupId groupId;
         public GroupStoreIdMapKey(DeviceId deviceId,
                                   GroupId groupId) {
diff --git a/core/store/dist/src/test/java/org/onosproject/store/ecmap/EventuallyConsistentMapImplTest.java b/core/store/dist/src/test/java/org/onosproject/store/ecmap/EventuallyConsistentMapImplTest.java
index b5696a2..ccf6ee7 100644
--- a/core/store/dist/src/test/java/org/onosproject/store/ecmap/EventuallyConsistentMapImplTest.java
+++ b/core/store/dist/src/test/java/org/onosproject/store/ecmap/EventuallyConsistentMapImplTest.java
@@ -15,10 +15,22 @@
  */
 package org.onosproject.store.ecmap;
 
-import com.google.common.collect.ComparisonChain;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.util.concurrent.MoreExecutors;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 import org.junit.After;
 import org.junit.Before;
@@ -32,38 +44,35 @@
 import org.onosproject.event.AbstractEvent;
 import org.onosproject.store.Timestamp;
 import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
-import org.onosproject.store.cluster.messaging.ClusterMessageHandler;
+import org.onosproject.store.cluster.messaging.ClusterCommunicationServiceAdapter;
 import org.onosproject.store.cluster.messaging.MessageSubject;
 import org.onosproject.store.impl.LogicalTimestamp;
-import org.onosproject.store.service.WallClockTimestamp;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.serializers.KryoSerializer;
 import org.onosproject.store.service.EventuallyConsistentMap;
 import org.onosproject.store.service.EventuallyConsistentMapEvent;
 import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.WallClockTimestamp;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Consumer;
-import java.util.function.Function;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.MoreExecutors;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static junit.framework.TestCase.assertFalse;
-import static org.easymock.EasyMock.*;
-import static org.junit.Assert.*;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Unit tests for EventuallyConsistentMapImpl.
@@ -697,7 +706,7 @@
      * Sets up a mock ClusterCommunicationService to expect a specific cluster
      * message to be broadcast to the cluster.
      *
-     * @param m message we expect to be sent
+     * @param message message we expect to be sent
      * @param clusterCommunicator a mock ClusterCommunicationService to set up
      */
     //FIXME rename
@@ -776,56 +785,7 @@
      * events coming in from other instances.
      */
     private final class TestClusterCommunicationService
-            implements ClusterCommunicationService {
-
-        @Override
-        public void addSubscriber(MessageSubject subject,
-                                  ClusterMessageHandler subscriber,
-                                  ExecutorService executor) {
-        }
-
-        @Override
-        public void removeSubscriber(MessageSubject subject) {}
-
-        @Override
-        public <M> void broadcast(M message, MessageSubject subject,
-                Function<M, byte[]> encoder) {
-        }
-
-        @Override
-        public <M> void broadcastIncludeSelf(M message,
-                MessageSubject subject, Function<M, byte[]> encoder) {
-        }
-
-        @Override
-        public <M> CompletableFuture<Void> unicast(M message, MessageSubject subject,
-                Function<M, byte[]> encoder, NodeId toNodeId) {
-            return null;
-        }
-
-        @Override
-        public <M> void multicast(M message, MessageSubject subject,
-                Function<M, byte[]> encoder, Set<NodeId> nodes) {
-        }
-
-        @Override
-        public <M, R> CompletableFuture<R> sendAndReceive(M message,
-                MessageSubject subject, Function<M, byte[]> encoder,
-                Function<byte[], R> decoder, NodeId toNodeId) {
-            return null;
-        }
-
-        @Override
-        public <M, R> void addSubscriber(MessageSubject subject,
-                Function<byte[], M> decoder, Function<M, R> handler,
-                Function<R, byte[]> encoder, Executor executor) {
-        }
-
-        @Override
-        public <M, R> void addSubscriber(MessageSubject subject,
-                Function<byte[], M> decoder, Function<M, CompletableFuture<R>> handler,
-                Function<R, byte[]> encoder) {
-        }
+            extends ClusterCommunicationServiceAdapter {
 
         @Override
         public <M> void addSubscriber(MessageSubject subject,
diff --git a/core/store/dist/src/test/java/org/onosproject/store/group/impl/DistributedGroupStoreTest.java b/core/store/dist/src/test/java/org/onosproject/store/group/impl/DistributedGroupStoreTest.java
new file mode 100644
index 0000000..560fdb3
--- /dev/null
+++ b/core/store/dist/src/test/java/org/onosproject/store/group/impl/DistributedGroupStoreTest.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.store.group.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onosproject.core.DefaultGroupId;
+import org.onosproject.core.GroupId;
+import org.onosproject.mastership.MastershipServiceAdapter;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.MastershipRole;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.DefaultGroupKey;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.group.GroupEvent;
+import org.onosproject.net.group.GroupKey;
+import org.onosproject.net.group.GroupOperation;
+import org.onosproject.net.group.GroupStore;
+import org.onosproject.net.group.GroupStoreDelegate;
+import org.onosproject.store.cluster.messaging.ClusterCommunicationServiceAdapter;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.TestStorageService;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.onosproject.net.NetTestTools.APP_ID;
+import static org.onosproject.net.NetTestTools.did;
+
+/**
+ * Distributed group store test.
+ */
+public class DistributedGroupStoreTest {
+
+    DeviceId deviceId1 = did("dev1");
+    DeviceId deviceId2 = did("dev2");
+    GroupId groupId1 = new DefaultGroupId(1);
+    GroupId groupId2 = new DefaultGroupId(2);
+    GroupKey groupKey1 = new DefaultGroupKey("abc".getBytes());
+    GroupKey groupKey2 = new DefaultGroupKey("def".getBytes());
+
+    TrafficTreatment treatment =
+            DefaultTrafficTreatment.emptyTreatment();
+    GroupBucket selectGroupBucket =
+            DefaultGroupBucket.createSelectGroupBucket(treatment);
+    GroupBucket failoverGroupBucket =
+            DefaultGroupBucket.createFailoverGroupBucket(treatment,
+                    PortNumber.IN_PORT, groupId1);
+
+    GroupBuckets buckets = new GroupBuckets(ImmutableList.of(selectGroupBucket));
+    GroupDescription groupDescription1 = new DefaultGroupDescription(
+            deviceId1,
+            GroupDescription.Type.INDIRECT,
+            buckets,
+            groupKey1,
+            groupId1.id(),
+            APP_ID);
+    GroupDescription groupDescription2 = new DefaultGroupDescription(
+            deviceId2,
+            GroupDescription.Type.INDIRECT,
+            buckets,
+            groupKey2,
+            groupId2.id(),
+            APP_ID);
+
+    DistributedGroupStore groupStoreImpl;
+    GroupStore groupStore;
+    EventuallyConsistentMap auditPendingReqQueue;
+
+    static class MasterOfAll extends MastershipServiceAdapter {
+        @Override
+        public MastershipRole getLocalRole(DeviceId deviceId) {
+            return MastershipRole.MASTER;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        groupStoreImpl = new DistributedGroupStore();
+        groupStoreImpl.storageService = new TestStorageService();
+        groupStoreImpl.clusterCommunicator = new ClusterCommunicationServiceAdapter();
+        groupStoreImpl.mastershipService = new MasterOfAll();
+        groupStoreImpl.activate();
+        groupStore = groupStoreImpl;
+        auditPendingReqQueue =
+                TestUtils.getField(groupStoreImpl, "auditPendingReqQueue");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        groupStoreImpl.deactivate();
+    }
+
+    /**
+     * Tests the initial state of the store.
+     */
+    @Test
+    public void testEmptyStore() {
+        assertThat(groupStore.getGroupCount(deviceId1), is(0));
+        assertThat(groupStore.getGroup(deviceId1, groupId1), nullValue());
+        assertThat(groupStore.getGroup(deviceId1, groupKey1), nullValue());
+    }
+
+    /**
+     * Tests adding a pending group.
+     */
+    @Test
+    public void testAddPendingGroup() throws Exception {
+        // Make sure the pending list starts out empty
+        assertThat(auditPendingReqQueue.size(), is(0));
+
+        // Add a new pending group. Make sure that the store remains empty
+        groupStore.storeGroupDescription(groupDescription1);
+        assertThat(groupStore.getGroupCount(deviceId1), is(0));
+        assertThat(groupStore.getGroup(deviceId1, groupId1), nullValue());
+        assertThat(groupStore.getGroup(deviceId1, groupKey1), nullValue());
+
+        // Make sure the group is pending
+        assertThat(auditPendingReqQueue.size(), is(1));
+
+        groupStore.deviceInitialAuditCompleted(deviceId1, true);
+
+        // Make sure the group isn't pending anymore
+        assertThat(auditPendingReqQueue.size(), is(0));
+    }
+
+
+    /**
+     * Tests adding and removing a group.
+     */
+    @Test
+    public void testAddRemoveGroup() throws Exception {
+        groupStore.deviceInitialAuditCompleted(deviceId1, true);
+        assertThat(groupStore.deviceInitialAuditStatus(deviceId1), is(true));
+
+        // Make sure the pending list starts out empty
+        assertThat(auditPendingReqQueue.size(), is(0));
+
+        groupStore.storeGroupDescription(groupDescription1);
+        assertThat(groupStore.getGroupCount(deviceId1), is(1));
+        assertThat(groupStore.getGroup(deviceId1, groupId1), notNullValue());
+        assertThat(groupStore.getGroup(deviceId1, groupKey1), notNullValue());
+
+        // Make sure that nothing is pending
+        assertThat(auditPendingReqQueue.size(), is(0));
+
+        Group groupById = groupStore.getGroup(deviceId1, groupId1);
+        Group groupByKey = groupStore.getGroup(deviceId1, groupKey1);
+        assertThat(groupById, notNullValue());
+        assertThat(groupByKey, notNullValue());
+        assertThat(groupById, is(groupByKey));
+        assertThat(groupById.deviceId(), is(did("dev1")));
+
+        groupStore.removeGroupEntry(groupById);
+
+        assertThat(groupStore.getGroupCount(deviceId1), is(0));
+        assertThat(groupStore.getGroup(deviceId1, groupId1), nullValue());
+        assertThat(groupStore.getGroup(deviceId1, groupKey1), nullValue());
+
+        // Make sure that nothing is pending
+        assertThat(auditPendingReqQueue.size(), is(0));
+    }
+
+    /**
+     * Tests adding and removing a group.
+     */
+    @Test
+    public void testRemoveGroupDescription() throws Exception {
+        groupStore.deviceInitialAuditCompleted(deviceId1, true);
+
+        groupStore.storeGroupDescription(groupDescription1);
+
+        groupStore.deleteGroupDescription(deviceId1, groupKey1);
+
+        // Group should still be there, marked for removal
+        assertThat(groupStore.getGroupCount(deviceId1), is(1));
+        Group queriedGroup = groupStore.getGroup(deviceId1, groupId1);
+        assertThat(queriedGroup.state(), is(Group.GroupState.PENDING_DELETE));
+
+    }
+
+    /**
+     * Tests pushing group metrics.
+     */
+    @Test
+    public void testPushGroupMetrics() {
+        groupStore.deviceInitialAuditCompleted(deviceId1, true);
+        groupStore.deviceInitialAuditCompleted(deviceId2, true);
+
+        GroupDescription groupDescription3 = new DefaultGroupDescription(
+                deviceId1,
+                GroupDescription.Type.SELECT,
+                buckets,
+                new DefaultGroupKey("aaa".getBytes()),
+                null,
+                APP_ID);
+
+        groupStore.storeGroupDescription(groupDescription1);
+        groupStore.storeGroupDescription(groupDescription2);
+        groupStore.storeGroupDescription(groupDescription3);
+        Group group1 = groupStore.getGroup(deviceId1, groupId1);
+
+        assertThat(group1, instanceOf(DefaultGroup.class));
+        DefaultGroup defaultGroup1 = (DefaultGroup) group1;
+        defaultGroup1.setPackets(55L);
+        defaultGroup1.setBytes(66L);
+        groupStore.pushGroupMetrics(deviceId1, ImmutableList.of(group1));
+
+        // Make sure the group was updated.
+
+        Group requeryGroup1 = groupStore.getGroup(deviceId1, groupId1);
+        assertThat(requeryGroup1.packets(), is(55L));
+        assertThat(requeryGroup1.bytes(), is(66L));
+
+    }
+
+    class TestDelegate implements GroupStoreDelegate {
+        private List<GroupEvent> eventsSeen = new LinkedList<>();
+        @Override
+        public void notify(GroupEvent event) {
+            eventsSeen.add(event);
+        }
+
+        public List<GroupEvent> eventsSeen() {
+            return eventsSeen;
+        }
+
+        public void resetEvents() {
+            eventsSeen.clear();
+        }
+    }
+
+    /**
+     * Tests group operation failed interface.
+     */
+    @Test
+    public void testGroupOperationFailed() {
+        TestDelegate delegate = new TestDelegate();
+        groupStore.setDelegate(delegate);
+        groupStore.deviceInitialAuditCompleted(deviceId1, true);
+        groupStore.deviceInitialAuditCompleted(deviceId2, true);
+
+        groupStore.storeGroupDescription(groupDescription1);
+        groupStore.storeGroupDescription(groupDescription2);
+
+        List<GroupEvent> eventsAfterAdds = delegate.eventsSeen();
+        assertThat(eventsAfterAdds, hasSize(2));
+        eventsAfterAdds.stream().forEach(event -> assertThat(event.type(), is(GroupEvent.Type.GROUP_ADD_REQUESTED)));
+        delegate.resetEvents();
+
+        GroupOperation opAdd =
+                GroupOperation.createAddGroupOperation(groupId1,
+                        GroupDescription.Type.INDIRECT,
+                        buckets);
+        groupStore.groupOperationFailed(deviceId1, opAdd);
+
+        List<GroupEvent> eventsAfterAddFailed = delegate.eventsSeen();
+        assertThat(eventsAfterAddFailed, hasSize(2));
+        assertThat(eventsAfterAddFailed.get(0).type(),
+                is(GroupEvent.Type.GROUP_ADD_FAILED));
+        assertThat(eventsAfterAddFailed.get(1).type(),
+                is(GroupEvent.Type.GROUP_REMOVED));
+        delegate.resetEvents();
+
+        GroupOperation opModify =
+                GroupOperation.createModifyGroupOperation(groupId2,
+                        GroupDescription.Type.INDIRECT,
+                        buckets);
+        groupStore.groupOperationFailed(deviceId2, opModify);
+        List<GroupEvent> eventsAfterModifyFailed = delegate.eventsSeen();
+        assertThat(eventsAfterModifyFailed, hasSize(1));
+        assertThat(eventsAfterModifyFailed.get(0).type(),
+                is(GroupEvent.Type.GROUP_UPDATE_FAILED));
+        delegate.resetEvents();
+
+        GroupOperation opDelete =
+                GroupOperation.createDeleteGroupOperation(groupId2,
+                        GroupDescription.Type.INDIRECT);
+        groupStore.groupOperationFailed(deviceId2, opDelete);
+        List<GroupEvent> eventsAfterDeleteFailed = delegate.eventsSeen();
+        assertThat(eventsAfterDeleteFailed, hasSize(1));
+        assertThat(eventsAfterDeleteFailed.get(0).type(),
+                is(GroupEvent.Type.GROUP_REMOVE_FAILED));
+        delegate.resetEvents();
+    }
+
+    /**
+     * Tests extraneous group operations.
+     */
+    @Test
+    public void testExtraneousOperations() {
+        ArrayList<Group> extraneous;
+        groupStore.deviceInitialAuditCompleted(deviceId1, true);
+
+        groupStore.storeGroupDescription(groupDescription1);
+        Group group1 = groupStore.getGroup(deviceId1, groupId1);
+
+        extraneous = Lists.newArrayList(groupStore.getExtraneousGroups(deviceId1));
+        assertThat(extraneous, hasSize(0));
+
+        groupStore.addOrUpdateExtraneousGroupEntry(group1);
+        extraneous = Lists.newArrayList(groupStore.getExtraneousGroups(deviceId1));
+        assertThat(extraneous, hasSize(1));
+
+        groupStore.removeExtraneousGroupEntry(group1);
+        extraneous = Lists.newArrayList(groupStore.getExtraneousGroups(deviceId1));
+        assertThat(extraneous, hasSize(0));
+    }
+
+    /**
+     * Tests updating of group descriptions.
+     */
+    @Test
+    public void testUpdateGroupDescription() {
+
+        GroupBuckets buckets =
+                new GroupBuckets(ImmutableList.of(failoverGroupBucket));
+
+        groupStore.deviceInitialAuditCompleted(deviceId1, true);
+        groupStore.storeGroupDescription(groupDescription1);
+
+        GroupKey newKey = new DefaultGroupKey("123".getBytes());
+        groupStore.updateGroupDescription(deviceId1,
+                groupKey1,
+                GroupStore.UpdateType.ADD,
+                buckets,
+                newKey);
+        Group group1 = groupStore.getGroup(deviceId1, groupId1);
+        assertThat(group1.appCookie(), is(newKey));
+        assertThat(group1.buckets().buckets(), hasSize(2));
+    }
+
+    @Test
+    public void testEqualsGroupStoreIdMapKey() {
+        DistributedGroupStore.GroupStoreIdMapKey key1 =
+            new DistributedGroupStore.GroupStoreIdMapKey(deviceId1, groupId1);
+        DistributedGroupStore.GroupStoreIdMapKey sameAsKey1 =
+                new DistributedGroupStore.GroupStoreIdMapKey(deviceId1, groupId1);
+        DistributedGroupStore.GroupStoreIdMapKey key2 =
+                new DistributedGroupStore.GroupStoreIdMapKey(deviceId2, groupId1);
+        DistributedGroupStore.GroupStoreIdMapKey key3 =
+                new DistributedGroupStore.GroupStoreIdMapKey(deviceId1, groupId2);
+
+        new EqualsTester()
+                .addEqualityGroup(key1, sameAsKey1)
+                .addEqualityGroup(key2)
+                .addEqualityGroup(key3)
+                .testEquals();
+    }
+
+    @Test
+    public void testEqualsGroupStoreKeyMapKey() {
+        DistributedGroupStore.GroupStoreKeyMapKey key1 =
+                new DistributedGroupStore.GroupStoreKeyMapKey(deviceId1, groupKey1);
+        DistributedGroupStore.GroupStoreKeyMapKey sameAsKey1 =
+                new DistributedGroupStore.GroupStoreKeyMapKey(deviceId1, groupKey1);
+        DistributedGroupStore.GroupStoreKeyMapKey key2 =
+                new DistributedGroupStore.GroupStoreKeyMapKey(deviceId2, groupKey1);
+        DistributedGroupStore.GroupStoreKeyMapKey key3 =
+                new DistributedGroupStore.GroupStoreKeyMapKey(deviceId1, groupKey2);
+
+        new EqualsTester()
+                .addEqualityGroup(key1, sameAsKey1)
+                .addEqualityGroup(key2)
+                .addEqualityGroup(key3)
+                .testEquals();
+    }
+
+    @Test
+    public void testEqualsGroupStoreMapKey() {
+        DistributedGroupStore.GroupStoreMapKey key1 =
+                new DistributedGroupStore.GroupStoreMapKey(deviceId1);
+        DistributedGroupStore.GroupStoreMapKey sameAsKey1 =
+                new DistributedGroupStore.GroupStoreMapKey(deviceId1);
+        DistributedGroupStore.GroupStoreMapKey key2 =
+                new DistributedGroupStore.GroupStoreMapKey(deviceId2);
+        DistributedGroupStore.GroupStoreMapKey key3 =
+                new DistributedGroupStore.GroupStoreMapKey(did("dev3"));
+
+        new EqualsTester()
+                .addEqualityGroup(key1, sameAsKey1)
+                .addEqualityGroup(key2)
+                .addEqualityGroup(key3)
+                .testEquals();
+    }
+}