/** * Copyright 2009 Google Inc. * * 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.waveprotocol.wave.examples.fedone.waveserver; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.inject.internal.Nullable; import junit.framework.TestCase; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.waveprotocol.wave.examples.fedone.common.CommonConstants.INDEX_WAVE_ID; import org.waveprotocol.wave.examples.fedone.common.HashedVersion; import static org.waveprotocol.wave.examples.fedone.common.HashedVersion.unsigned; import org.waveprotocol.wave.examples.fedone.common.WaveletOperationSerializer; import static org.waveprotocol.wave.examples.fedone.common.WaveletOperationSerializer.serialize; import org.waveprotocol.wave.examples.fedone.waveserver.ClientFrontend.OpenListener; import static org.waveprotocol.wave.examples.fedone.waveserver.ClientFrontendImpl.DIGEST_AUTHOR; import static org.waveprotocol.wave.examples.fedone.waveserver.ClientFrontendImpl.DIGEST_DOCUMENT_ID; import static org.waveprotocol.wave.examples.fedone.waveserver.ClientFrontendImpl.createUnsignedDeltas; import static org.waveprotocol.wave.examples.fedone.waveserver.ClientFrontendImpl.indexWaveletNameFor; import static org.waveprotocol.wave.examples.fedone.waveserver.ClientFrontendImpl.waveletNameForIndexWavelet; import org.waveprotocol.wave.federation.FederationErrors; import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion; import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta; import org.waveprotocol.wave.model.document.operation.BufferedDocOp; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder; import static org.waveprotocol.wave.model.id.IdConstants.CONVERSATION_ROOT_WAVELET; 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.wave.AddParticipant; import org.waveprotocol.wave.model.operation.wave.NoOp; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; import org.waveprotocol.wave.model.operation.wave.WaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletDocumentOperation; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.waveserver.SubmitResultListener; import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; /** * */ public class ClientFrontendImplTest extends TestCase { private static final WaveId WAVE_ID = new WaveId("domain", "waveId"); private static final WaveletId WAVELET_ID = new WaveletId("domain", CONVERSATION_ROOT_WAVELET); private static final WaveletName WAVELET_NAME = WaveletName.of(WAVE_ID, WAVELET_ID); private static final WaveletName INDEX_WAVELET_NAME = WaveletName.of(INDEX_WAVE_ID, new WaveletId("domain", "waveId")); private static final ParticipantId USER = new ParticipantId("user@host.com"); // waveletIdPrefixes to use when subscribing to all wavelets private static final Set<String> ALL_WAVELETS = ImmutableSet.of(""); private static final HashedVersion VERSION_0 = HashedVersion.versionZero(WAVELET_NAME); private static final ProtocolWaveletDelta DELTA = serialize(new WaveletDelta(USER, ImmutableList.of(new AddParticipant(USER))), VERSION_0); private static final DeltaSequence DELTAS = new DeltaSequence(ImmutableList.of(DELTA), serialize(HashedVersion.unsigned(1L))); private static final Map<String, BufferedDocOp> DOCUMENT_STATE = ImmutableMap.of(); private ClientFrontendImpl clientFrontend; private WaveletProvider waveletProvider; @Override protected void setUp() throws Exception { super.setUp(); waveletProvider = mock(WaveletProvider.class); this.clientFrontend = new ClientFrontendImpl(waveletProvider); verify(waveletProvider).setListener((WaveletListener) isNotNull()); } /** * Test that a wavelet name can be converted to the corresponding index * wavelet name if and only if the wavelet's domain equals the wave's domain * and the wavelet's ID is CONVERSATION_ROOT_WAVELET. */ public void testIndexWaveletNameConversion() { assertEquals(INDEX_WAVELET_NAME, indexWaveletNameFor(WAVELET_NAME)); assertEquals(WAVELET_NAME, waveletNameForIndexWavelet(INDEX_WAVELET_NAME)); WaveletName invalid = WaveletName.of(WAVE_ID, new WaveletId("otherdomain", CONVERSATION_ROOT_WAVELET)); try { indexWaveletNameFor(invalid); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { //pass } invalid = WaveletName.of(WAVE_ID, new WaveletId("domain", "otherId")); try { indexWaveletNameFor(invalid); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { //pass } invalid = WaveletName.of(INDEX_WAVE_ID, new WaveletId(INDEX_WAVE_ID.getDomain(), "other-wavelet-id")); try { indexWaveletNameFor(invalid); fail("index wave wavelets shouldn't be convertable to index wave"); } catch (IllegalArgumentException expected) { // pass } } /** * Tests that openRequest() yields no deltas if none are received via * waveletUpdate(). */ public void testInitialOpenRequestYieldsNothing() { OpenListener listener = mock(OpenListener.class); clientFrontend.openRequest(USER, WAVE_ID, ALL_WAVELETS, Integer.MAX_VALUE, false, listener); verifyZeroInteractions(listener); clientFrontend.openRequest(USER, INDEX_WAVE_ID, ALL_WAVELETS, Integer.MAX_VALUE, false, listener); // we don't expect any invocations on the listener verifyZeroInteractions(listener); } /** * Tests that if our subscription doesn't involve a matching waveletIdPrefix, * deltas arriving via waveletUpdate(), aren't forwarded to the listener. */ public void testDeltasArentPropagatedIfNotSubscribedToWavelet() { OpenListener listener = mock(OpenListener.class); clientFrontend.openRequest(USER, WAVE_ID, ImmutableSet.of("nonexisting-wavelet"), Integer.MAX_VALUE, false, listener); clientFrontend.waveletUpdate( WAVELET_NAME, DELTAS, DELTAS.getEndVersion(), DOCUMENT_STATE); verifyZeroInteractions(listener); } /** * Test that clientFrontend.submitRequest() triggers * waveletProvider.submitRequest(). */ public void testSubmitGetsForwardedToWaveletProvider() { SubmitResultListener listener = mock(SubmitResultListener.class); clientFrontend.submitRequest(WAVELET_NAME, DELTA, listener); verify(waveletProvider).submitRequest( eq(WAVELET_NAME), eq(DELTA), (SubmitResultListener) isNotNull()); verifyZeroInteractions(listener); } /** * Tests that an attempt to submit a delta to the index wave immediately * yields an expected failure message. */ public void testCannotSubmitToIndexWave() { SubmitResultListener listener = mock(SubmitResultListener.class); clientFrontend.submitRequest(INDEX_WAVELET_NAME, DELTA, listener); verify(listener).onFailure(FederationErrors.badRequest("Wavelet " + INDEX_WAVELET_NAME + " is readonly")); } /** * Tests that we get deltas if they arrive some time after we've opened * a subscription. */ public void testOpenThenSendDeltas() { OpenListener listener = mock(OpenListener.class); clientFrontend.openRequest(USER, WAVE_ID, ALL_WAVELETS, Integer.MAX_VALUE, false, listener); clientFrontend.participantUpdate(WAVELET_NAME, USER, DELTAS, true, false, "", ""); verify(listener).onUpdate(WAVELET_NAME, null, DELTAS, DELTAS.getEndVersion(), null); } /** * 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 testOpenIndexThenSendDeltasNotOfInterest() { OpenListener listener = mock(OpenListener.class); clientFrontend.openRequest(USER, INDEX_WAVE_ID, ALL_WAVELETS, Integer.MAX_VALUE, false, listener); List<? extends WaveletOperation> ops = ImmutableList.of(NoOp.INSTANCE); WaveletDelta delta = new WaveletDelta(USER, ops); DeltaSequence deltas = new DeltaSequence( ImmutableList.of(serialize(delta, VERSION_0)), serialize(HashedVersion.unsigned(1L))); clientFrontend.participantUpdate(WAVELET_NAME, USER, deltas, true, false, "", ""); verifyZeroInteractions(listener); } /** * An OpenListener that expects only onUpdate() calls and publishes the * values passed in. */ static final class UpdateListener implements OpenListener { WaveletName waveletName = null; DeltaSequence deltas = null; ProtocolHashedVersion endVersion = null; @Override public void onFailure(String errorMessage) { fail("unexpected"); } @Override public void onUpdate(WaveletName wn, @Nullable WaveletSnapshotAndVersions snapshot, List<ProtocolWaveletDelta> newDeltas, @Nullable ProtocolHashedVersion endVersion, @Nullable ProtocolHashedVersion committedVersion) { assertNull(snapshot); assertNull(this.waveletName); // make sure we're not called twice assertNotNull(endVersion); // TODO(arb): check the committedVersion field correctly this.waveletName = wn; this.deltas = new DeltaSequence(newDeltas, endVersion); this.endVersion = endVersion; } void clear() { assertNotNull(this.waveletName); this.waveletName = null; this.deltas = null; this.endVersion = null; } } private ProtocolWaveletDelta makeDelta(ParticipantId author, HashedVersion startVersion, WaveletOperation...operations) { WaveletDelta delta = new WaveletDelta(author, ImmutableList.of(operations)); return serialize(delta, startVersion); } private void waveletUpdate(HashedVersion startVersion, Map<String, BufferedDocOp> documentState, WaveletOperation... operations) { ProtocolWaveletDelta delta = makeDelta(USER, startVersion, operations); DeltaSequence deltas = createUnsignedDeltas(ImmutableList.of(delta)); clientFrontend.waveletUpdate( WAVELET_NAME, deltas, deltas.getEndVersion(), documentState); } private BufferedDocOp makeAppend(int retain, String text) { DocOpBuilder builder = new DocOpBuilder(); if (retain > 0) { builder.retain(retain); } builder.characters(text); return builder.build(); } private WaveletDocumentOperation makeAppendOp(String documentId, int retain, String text) { return new WaveletDocumentOperation(documentId, makeAppend(retain, text)); } /** * Tests that a delta involving an addParticipant and a characters op * gets pushed through to the index wave as deltas that just summarise * the changes to the digest text and the participants, ignoring any * text from the first \n onwards. */ public void testOpenIndexThenSendInterestingDeltas() { UpdateListener listener = new UpdateListener(); clientFrontend.openRequest(USER, INDEX_WAVE_ID, ALL_WAVELETS, Integer.MAX_VALUE, false, listener); waveletUpdate(VERSION_0, ImmutableMap.of("default", makeAppend(0, "Hello, world\nignored text")), new AddParticipant(USER), NoOp.INSTANCE ); assertEquals(INDEX_WAVELET_NAME, listener.waveletName); WaveletOperation helloWorldOp = makeAppendOp(DIGEST_DOCUMENT_ID, 0, "Hello, world"); DeltaSequence expectedDeltas = createUnsignedDeltas(ImmutableList.of( makeDelta(DIGEST_AUTHOR, unsigned(0), helloWorldOp), makeDelta(USER, unsigned(1L), new AddParticipant(USER)))); assertEquals(expectedDeltas, listener.deltas); } /** * Tests that when a subscription is added later than version 0, that listener * gets all previous deltas immediately, and both existing listeners and the * new listener get subsequent updates. */ public void testOpenAfterVersionZero() { UpdateListener oldListener = new UpdateListener(); clientFrontend.openRequest(USER, WAVE_ID, ALL_WAVELETS, Integer.MAX_VALUE, false, oldListener); Map<String, BufferedDocOp> documentState = Maps.newHashMap(); BufferedDocOp addTextOp = makeAppend(0, "Hello, world"); waveletUpdate(VERSION_0, documentState, new AddParticipant(USER), new WaveletDocumentOperation("docId", addTextOp)); documentState.put("docId", addTextOp); assertTrue(!oldListener.deltas.isEmpty()); // TODO(tobiast): Let getHistory() return a DeltaSequence, and simplify this test NavigableSet<ProtocolWaveletDelta> expectedDeltas = new TreeSet<ProtocolWaveletDelta>( WaveletContainerImpl.transformedDeltaComparator); expectedDeltas.addAll(ImmutableList.copyOf(oldListener.deltas)); ProtocolHashedVersion startVersion = serialize(HashedVersion.versionZero(WAVELET_NAME)); when(waveletProvider.getHistory(WAVELET_NAME, startVersion, oldListener.endVersion)).thenReturn(expectedDeltas); UpdateListener newListener = new UpdateListener(); clientFrontend.openRequest(USER, WAVE_ID, ALL_WAVELETS, Integer.MAX_VALUE, false, newListener); // Upon subscription, newListener immediately got all the previous deltas assertEquals(oldListener.deltas, newListener.deltas); assertEquals(oldListener.endVersion, newListener.endVersion); when(waveletProvider.getHistory(WAVELET_NAME, serialize(HashedVersion.versionZero(WAVELET_NAME)), oldListener.endVersion)).thenReturn( expectedDeltas ); HashedVersion version = WaveletOperationSerializer.deserialize(oldListener.endVersion); oldListener.clear(); newListener.clear(); waveletUpdate(version, documentState, new AddParticipant(new ParticipantId("another-user")), NoOp.INSTANCE, new RemoveParticipant(USER)); // Subsequent deltas go to both listeners assertEquals(oldListener.deltas, newListener.deltas); assertEquals(oldListener.endVersion, newListener.endVersion); } }