Package org.waveprotocol.wave.model.conversation

Source Code of org.waveprotocol.wave.model.conversation.WaveletBasedConversationTest

/**
* 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.wave.model.conversation;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.waveprotocol.wave.model.testing.ExtraAsserts.assertStructureEquivalent;

import org.waveprotocol.wave.model.conversation.Conversation.Anchor;
import org.waveprotocol.wave.model.conversation.testing.BlipTestUtils;
import org.waveprotocol.wave.model.document.MutableDocument;
import org.waveprotocol.wave.model.document.ObservableDocument;
import org.waveprotocol.wave.model.document.ObservableMutableDocument;
import org.waveprotocol.wave.model.document.MutableDocument.Action;
import org.waveprotocol.wave.model.document.util.DefaultDocumentEventRouter;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
import org.waveprotocol.wave.model.id.IdGenerator;
import org.waveprotocol.wave.model.testing.FakeIdGenerator;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.wave.Blip;
import org.waveprotocol.wave.model.wave.Wavelet;
import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView;

import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;

/**
* Conversation tests for the wavelet-based conversation.
*
* These tests mostly check the conversation interprets and generates the
* correct manifest schema.
*
* @author anorth@google.com (Alex North)
*/

public class WaveletBasedConversationTest extends ConversationTestBase {

  private IdGenerator idGenerator;
  private ObservableWaveView waveView;
  private WaveBasedConversationView conversationView;
  private WaveletBasedConversation target;
  private ObservableMutableDocument<?, ?, ?> manifestDoc;

  private WaveletBasedConversationBlip.Listener blipListener;
  private WaveletBasedConversationThread.Listener threadListener;

  @Override
  protected void setUp() throws Exception {
    idGenerator = FakeIdGenerator.create();
    waveView = ConversationTestUtils.createWaveView(idGenerator);
    conversationView = WaveBasedConversationView.create(waveView, idGenerator);
    target = makeConversation();
    manifestDoc = WaveletBasedConversation.getManifestDocument(target.getWavelet());

    blipListener = mock(WaveletBasedConversationBlip.Listener.class);
    threadListener = mock(WaveletBasedConversationThread.Listener.class);

    super.setUp();
  }

  @Override
  protected void tearDown() throws Exception {
    super.tearDown();
  }

  @Override
  protected WaveletBasedConversation makeConversation() {
    return conversationView.createConversation();
  }

  @Override
  protected WaveletBasedConversation mirrorConversation(ObservableConversation toMirror) {
    WaveletBasedConversation backer = (WaveletBasedConversation) toMirror;
    ObservableDocument backerManifestDoc = WaveletBasedConversation.getManifestDocument(
        backer.getWavelet());
    ObservableManifest manifest = backerManifestDoc.with(
        new ObservableMutableDocument.Method<ObservableManifest>() {
          @Override
          public <N, E extends N, T extends N> ObservableManifest exec(
              ObservableMutableDocument<N, E, T> doc) {
            E top = DocHelper.expectAndGetFirstTopLevelElement(doc,
                DocumentBasedManifest.MANIFEST_TOP_TAG);
            return DocumentBasedManifest.createOnExisting(DefaultDocumentEventRouter.create(doc),
                top);
          }
        });
    return WaveletBasedConversation.create(conversationView, backer.getWavelet(), manifest,
        idGenerator);
  }

  @Override
  protected void assertBlipValid(ConversationBlip blip) {
    ((WaveletBasedConversationBlip)blip).checkIsUsable();
  }

  @Override
  protected void assertBlipInvalid(ConversationBlip blip) {
    try {
      ((WaveletBasedConversationBlip)blip).checkIsUsable();
      fail("Expected blip to be invalid");
    } catch (IllegalStateException expected) {
    }
  }

  @Override
  protected void assertThreadInvalid(ConversationThread thread) {
    try {
      ((WaveletBasedConversationThread)thread).checkIsUsable();
      fail("Expected thread to be invalid");
    } catch (IllegalStateException expected) {
    }
  }

  @Override
  protected void assertThreadValid(ConversationThread thread) {
    ((WaveletBasedConversationThread)thread).checkIsUsable();
  }

  //
  // Initialization
  //

  /**
   * Tests that an empty wavelet is recognised as having no conversation
   * structure.
   */
  public void testNewWaveletHasNoConversation() {
    Wavelet wavelet = waveView.createWavelet();
    assertFalse(WaveletBasedConversation.waveletHasConversation(wavelet));
  }

  /**
   * Tests that a wavelet with an initialised manifest is recognised as having
   * conversation structure.
   */
  public void testWaveletWithManifestHasConversation() {
    assertTrue(WaveletBasedConversation.waveletHasConversation(target.getWavelet()));
  }

  /**
   * Tests that makeWaveletConversational does so.
   */
  public void testHackMakeWaveletConversationalMakesConversation() {
    Wavelet wavelet = waveView.createWavelet();
    WaveletBasedConversation.makeWaveletConversational(wavelet);
    assertTrue(WaveletBasedConversation.waveletHasConversation(wavelet));
  }

  /**
   * Tests that WaveletBasedConversation does not die if an additional
   * conversation element is added to the manifest dynamically.
   */
  public void testDynamicAdditionOfExtraConversationElementDoesNotFail() {
    // target is currently listening to the manifest, so we only need to poke it.
    manifestDoc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        doc.createChildElement(doc.getDocumentElement(), DocumentBasedManifest.MANIFEST_TOP_TAG,
            Collections.<String, String>emptyMap());
      }
    });
  }

  /**
   * Tests that WaveletBasedConversation does not die if it is loaded on a
   * manifest document with multiple conversation elements.
   */
  public void testMultipleConversationElementsDoesNotPreventLoad() {
    manifestDoc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        doc.createChildElement(doc.getDocumentElement(), DocumentBasedManifest.MANIFEST_TOP_TAG,
            Collections.<String, String>emptyMap());
      }
    });

    // Manifest now has multiple elements.  Re-load conversation view.
    conversationView = WaveBasedConversationView.create(waveView, idGenerator);
  }

  //
  // Anchors
  //

  /**
   * Tests that an empty manifest document means no anchor.
   */
  public void testEmptyManifestHasNoAnchor() {
    assertFalse(target.hasAnchor());
  }

  /**
   * Tests that a wavelet with a manifest but no anchor attributes is not
   * anchored.
   */
  public void testMissingAnchorAttributesMeansNoAnchor() {
    setManifestAttribute(manifestDoc, "sort", "a-value");
    assertFalse(target.hasAnchor());
  }

  /**
   * Tests that setting an anchor updates the conversation manifest correctly.
   */
  public void testSetAnchorUpdatesManifest() {
    // Anchor target(wavelet1) in alternate (wavelet2).
    WaveletBasedConversation conversation2 = makeConversation();
    populate(conversation2);
    ConversationBlip firstBlip = getFirstBlip(conversation2);
    Anchor anchor = conversation2.createAnchor(firstBlip);
    target.setAnchor(anchor);
    assertEquals(WaveletBasedConversation.idFor(conversation2.getWavelet().getId()),
        getManifestAttribute(manifestDoc, "anchorWavelet"));
    assertEquals(firstBlip.getId(), getManifestAttribute(manifestDoc, "anchorBlip"));
  }

  /**
   * Tests that setting a null anchor updates the manifest and makes the wavelet
   * un-anchored.
   */
  public void testClearAnchorClearsManifest() {
    WaveletBasedConversation conversation2 = makeConversation();
    populate(conversation2);
    Anchor anchor = conversation2.createAnchor(getFirstBlip(conversation2));
    target.setAnchor(anchor);
    target.setAnchor(null);
    assertNull(getManifestAttribute(manifestDoc, "anchorWavelet"));
    assertNull(getManifestAttribute(manifestDoc, "anchorBlip"));
  }

  //
  // Threads and blips.
  //

  public void testAbstractBlipIdMatchesConcreteBlipId() {
    populate(target);
    WaveletBasedConversationBlip convBlip = target.getRootThread().getFirstBlip();
    assertEquals(convBlip.getId(), convBlip.getBlip().getId());
  }

  /**
   * Tests that the wavelet-based conversation reads meta-data from the
   * underlying wavelet structures. This test will go away when meta-data is
   * stored in the conversation documents.
   */
  public void testConversationBlipMetadataMatchesWavelet() {
    populate(target);
    ConversationBlip convBlip = target.getRootThread().getFirstBlip();
    Wavelet wavelet = target.getWavelet();
    Blip blip = wavelet.getBlip(convBlip.getId());

    assertEquals(blip.getId(), convBlip.getId());
    assertEquals(blip.getLastModifiedVersion().longValue(), convBlip.getLastModifiedVersion());
    assertEquals(blip.getLastModifiedTime().longValue(), convBlip.getLastModifiedTime());
    assertEquals(blip.getAuthorId(), convBlip.getAuthorId());
    assertEquals(blip.getContributorIds(), convBlip.getContributorIds());
  }

  public void testAppendBlipsInRootThreadUpdatesManifest() {
    WaveletBasedConversationBlip first = target.getRootThread().appendBlip();
    assertManifestXml("<blip id=\"" + first.getId() + "\"></blip>");

    WaveletBasedConversationBlip second = target.getRootThread().appendBlip();
    assertManifestXml("<blip id=\"" + first.getId() + "\"></blip>" +
        "<blip id=\"" + second.getId() + "\"></blip>");
    assertMirrorConversationEquivalent();
  }

  public void testAppendNonInlineRepliesUpdatesManifest() {
    WaveletBasedConversationBlip blip = target.getRootThread().appendBlip();
    WaveletBasedConversationThread firstReply = blip.addReplyThread();
    WaveletBasedConversationBlip firstReplyBlip = firstReply.appendBlip();
    assertManifestXml("<blip id=\"" + blip.getId() + "\">" +
        "<thread id=\"" + firstReply.getId() + "\">" +
          "<blip id=\"" + firstReplyBlip.getId() + "\"></blip>" +
        "</thread>" +
        "</blip>");
    WaveletBasedConversationThread secondReply = blip.addReplyThread();
    WaveletBasedConversationBlip secondReplyBlip = secondReply.appendBlip();
    assertManifestXml("<blip id=\"" + blip.getId() + "\">" +
        "<thread id=\"" + firstReply.getId() + "\">" +
          "<blip id=\"" + firstReplyBlip.getId() + "\"></blip>" +
        "</thread>" +
        "<thread id=\"" + secondReply.getId() + "\">" +
          "<blip id=\"" + secondReplyBlip.getId() + "\"></blip>" +
        "</thread>" +
        "</blip>");
    assertMirrorConversationEquivalent();
  }

  public void testAppendBlipsInReplyThreadsUpdatesManifest() {
    WaveletBasedConversationBlip blip = target.getRootThread().appendBlip();
    WaveletBasedConversationThread reply = blip.addReplyThread();
    WaveletBasedConversationBlip firstReplyBlip = reply.appendBlip();
    WaveletBasedConversationBlip secondReplyBlip = reply.appendBlip();

    assertManifestXml("<blip id=\"" + blip.getId() + "\">" +
        "<thread id=\"" + reply.getId() + "\">" +
        "<blip id=\"" + firstReplyBlip.getId() + "\"></blip>" +
        "<blip id=\"" + secondReplyBlip.getId() + "\"></blip>" +
        "</thread>" +
        "</blip>");
    assertMirrorConversationEquivalent();
  }

  public void testAppendInlineReplyUpdatesManifest() {
    WaveletBasedConversationBlip blip = target.getRootThread().appendBlip();
    WaveletBasedConversationThread reply = blip.addReplyThread(locateAfterLineElement(
        blip.getContent()));
    WaveletBasedConversationBlip replyBlip = reply.appendBlip();
    assertManifestXml("<blip id=\"" + blip.getId() + "\">" +
        "<thread id=\"" + reply.getId() + "\" inline=\"true\">" +
          "<blip id=\"" + replyBlip.getId() + "\"></blip>" +
        "</thread>" +
        "</blip>");
    assertEquals(Blips.INITIAL_HEAD +
        "<body><line></line><reply id=\"" + reply.getId() + "\"></reply></body>",
        XmlStringBuilder.innerXml(blip.getContent()).toString());
    assertMirrorConversationEquivalent();
  }

  public void testDeleteBlipNoRepliesUpdatesManifest() {
    WaveletBasedConversationBlip blip = target.getRootThread().appendBlip();
    blip.delete();
    assertManifestXml("");
    assertStructureEquivalent(XmlStringBuilder.createEmpty(), blip.getContent());
    assertMirrorConversationEquivalent();
  }

  public void testDeleteBlipWithInlineReplyUpdatesManifest() {
    WaveletBasedConversationBlip blip = target.getRootThread().appendBlip();
    WaveletBasedConversationThread reply = blip.addReplyThread(
        BlipTestUtils.getBodyPosition(blip) + 3);
    WaveletBasedConversationBlip replyBlip = reply.appendBlip();

    blip.delete();
    // The first blip is gone, and the inline reply and its blip are gone too.
    // Both blips' content is gone.
    assertManifestXml("");
    assertStructureEquivalent(XmlStringBuilder.createEmpty(), blip.getContent());
    assertStructureEquivalent(XmlStringBuilder.createEmpty(), replyBlip.getContent());
    assertMirrorConversationEquivalent();
  }

  // Bug 2220263.
  public void testDeleteLastBlipInThreadRemovesThread() {
    ConversationBlip rootBlip = target.getRootThread().appendBlip();
    ConversationThread topThread = rootBlip.addReplyThread();
    ConversationBlip topBlip = topThread.appendBlip();

    ConversationThread firstReply = topBlip.addReplyThread();
    firstReply.appendBlip().delete();

    assertNull(topBlip.getReplyThread(firstReply.getId()));
    assertManifestXml("<blip id=\"" + rootBlip.getId() + "\">" +
        "<thread id=\"" + topThread.getId() + "\">" +
        "<blip id=\"" + topBlip.getId() + "\"></blip>" +
        "</thread>" +
        "</blip>");
  }

  /**
   * Test that producing a complex conversation structure programatically
   * results in the expected manifest, and that that manifest is parsed
   * back into an equivalent conversation structure.
   */
  public void testComplexManifestProducesEquivalentConversation() {
    // Set up wavelet structure. The root wavelet has just a single blip.
    // The second (target) wavelet is a private reply to the root. It has
    // two blips in the root thread. The first has two reply threads with one
    // blip each. The first reply is inline, with an anchor in the content
    // of the root thread's first blip.
    WaveletBasedConversation rootConv = makeConversation();
    WaveletBasedConversationBlip rootWaveletRootBlip = rootConv.getRootThread().appendBlip();

    target.setAnchor(rootConv.createAnchor(rootConv.getRootThread().getFirstBlip()));
    WaveletBasedConversationBlip firstBlip = target.getRootThread().appendBlip();
    WaveletBasedConversationBlip secondBlip = target.getRootThread().appendBlip();

    WaveletBasedConversationThread firstReply = firstBlip.addReplyThread();
    WaveletBasedConversationBlip firstReplyFirstBlip = firstReply.appendBlip();
    final WaveletBasedConversationThread secondReply = firstBlip.addReplyThread(
        BlipTestUtils.getBodyPosition(firstBlip) + 1);
    WaveletBasedConversationBlip secondReplyFirstBlip = secondReply.appendBlip();

    firstBlip.getContent().with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        int location = locateAfterLineElement(doc);
        Point<N> point = doc.locate(location);
        E element = doc.createElement(point, "reply",
            Collections.singletonMap("id", secondReply.getId()));
      }
    });

    // Clear attributes on the manifest since we're not interested in testing those.
    manifestDoc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        E top = DocHelper.getFirstChildElement(doc, doc.getDocumentElement());
        doc.setElementAttribute(top, "anchorBlip", null);
        doc.setElementAttribute(top, "anchorWavelet", null);
      }
    });

    assertManifestXml(
        "<blip id=\"" + firstBlip.getId() + "\">"
        + "<thread id=\"" + firstReply.getId() + "\">"
          + "<blip id=\"" + firstReplyFirstBlip.getId() + "\"></blip>"
        + "</thread>"
        + "<thread id=\"" + secondReply.getId() + "\" inline=\"true\">"
          + "<blip id=\"" + secondReplyFirstBlip.getId() + "\"></blip>"
        + "</thread>"
        + "</blip>"
      + "<blip id=\"" + secondBlip.getId() + "\"></blip>");
    assertMirrorConversationEquivalent();
  }

  /**
   * Tests that empty threads are not ignored.
   */
  public void testCreateWithEmptyManifestThreadNotIgnored() {
    ConversationBlip blip = target.getRootThread().appendBlip();
    ConversationThread thread = blip.addReplyThread();

    WaveletBasedConversation another = mirrorConversation(target);
    assertNotNull(another.getRootThread().getFirstBlip());
    assertTrue(another.getRootThread().getFirstBlip().getReplyThreads().iterator().hasNext());
  }

  /**
   * Test that a conversation can be created on a manifest that contains blips
   * that are not backed by the wavelet.
   */
  public void testBlipMissingFromWavelet() {
    WaveletBasedConversation empty = target;
    WaveletBasedConversation nonEmpty = makeConversation();
    nonEmpty.getRootThread().appendBlip();
    WaveletBasedConversation conversation = WaveletBasedConversation.create(conversationView,
        empty.getWavelet(), nonEmpty.getManifest(), idGenerator);
    assertNull(conversation.getRootThread().getFirstBlip());
    assertEquals(Collections.emptyList(), getBlipList(conversation.getRootThread()));
  }

  /**
   * Test that we can cope with blips being added to the manifest but not to the
   * wavelet.
   */
  public void testAddingBlipMissingFromWavelet() {
    manifestDoc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        N rootThreadNode = doc.getFirstChild(doc.getDocumentElement());
        E rootThread = doc.asElement(rootThreadNode);
        doc.createChildElement(rootThread, "blip", Collections.singletonMap(
            "id", idGenerator.newBlipId()));
      }
    });
    assertNull(target.getRootThread().getFirstBlip());
    assertEquals(Collections.emptyList(), getBlipList(target.getRootThread()));
  }

  /**
   * Test that iterating a thread whose manifest contains blips not backed by the
   * wavelet skips those blips.
   */
  public void testMissingBlipIteration() {
    WaveletBasedConversationThread thread = target.getRootThread();
    WaveletBasedConversationBlip first = thread.appendBlip();
    manifestDoc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        N rootThreadNode = doc.getFirstChild(doc.getDocumentElement());
        E rootThread = doc.asElement(rootThreadNode);
        doc.createChildElement(rootThread, "blip", Collections.singletonMap(
            "id", idGenerator.newBlipId()));
      }
    });
    WaveletBasedConversationBlip third = thread.appendBlip();
    assertEquals(3, CollectionUtils.newArrayList(thread.getManifestThread().getBlips()).size());
    assertEquals(Arrays.asList(first, third), getBlipList(thread));
  }

  // Bug 2268864.
  public void testObsoleteThreadThenRestoreRemoveBlipDoesntDie() {
    WaveletBasedConversationBlip first = target.getRootThread().appendBlip();
    WaveletBasedConversationThread willBecomeEmpty = first.addReplyThread();
    ConversationBlip toggleBlip = willBecomeEmpty.appendBlip();
    String toggleBlipId = toggleBlip.getId();

    // Make the thread empty by remotely removing its blip.
    ManifestBlip manifestRootBlip = target.getManifest().getRootThread().getBlip(0);
    ManifestThread manifestReply = manifestRootBlip.getReply(0);
    manifestReply.removeBlip(manifestReply.getBlip(0));

    assertThreadValid(willBecomeEmpty);

    // Re-add then remove the blip, as can happen in playback.
    manifestReply.appendBlip(toggleBlipId);
    manifestReply.removeBlip(manifestReply.getBlip(0));
  }

  public void testRemoveRestoreThreadAfterObsoleteThreadDoesntDie() {
    WaveletBasedConversationBlip first = target.getRootThread().appendBlip();
    WaveletBasedConversationThread reply11 = first.addReplyThread();
    ConversationBlip blip1 = reply11.appendBlip();
    String blip1Id = blip1.getId();

    WaveletBasedConversationThread reply2 = first.addReplyThread();

    // Make first thread empty by removing its blip.
    ManifestBlip manifestRootBlip = target.getManifest().getRootThread().getBlip(0);
    ManifestThread manifestReply1 = manifestRootBlip.getReply(0);
    manifestReply1.removeBlip(manifestReply1.getBlip(0));
    assertThreadValid(reply11);

    // Remove and re-add second thread, as can happen in playback.
    String thread2Id = reply2.getId();
    ManifestThread manifestReply2 = manifestRootBlip.getReply(1);
    manifestRootBlip.removeReply(manifestReply2);
    manifestRootBlip.appendReply(thread2Id, false);

    assertThreadInvalid(reply2);
  }

  //
  // Non-interface methods.
  //

  public void testGetBlipRetrievesBlip() {
    WaveletBasedConversationBlip blip = target.getRootThread().appendBlip();
    WaveletBasedConversationThread reply = blip.addReplyThread();
    WaveletBasedConversationBlip replyBlip = reply.appendBlip();

    assertSame(blip, target.getBlip(blip.getId()));
    assertSame(replyBlip, target.getBlip(replyBlip.getId()));
    assertNull(target.getBlip("foobar"));
  }

  //
  // Concurrent behaviour.
  //

  public void testConcrrentDeletionOfFinalBlipsLeavesEmptyThread() {
    WaveletBasedConversationBlip first = target.getRootThread().appendBlip();
    WaveletBasedConversationThread replyThread = first.addReplyThread();
    WaveletBasedConversationBlip b1 = replyThread.appendBlip();
    WaveletBasedConversationBlip b2 = replyThread.appendBlip();

    // Locally delete b1, remotely delete b2.
    b1.delete();

    replyThread.addListener(threadListener);
    b2.addListener(blipListener);
    target.addListener(convListener);

    remoteRemoveBlip(b2);
    // Expect blip deletion events and it to be invalid.
    verify(blipListener).onDeleted();
    verify(convListener).onBlipDeleted(b2);
    assertBlipInvalid(b2);
    assertThreadValid(replyThread);
    assertEquals(Arrays.asList(replyThread), CollectionUtils.newArrayList(first.getReplyThreads()));

    // The manifest now has a thread with no blips.
    assertEquals(1, first.getManifestBlip().numReplies());
    assertEquals(0, first.getManifestBlip().getReply(0).numBlips());

    // Still there after the next mutation.
    ObservableConversationBlip second = target.getRootThread().appendBlip();
    assertThreadValid(replyThread);
    assertEquals(Arrays.asList(replyThread), CollectionUtils.newArrayList(first.getReplyThreads()));
    verify(convListener).onBlipAdded(second);
    verifyNoMoreInteractions(blipListener, threadListener, convListener);
  }

  public void testConcurrentDeletionOfFinalThreadsLeavesEmptyBlip() {
    WaveletBasedConversationBlip first = target.getRootThread().appendBlip();
    WaveletBasedConversationThread t1 = first.addReplyThread();
    WaveletBasedConversationBlip t1b = t1.appendBlip();
    WaveletBasedConversationThread t2 = first.addReplyThread();
    WaveletBasedConversationBlip t2b = t2.appendBlip();

    // Locally delete t1, remotely delete t2.
    t1b.delete();

    first.addListener(blipListener);
    t2.addListener(threadListener);
    target.addListener(convListener);

    remoteRemoveBlip(t2b);
    remoteRemoveThread(t2);
    // Expect thread t2 deletion events and it to be invalid.
    verify(threadListener).onDeleted();
    verify(convListener).onBlipDeleted(t2b);
    verify(convListener).onThreadDeleted(t2);
    assertBlipInvalid(t2b);
    assertThreadInvalid(t2);
    assertBlipValid(first);
    assertNotNull(target.getRootThread().getFirstBlip());

    // The manifest now has an empty blip.
    assertEquals(0, first.getManifestBlip().numReplies());

    // Still there after next write.
    WaveletBasedConversationBlip second = target.getRootThread().appendBlip();
    assertBlipValid(first);
    verify(convListener).onBlipAdded(second);
    verifyNoMoreInteractions(blipListener, threadListener, convListener);
  }

  /**
   * Sets an attribute value on the conversation element in a manifest document.
   *
   * @param doc manifest document
   * @param attribute attribute to set
   * @param value value to set
   */
  private static void setManifestAttribute(MutableDocument<?, ?, ?> doc, final String attribute,
      final String value) {
    doc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        E top = DocHelper.expectAndGetFirstTopLevelElement(
            doc, DocumentBasedManifest.MANIFEST_TOP_TAG);
        doc.setElementAttribute(top, attribute, value);

      }
    });
  }

  /**
   * Gets an attribute value from the conversation element in a manifest
   * document.
   *
   * @param doc manifest document
   * @param attribute attribute to get
   * @return value of {@code attribute}
   */
  private static <N> String getManifestAttribute(MutableDocument<N, ?, ?> doc, String attribute) {
    // NOTE(user): two-stage generic unfunging is required for Sun's compiler.
    return getManifestAttributeInner(doc, attribute);
  }

  private static <E> String getManifestAttributeInner(MutableDocument<? super E, E, ?> doc,
      String attribute) {
    E top = DocHelper.expectAndGetFirstTopLevelElement(doc, DocumentBasedManifest.MANIFEST_TOP_TAG);
    return doc.getAttribute(top, attribute);
  }

  /**
   * Removes a blip from the manifest, as though a remote client had done so.
   */
  private void remoteRemoveBlip(WaveletBasedConversationBlip blip) {
    ManifestThread parentThread = blip.getThread().getManifestThread();
    ManifestBlip manifestBlip = blip.getManifestBlip();
    parentThread.removeBlip(manifestBlip);
  }

  /**
   * Removes a thread from the manifest, as though a remote client had done so.
   */
  private void remoteRemoveThread(WaveletBasedConversationThread thread) {
    ManifestBlip parentBlip = thread.getParentBlip().getManifestBlip();
    ManifestThread manifestThread = thread.getManifestThread();
    parentBlip.removeReply(manifestThread);
  }

  /**
   * Asserts that the manifest content within the "conversation" tag matches an
   * expected string.
   */
  private void assertManifestXml(final String expected) {
    manifestDoc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        XmlStringBuilder exp = XmlStringBuilder.createFromXmlString(expected);
        assertStructureEquivalent(exp.wrap("conversation"), doc);
      }
    });
  }

  /**
   * Asserts that a new conversation model built on top of the target
   * conversation's substrate matches the structure of that.
   */
  private void assertMirrorConversationEquivalent() {
    Conversation copy = mirrorConversation(target);
    assertThreadsEquivalent(target.getRootThread(), copy.getRootThread());
  }

  private static void assertThreadsEquivalent(ConversationThread expected,
      ConversationThread actual) {
    assertEquals("Mismatched id in constructed conversation thread",
        expected.getId(), actual.getId());
    assertEquals("Mismatched first blip in constructed conversation thread",
        expected.getFirstBlip() == null, actual.getFirstBlip() == null);
    Iterator<? extends ConversationBlip> expectedBlips = expected.getBlips().iterator();
    Iterator<? extends ConversationBlip> actualBlips = actual.getBlips().iterator();
    while (expectedBlips.hasNext()) {
      assertTrue("Missing blip in reconstructed conversation", actualBlips.hasNext());
      ConversationBlip expectedBlip = expectedBlips.next();
      ConversationBlip actualBlip = actualBlips.next();
      assertBlipsEquivalent(expectedBlip, actualBlip);
    }
    assertFalse("Extra blip in reconstructed conversation", actualBlips.hasNext());
  }

  private static void assertBlipsEquivalent(ConversationBlip expected,
      ConversationBlip actual) {
    assertEquals("Mismatched id in constructed conversation blip",
        expected.getId(), actual.getId());
    assertEquals("Mismatched author in constructed conversation blip",
        expected.getAuthorId(), actual.getAuthorId());
    assertEquals("Mismatched timestamp in constructed conversation blip",
        expected.getLastModifiedTime(), actual.getLastModifiedTime());
    assertEquals("Mismatched contributors in constructed conversation blip",
        expected.getContributorIds(), actual.getContributorIds());
    Iterator<? extends ConversationThread> expectedThreads =
      expected.getReplyThreads().iterator();
    Iterator<? extends ConversationThread> actualThreads = actual.getReplyThreads().iterator();
    while (expectedThreads.hasNext()) {
      assertTrue("Missing thread in reconstructed conversation", actualThreads.hasNext());
      ConversationThread expectedThread = expectedThreads.next();
      ConversationThread actualThread = actualThreads.next();
      assertThreadsEquivalent(expectedThread, actualThread);
    }
    assertFalse("Extra thread in reconstructed conversation", actualThreads.hasNext());
  }
}
TOP

Related Classes of org.waveprotocol.wave.model.conversation.WaveletBasedConversationTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.