package com.opengamma.web.analytics.push;
import static org.mockito.Matchers.anyCollection;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import java.util.Collection;
import org.mockito.ArgumentMatcher;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.threeten.bp.Instant;
import com.google.common.collect.Lists;
import com.opengamma.core.change.ChangeEvent;
import com.opengamma.core.change.ChangeType;
import com.opengamma.id.UniqueId;
import com.opengamma.util.test.TestGroup;
import com.opengamma.web.analytics.rest.MasterType;
/**
* Test the subscription mechanism of {@link ClientConnection}. Subscriptions are added for REST URLs. When any
* event fires for a particular URL the URL is considered dirty and the client should re-request it. Therefore there
* is no need to fire any more events for that URL until the client resubscribes. So when any event fires for
* a URL all other subscriptions for that URL should be cleared.
*/
@Test(groups = TestGroup.UNIT)
public class ClientConnectionTest {
private static final String USER_ID = "USER_ID";
private static final String CLIENT_ID = "CLIENT_ID";
private static final String TEST_URL1 = "TEST_URL1";
private static final String TEST_URL2 = "TEST_URL2";
private static final UniqueId _uid1 = UniqueId.of("Tst", "101");
private static final UniqueId _uid2 = UniqueId.of("Tst", "102");
private UpdateListener _listener;
private ClientConnection _connection;
@BeforeMethod
public void setUp() throws Exception {
_listener = mock(UpdateListener.class);
ConnectionTimeoutTask timeoutTask = mock(ConnectionTimeoutTask.class);
_connection = new ClientConnection(USER_ID, CLIENT_ID, _listener, timeoutTask);
}
/**
* Checks that a listener to changes to an entity gets fired once and once only.
*/
@Test
public void subscribeToEntityEvent() {
ChangeEvent event = new ChangeEvent(ChangeType.CHANGED, _uid1.getObjectId(), null, null, Instant.now());
// send an event and make sure the _listener doesn't receive it before subscription
_connection.entityChanged(event);
verify(_listener, never()).itemsUpdated(anyCollection());
// subscribe and verify the _listener receives the event
_connection.subscribe(_uid1, TEST_URL1);
_connection.entityChanged(event);
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1));
// send the event again and make sure the subscription has been cancelled
_connection.entityChanged(event);
verifyNoMoreInteractions(_listener);
}
@Test
public void subscribeToMaster() {
// send an event before subscription and check the listener doesn't receive it
_connection.masterChanged(MasterType.PORTFOLIO);
verify(_listener, never()).itemsUpdated(anyCollection());
// subscribe and verify the listener receives the event
_connection.subscribe(MasterType.PORTFOLIO, TEST_URL1);
_connection.masterChanged(MasterType.PORTFOLIO);
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1));
// send the event again and make sure the subscription has been cancelled
_connection.masterChanged(MasterType.PORTFOLIO);
verifyNoMoreInteractions(_listener);
}
/**
* Multiple URLs subscribing to the same entity should produce multiple updates when the entity changes.
*/
@Test
public void multipleSubscriptionsToEntity() {
ChangeEvent event = new ChangeEvent(ChangeType.CHANGED, _uid1.getObjectId(), null, null, Instant.now());
// subscribe and verify the listener receives the event
_connection.subscribe(_uid1, TEST_URL1);
_connection.subscribe(_uid1, TEST_URL2);
_connection.entityChanged(event);
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1, TEST_URL2));
// send the event again and make sure the subscription has been cancelled
_connection.entityChanged(event);
verifyNoMoreInteractions(_listener);
}
/**
* Multiple URLs subscribing to the same master should produce multiple updates when the master fires a change event.
*/
@Test
public void multipleSubscriptionsToMaster() {
// subscribe and verify the listener receives the event
_connection.subscribe(MasterType.PORTFOLIO, TEST_URL1);
_connection.subscribe(MasterType.PORTFOLIO, TEST_URL2);
_connection.masterChanged(MasterType.PORTFOLIO);
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1, TEST_URL2));
// send the event again and make sure the subscription has been cancelled
_connection.masterChanged(MasterType.PORTFOLIO);
verifyNoMoreInteractions(_listener);
}
/**
* A particular URL should only have one event delivered no matter how many subscriptions it has. After the
* first event no more should be triggered until a new subscription is set up.
*/
@Test
public void multipeEntitySubscriptionsForSameUrl() {
ChangeEvent event1 = new ChangeEvent(ChangeType.CHANGED, _uid1.getObjectId(), null, null, Instant.now());
ChangeEvent event2 = new ChangeEvent(ChangeType.CHANGED, _uid2.getObjectId(), null, null, Instant.now());
// subscribe and verify the listener receives the event
_connection.subscribe(_uid1, TEST_URL1);
_connection.subscribe(_uid2, TEST_URL1);
_connection.entityChanged(event1);
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1));
// send an event for the other subscription and check nothing is delivered to the listener
_connection.entityChanged(event2);
verifyNoMoreInteractions(_listener);
}
/**
* A particular URL should only have one event delivered no matter how many subscriptions it has. After the
* first event no more should be triggered until a new subscription is set up.
*/
@Test
public void multipeMasterSubscriptionsForSameUrl() {
// subscribe and verify the listener receives the event
_connection.subscribe(MasterType.PORTFOLIO, TEST_URL1);
_connection.subscribe(MasterType.POSITION, TEST_URL1);
_connection.masterChanged(MasterType.PORTFOLIO);
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1));
// send an event for the other subscribed master type and make sure the subscription has been cancelled
_connection.masterChanged(MasterType.POSITION);
verifyNoMoreInteractions(_listener);
}
/**
* If there is an entity and master subscription for the same URL then only one event should fire even
* if both the entity and master are changed.
*/
@Test
public void masterAndEntitySubscriptionForSameUrlMasterChangesFirst() {
_connection.subscribe(_uid1, TEST_URL1);
_connection.subscribe(MasterType.PORTFOLIO, TEST_URL1);
_connection.masterChanged(MasterType.PORTFOLIO);
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1));
_connection.entityChanged(new ChangeEvent(ChangeType.CHANGED, _uid1.getObjectId(), null, null, Instant.now()));
verifyNoMoreInteractions(_listener);
}
/**
* If there is an entity and master subscription for the same URL then only one event should fire even
* if both the entity and master are changed.
*/
@Test
public void masterAndEntitySubscriptionForSameUrlEntityChangesFirst() {
_connection.subscribe(_uid1, TEST_URL1);
_connection.subscribe(MasterType.PORTFOLIO, TEST_URL1);
_connection.entityChanged(new ChangeEvent(ChangeType.CHANGED, _uid1.getObjectId(), null, null, Instant.now()));
verify(_listener).itemsUpdated(CollectionMatcher.collectionOf(TEST_URL1));
_connection.masterChanged(MasterType.PORTFOLIO);
verifyNoMoreInteractions(_listener);
}
/**
* Checks that nothing happens when a subscription is set up for an entity and a different entity changes.
*/
@Test
public void subscriptionForDifferentEntity() {
_connection.subscribe(_uid1, TEST_URL1);
_connection.entityChanged(new ChangeEvent(ChangeType.CHANGED, _uid2.getObjectId(), null, null, Instant.now()));
verifyZeroInteractions(_listener);
}
/**
* Checks that nothing happens when a subscription is set up for a master and a different master changes.
*/
@Test
public void subscriptionForDifferentMaster() {
_connection.subscribe(MasterType.PORTFOLIO, TEST_URL1);
_connection.masterChanged(MasterType.POSITION);
verifyZeroInteractions(_listener);
}
}
/**
* <p>Matcher for checking an argument to a mock is a collection containing all of the specified items and nothing else.
* The order of the elements isn't important.</p>
* <em>I'm surprised this isn't already available in Mockito, although maybe it's there and I just can't find it.</em>
* @param <T> The type of items in the collection
*/
@SuppressWarnings("unchecked")
class CollectionMatcher<T> extends ArgumentMatcher<Collection<T>> {
private final Collection<T> _expected;
public CollectionMatcher(T... expectedItems) {
_expected = Lists.newArrayList(expectedItems);
}
@Override
public boolean matches(Object o) {
Collection<T> result = Lists.newArrayList((Collection<T>) o);
for (T expectedItem : _expected) {
// every member of the expected collection must be present in the result
if (!result.remove(expectedItem)) {
return false;
}
}
// if all members of the expected collection were in the result and after removing them the result is empty
// the collections must have contained exactly the same elements
return result.isEmpty();
}
static <T> Collection<T> collectionOf(T... items) {
return argThat(new CollectionMatcher<T>(items));
}
}