/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.waveprotocol.box.server.frontend;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import junit.framework.TestCase;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.waveprotocol.box.common.DeltaSequence;
import org.waveprotocol.box.common.ExceptionalIterator;
import org.waveprotocol.box.common.comms.WaveClientRpc.WaveletVersion;
import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer;
import org.waveprotocol.box.server.frontend.ClientFrontend.OpenListener;
import org.waveprotocol.box.server.util.WaveletDataUtil;
import org.waveprotocol.box.server.waveserver.WaveServerException;
import org.waveprotocol.box.server.waveserver.WaveletProvider;
import org.waveprotocol.box.server.waveserver.WaveletProvider.SubmitRequestListener;
import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta;
import org.waveprotocol.wave.model.id.IdConstants;
import org.waveprotocol.wave.model.id.IdFilter;
import org.waveprotocol.wave.model.id.IdFilters;
import org.waveprotocol.wave.model.id.IdURIEncoderDecoder;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.testing.DeltaTestUtil;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.version.HashedVersionFactory;
import org.waveprotocol.wave.model.version.HashedVersionFactoryImpl;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import org.waveprotocol.wave.util.escapers.jvm.JavaUrlCodec;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Tests for {@link ClientFrontendImpl}.
*/
public class ClientFrontendImplTest extends TestCase {
private static final IdURIEncoderDecoder URI_CODEC =
new IdURIEncoderDecoder(new JavaUrlCodec());
private static final HashedVersionFactory HASH_FACTORY =
new HashedVersionFactoryImpl(URI_CODEC);
private static final WaveId WAVE_ID = WaveId.of("example.com", "waveId");
private static final WaveId INDEX_WAVE_ID = WaveId.of("indexdomain", "indexwave");
private static final WaveletId W1 =
WaveletId.of("example.com", IdConstants.CONVERSATION_ROOT_WAVELET);
private static final WaveletId W2 = WaveletId.of("example.com", "conv+2");
private static final WaveletName WN1 = WaveletName.of(WAVE_ID, W1);
private static final WaveletName WN2 = WaveletName.of(WAVE_ID, W2);
private static final ParticipantId USER = new ParticipantId("user@example.com");
private static final DeltaTestUtil UTIL = new DeltaTestUtil(USER);
private static final HashedVersion V0 = HASH_FACTORY.createVersionZero(WN1);
private static final HashedVersion V1 = HashedVersion.unsigned(1L);
private static final HashedVersion V2 = HashedVersion.unsigned(2L);
private static final TransformedWaveletDelta DELTA = TransformedWaveletDelta.cloneOperations(
USER, V1, 0, ImmutableList.of(UTIL.addParticipant(USER)));
private static final DeltaSequence DELTAS = DeltaSequence.of(DELTA);
private static final ProtocolWaveletDelta SERIALIZED_DELTA =
CoreWaveletOperationSerializer.serialize(DELTA);
private static final Collection<WaveletVersion> NO_KNOWN_WAVELETS =
Collections.<WaveletVersion>emptySet();
private ClientFrontendImpl clientFrontend;
private WaveletProvider waveletProvider;
@Override
protected void setUp() throws Exception {
super.setUp();
waveletProvider = mock(WaveletProvider.class);
when(waveletProvider.getWaveletIds(any(WaveId.class))).thenReturn(ImmutableSet.<WaveletId>of());
WaveletInfo waveletInfo = WaveletInfo.create(HASH_FACTORY, waveletProvider);
clientFrontend = new ClientFrontendImpl(waveletProvider, waveletInfo);
}
public void testCannotOpenWavesWhenNotLoggedIn() throws Exception {
OpenListener listener = mock(OpenListener.class);
clientFrontend.openRequest(null, WAVE_ID, IdFilters.ALL_IDS, NO_KNOWN_WAVELETS, listener);
verify(listener).onFailure("Not logged in");
CommittedWaveletSnapshot snapshot = provideWavelet(WN1);
clientFrontend.waveletUpdate(snapshot.snapshot, DELTAS);
Mockito.verifyNoMoreInteractions(listener);
}
public void testOpenEmptyWaveReceivesChannelIdAndMarker() {
OpenListener listener = openWave(IdFilters.ALL_IDS);
verifyChannelId(listener);
verifyMarker(listener, WAVE_ID);
}
public void testTwoSubscriptionsReceiveDifferentChannelIds() {
OpenListener listener1 = openWave(IdFilters.ALL_IDS);
String ch1 = verifyChannelId(listener1);
OpenListener listener2 = openWave(IdFilters.ALL_IDS);
String ch2 = verifyChannelId(listener2);
assertFalse(ch1.equals(ch2));
}
public void testOpenWaveRecievesSnapshotsThenMarker() throws Exception {
CommittedWaveletSnapshot snapshot1 = provideWavelet(WN1);
CommittedWaveletSnapshot snapshot2 = provideWavelet(WN2);
when(waveletProvider.getWaveletIds(WAVE_ID)).thenReturn(ImmutableSet.of(W1, W2));
when(waveletProvider.checkAccessPermission(WN1, USER)).thenReturn(true);
when(waveletProvider.checkAccessPermission(WN2, USER)).thenReturn(true);
OpenListener listener = openWave(IdFilters.ALL_IDS);
verify(listener).onUpdate(eq(WN1), eq(snapshot1), eq(DeltaSequence.empty()),
eq(V0), isNullMarker(), any(String.class));
verify(listener).onUpdate(eq(WN2), eq(snapshot2), eq(DeltaSequence.empty()),
eq(V0), isNullMarker(), any(String.class));
verifyMarker(listener, WAVE_ID);
}
/**
* Tests that a snapshot not matching the subscription filter is not received.
* @throws WaveServerException
*/
@SuppressWarnings("unchecked") // Mock container
public void testUnsubscribedSnapshotNotRecieved() throws Exception {
OpenListener listener = openWave(IdFilter.ofPrefixes("non-existing"));
verifyChannelId(listener);
verifyMarker(listener, WAVE_ID);
ReadableWaveletData wavelet = provideWavelet(WN1).snapshot;
clientFrontend.waveletUpdate(wavelet, DELTAS);
verify(listener, Mockito.never()).onUpdate(eq(WN1),
any(CommittedWaveletSnapshot.class), Matchers.anyList(),
any(HashedVersion.class), isNullMarker(), anyString());
}
/**
* Tests that we get deltas.
*/
public void testReceivedDeltasSentToClient() throws Exception {
CommittedWaveletSnapshot snapshot = provideWavelet(WN1);
when(waveletProvider.getWaveletIds(WAVE_ID)).thenReturn(ImmutableSet.of(W1));
when(waveletProvider.checkAccessPermission(WN1, USER)).thenReturn(true);
OpenListener listener = openWave(IdFilters.ALL_IDS);
verify(listener).onUpdate(eq(WN1), eq(snapshot), eq(DeltaSequence.empty()),
eq(V0), isNullMarker(), any(String.class));
verifyMarker(listener, WAVE_ID);
TransformedWaveletDelta delta = TransformedWaveletDelta.cloneOperations(USER, V2, 1234567890L,
Arrays.asList(UTIL.noOp()));
DeltaSequence deltas = DeltaSequence.of(delta);
clientFrontend.waveletUpdate(snapshot.snapshot, deltas);
verify(listener).onUpdate(eq(WN1), isNullSnapshot(), eq(deltas),
isNullVersion(), isNullMarker(), anyString());
}
/**
* Tests that submit requests are forwarded to the wavelet provider.
*/
public void testSubmitForwardedToWaveletProvider() {
OpenListener openListener = openWave(IdFilters.ALL_IDS);
String channelId = verifyChannelId(openListener);
SubmitRequestListener submitListener = mock(SubmitRequestListener.class);
clientFrontend.submitRequest(USER, WN1, SERIALIZED_DELTA, channelId, submitListener);
verify(waveletProvider).submitRequest(eq(WN1), eq(SERIALIZED_DELTA),
any(SubmitRequestListener.class));
verifyZeroInteractions(submitListener);
}
public void testCannotSubmitAsDifferentUser() {
ParticipantId otherParticipant = new ParticipantId("another@example.com");
OpenListener openListener = openWave(IdFilters.ALL_IDS);
String channelId = verifyChannelId(openListener);
SubmitRequestListener submitListener = mock(SubmitRequestListener.class);
clientFrontend.submitRequest(otherParticipant, WN1, SERIALIZED_DELTA, channelId,
submitListener);
verify(submitListener).onFailure(anyString());
verify(submitListener, never()).onSuccess(anyInt(), (HashedVersion) any(), anyLong());
}
/**
* Tests that if we open the index wave, we don't get updates from the
* original wave if they contain no interesting operations (add/remove
* participant or text).
*/
public void testUninterestingDeltasDontUpdateIndex() throws WaveServerException {
provideWaves(Collections.<WaveId> emptySet());
OpenListener listener = openWave(INDEX_WAVE_ID, IdFilters.ALL_IDS);
verifyChannelId(listener);
verifyMarker(listener, INDEX_WAVE_ID);
HashedVersion v1 = HashedVersion.unsigned(1L);
TransformedWaveletDelta delta = makeDelta(USER, v1, 0L, UTIL.noOp());
DeltaSequence deltas = DeltaSequence.of(delta);
WaveletData wavelet = WaveletDataUtil.createEmptyWavelet(WN1, USER, V0, 1234567890L);
clientFrontend.waveletUpdate(wavelet, deltas);
WaveletName dummyWaveletName = ClientFrontendImpl.createDummyWaveletName(INDEX_WAVE_ID);
verify(listener, Mockito.never()).onUpdate(eq(dummyWaveletName),
any(CommittedWaveletSnapshot.class),
isDeltasStartingAt(0),
any(HashedVersion.class), isNullMarker(), anyString());
}
/**
* Opens a wave and returns a mock listener.
*/
private ClientFrontend.OpenListener openWave(WaveId waveId, IdFilter filter) {
OpenListener openListener = mock(OpenListener.class);
clientFrontend.openRequest(USER, waveId, filter, NO_KNOWN_WAVELETS, openListener);
return openListener;
}
private ClientFrontend.OpenListener openWave(IdFilter filter) {
return openWave(WAVE_ID, filter);
}
private TransformedWaveletDelta makeDelta(ParticipantId author, HashedVersion endVersion,
long timestamp, WaveletOperation... operations) {
return TransformedWaveletDelta.cloneOperations(author, endVersion, timestamp,
Arrays.asList(operations));
}
/**
* Initialises the wavelet provider to provide a collection of waves.
*/
private void provideWaves(Collection<WaveId> waves) throws WaveServerException {
when(waveletProvider.getWaveIds()).thenReturn(
ExceptionalIterator.FromIterator.<WaveId, WaveServerException> create(
waves.iterator()));
}
/**
* Prepares the wavelet provider to provide a new wavelet.
*
* @param name new wavelet name
* @return the new wavelet snapshot
*/
private CommittedWaveletSnapshot provideWavelet(WaveletName name) throws WaveServerException,
OperationException {
WaveletData wavelet = WaveletDataUtil.createEmptyWavelet(name, USER, V0, 1234567890L);
DELTA.get(0).apply(wavelet);
CommittedWaveletSnapshot snapshot = new CommittedWaveletSnapshot(wavelet, V0);
when(waveletProvider.getSnapshot(name)).thenReturn(snapshot);
when(waveletProvider.getWaveletIds(name.waveId)).thenReturn(ImmutableSet.of(name.waveletId));
return snapshot;
}
/**
* Verifies that the listener received a channel id.
*
* @return the channel id received
*/
private static String verifyChannelId(OpenListener listener) {
ArgumentCaptor<String> channelIdCaptor = ArgumentCaptor.forClass(String.class);
verify(listener).onUpdate(any(WaveletName.class), isNullSnapshot(), eq(DeltaSequence.empty()),
isNullVersion(), isNullMarker(), channelIdCaptor.capture());
return channelIdCaptor.getValue();
}
/**
* Verifies that the listener received a marker.
*/
private static void verifyMarker(OpenListener listener, WaveId waveId) {
ArgumentCaptor<WaveletName> waveletNameCaptor = ArgumentCaptor.forClass(WaveletName.class);
verify(listener).onUpdate(waveletNameCaptor.capture(), isNullSnapshot(),
eq(DeltaSequence.empty()), isNullVersion(), eq(true), (String) Mockito.isNull());
assertEquals(waveId, waveletNameCaptor.getValue().waveId);
}
private static CommittedWaveletSnapshot isNullSnapshot() {
return (CommittedWaveletSnapshot) Mockito.isNull();
}
private static HashedVersion isNullVersion() {
return (HashedVersion) Mockito.isNull();
}
private static Boolean isNullMarker() {
return (Boolean) Mockito.isNull();
}
private static List<TransformedWaveletDelta> isDeltasStartingAt(final long version) {
return argThat(new ArgumentMatcher<List<TransformedWaveletDelta>>() {
@Override
public boolean matches(Object sequence) {
if (sequence != null) {
DeltaSequence s = (DeltaSequence) sequence;
return (s.size() > 0) && (s.getStartVersion() == version);
}
return false;
}
});
}
}