org.jboss.aerogear.sync.server.netty.DiffSyncHandlerTest.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.aerogear.sync.server.netty.DiffSyncHandlerTest.java

Source

/**
 * JBoss, Home of Professional Open Source
 * Copyright Red Hat, Inc., and individual contributors.
 *
 * 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.jboss.aerogear.sync.server.netty;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.jboss.aerogear.sync.DefaultClientDocument;
import org.jboss.aerogear.sync.PatchMessage;
import org.jboss.aerogear.sync.client.ClientInMemoryDataStore;
import org.jboss.aerogear.sync.client.DefaultPatchObservable;
import org.jboss.aerogear.sync.diffmatchpatch.DiffMatchPatchDiff.Operation;
import org.jboss.aerogear.sync.client.ClientSyncEngine;
import org.jboss.aerogear.sync.diffmatchpatch.DiffMatchPatchEdit;
import org.jboss.aerogear.sync.diffmatchpatch.DiffMatchPatchMessage;
import org.jboss.aerogear.sync.diffmatchpatch.JsonMapper;
import org.jboss.aerogear.sync.diffmatchpatch.client.DiffMatchPatchClientSynchronizer;
import org.jboss.aerogear.sync.diffmatchpatch.server.DiffMatchPatchServerSynchronizer;
import org.jboss.aerogear.sync.server.ServerInMemoryDataStore;
import org.jboss.aerogear.sync.server.ServerSyncEngine;
import org.jboss.aerogear.sync.server.ServerSynchronizer;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.jboss.aerogear.sync.diffmatchpatch.JsonMapper.fromJson;

public class DiffSyncHandlerTest {

    @Test
    public void unknownMessageType() {
        final EmbeddedChannel channel = embeddedChannel();
        final JsonNode json = writeTextFrame(message("bogus").toString(), channel);
        assertThat(json.get("result").asText(), equalTo("Unknown msgType 'bogus'"));
    }

    @Test
    public void addDocument() {
        final EmbeddedChannel channel = embeddedChannel();
        final String docId = UUID.randomUUID().toString();
        final String clientId = "client1";
        final PatchMessage<DiffMatchPatchEdit> patchMessage = sendAddDoc(docId, clientId, "Once upon a time",
                channel);
        assertThat(patchMessage.documentId(), equalTo(docId));
        assertThat(patchMessage.clientId(), equalTo(clientId));
        assertThat(patchMessage.edits().size(), is(1));
        assertThat(patchMessage.edits().peek().diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
    }

    @Test
    public void addDocumentObjectContent() {
        final EmbeddedChannel channel = embeddedChannel();
        final String docId = UUID.randomUUID().toString();
        final String clientId = "client1";
        final String content = "{\"content\": {\"name\": \"Dr.Rosen\"}}";
        final PatchMessage<DiffMatchPatchEdit> patchMessage = sendAddDoc(docId, clientId, content, channel);
        assertThat(patchMessage.documentId(), equalTo(docId));
        assertThat(patchMessage.clientId(), equalTo(clientId));
        assertThat(patchMessage.edits().size(), is(1));
        assertThat(patchMessage.edits().peek().diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
    }

    @Test
    public void addDocumentArrayContent() throws Exception {
        final EmbeddedChannel channel = embeddedChannel();
        final String docId = UUID.randomUUID().toString();
        final String clientId = "client1";
        final String content = "{\"content\": [\"one\", \"two\"]}";
        final PatchMessage<DiffMatchPatchEdit> patchMessage = sendAddDoc(docId, clientId, content, channel);
        assertThat(patchMessage.documentId(), equalTo(docId));
        assertThat(patchMessage.clientId(), equalTo(clientId));
        assertThat(patchMessage.edits().size(), is(1));
        assertThat(patchMessage.edits().peek().diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
        assertThat(patchMessage.edits().peek().diff().diffs().get(0).text(), equalTo(content));
    }

    @Test
    public void addDocumentAlreadyExisting() {
        final EmbeddedChannel channel = embeddedChannel();
        final String docId = UUID.randomUUID().toString();
        final String clientOneId = "client1";
        final String clientTwoId = "client2";
        final String content = "{\"content\": {\"name\": \"Dr.Rosen\"}}";

        final PatchMessage<DiffMatchPatchEdit> patchMessageOne = sendAddDoc(docId, clientOneId, content, channel);
        assertThat(patchMessageOne.documentId(), equalTo(docId));
        assertThat(patchMessageOne.clientId(), equalTo(clientOneId));
        assertThat(patchMessageOne.edits().size(), is(1));
        final DiffMatchPatchEdit editOne = patchMessageOne.edits().peek();
        assertThat(editOne.clientVersion(), is(0L));
        assertThat(editOne.serverVersion(), is(1L));
        assertThat(editOne.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        final PatchMessage<DiffMatchPatchEdit> patchMessageTwo = sendAddDoc(docId, clientTwoId, content, channel);
        assertThat(patchMessageTwo.documentId(), equalTo(docId));
        assertThat(patchMessageTwo.clientId(), equalTo(clientTwoId));
        assertThat(patchMessageTwo.edits().size(), is(1));
        final DiffMatchPatchEdit editTwo = patchMessageTwo.edits().peek();
        assertThat(editTwo.clientVersion(), is(-1L));
        assertThat(editTwo.serverVersion(), is(1L));
        assertThat(editTwo.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
    }

    @Test
    public void addDocumentWithoutContent() {
        final ServerInMemoryDataStore<String, DiffMatchPatchEdit> dataStore = new ServerInMemoryDataStore<String, DiffMatchPatchEdit>();
        final EmbeddedChannel channel1 = embeddedChannel(dataStore);
        final EmbeddedChannel channel2 = embeddedChannel(dataStore);
        final String docId = UUID.randomUUID().toString();
        final String client1Id = "client1";
        final String client2Id = "client2";
        final String baseContent = "You shall not pass";

        // client1 sends the initial base document.
        sendAddDoc(docId, client1Id, baseContent, channel1);

        // client2 sends a add document request without any content.
        final PatchMessage<DiffMatchPatchEdit> patchMessage = sendAddDoc(docId, client2Id, channel2);
        assertThat(patchMessage.documentId(), equalTo(docId));
        assertThat(patchMessage.clientId(), equalTo(client2Id));
        assertThat(patchMessage.edits().size(), is(1));
        final DiffMatchPatchEdit edit = patchMessage.edits().peek();
        assertThat(edit.clientVersion(), is(-1L));
        assertThat(edit.serverVersion(), is(1L));
        assertThat(edit.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
        assertThat(edit.diff().diffs().get(0).text(), equalTo(baseContent));
    }

    @Test
    public void addDocumentWithContentConcurrent() throws Exception {
        final ExecutorService executorService = Executors.newCachedThreadPool();
        final int iterations = 100;
        final CountDownLatch await = new CountDownLatch(1);
        final CountDownLatch latch = new CountDownLatch(iterations);
        final String content = "You shall not pass!";
        final List<Future<PatchMessage<DiffMatchPatchEdit>>> futures = new ArrayList<Future<PatchMessage<DiffMatchPatchEdit>>>();
        final String client2Id = "client2";
        for (int i = 0; i < iterations; i++) {
            final String docId = UUID.randomUUID().toString();
            final ServerInMemoryDataStore<String, DiffMatchPatchEdit> dataStore = new ServerInMemoryDataStore<String, DiffMatchPatchEdit>();
            final EmbeddedChannel channel1 = embeddedChannel(dataStore);
            final EmbeddedChannel channel2 = embeddedChannel(dataStore);
            executorService.submit(new AddDocumentTask(channel1, docId, "client1", content, await, latch));
            final Future<PatchMessage<DiffMatchPatchEdit>> future = executorService
                    .submit(new AddDocumentTask(channel2, docId, client2Id, content, await, latch));
            futures.add(future);
        }
        await.countDown();
        latch.await();
        for (Future<PatchMessage<DiffMatchPatchEdit>> future : futures) {
            final PatchMessage<DiffMatchPatchEdit> patchMessage = future.get();
            assertThat(patchMessage.clientId(), equalTo(client2Id));
            assertThat(patchMessage.edits().size(), is(1));
            final DiffMatchPatchEdit edit = patchMessage.edits().peek();
            assertThat(edit.serverVersion(), is(1L));
            assertThat(edit.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
            assertThat(edit.diff().diffs().get(0).text(), equalTo(content));
        }
        executorService.shutdown();
    }

    @Test
    public void addDocumentWithoutContentConcurrent() throws Exception {
        final ExecutorService executorService = Executors.newCachedThreadPool();
        final int iterations = 100;
        final CountDownLatch await = new CountDownLatch(1);
        final CountDownLatch latch = new CountDownLatch(iterations);
        final String content = "You shall not pass!";
        final List<Future<PatchMessage<DiffMatchPatchEdit>>> futures = new ArrayList<Future<PatchMessage<DiffMatchPatchEdit>>>();
        final String client2Id = "client2";
        for (int i = 0; i < iterations; i++) {
            final String docId = UUID.randomUUID().toString();
            final ServerInMemoryDataStore<String, DiffMatchPatchEdit> dataStore = new ServerInMemoryDataStore<String, DiffMatchPatchEdit>();
            final EmbeddedChannel channel1 = embeddedChannel(dataStore);
            final EmbeddedChannel channel2 = embeddedChannel(dataStore);
            executorService.submit(new AddDocumentTask(channel1, docId, "client1", content, await, latch));
            final Future<PatchMessage<DiffMatchPatchEdit>> future = executorService
                    .submit(new AddDocumentTask(channel2, docId, client2Id, null, await, latch));
            futures.add(future);
        }
        await.countDown();
        latch.await();
        for (Future<PatchMessage<DiffMatchPatchEdit>> future : futures) {
            final PatchMessage<DiffMatchPatchEdit> patchMessage = future.get();
            assertThat(patchMessage.clientId(), equalTo(client2Id));
            // patchMessage can be empty if there is not yet and underlying document in the data store.
            // in our case this means that the first thread has not yet been executed.
            if (!patchMessage.edits().isEmpty()) {
                assertThat(patchMessage.edits().size(), is(1));
                final DiffMatchPatchEdit edit = patchMessage.edits().peek();
                assertThat(edit.serverVersion(), is(1L));
                assertThat(edit.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
                assertThat(edit.diff().diffs().get(0).text(), equalTo(content));
            }
        }
        executorService.shutdown();
    }

    private static class AddDocumentTask implements Callable<PatchMessage<DiffMatchPatchEdit>> {

        private final EmbeddedChannel channel;
        protected final String docId;
        protected final String clientId;
        protected final String content;
        private final CountDownLatch await;
        private final CountDownLatch latch;

        AddDocumentTask(final EmbeddedChannel channel, final String docId, final String clientId,
                final String content, final CountDownLatch await, final CountDownLatch latch) {
            this.channel = channel;
            this.docId = docId;
            this.clientId = clientId;
            this.content = content;
            this.latch = latch;
            this.await = await;
        }

        @Override
        public PatchMessage<DiffMatchPatchEdit> call() throws InterruptedException {
            try {
                await.await();
                return sendAddDoc(docId, clientId, content, channel);
            } finally {
                latch.countDown();
            }
        }
    }

    @Test
    public void patch() {
        final ServerInMemoryDataStore<String, DiffMatchPatchEdit> dataStore = new ServerInMemoryDataStore<String, DiffMatchPatchEdit>();
        final EmbeddedChannel channel1 = embeddedChannel(dataStore);
        final EmbeddedChannel channel2 = embeddedChannel(dataStore);
        final String docId = UUID.randomUUID().toString();
        final String originalContent = "{\"content\": \"Do or do not, there is no try.\"}";
        final String updatedContent = "{\"content\": \"Do or do not, there is no try!\"}";
        final String client1Id = "client1";
        final String client2Id = "client2";

        // add same document but with two different clients/channels.
        sendAddDocMsg(docId, client1Id, originalContent, channel1);
        sendAddDocMsg(docId, client2Id, originalContent, channel2);

        final PatchMessage<DiffMatchPatchEdit> clientEdit = generateClientSideEdits(docId, originalContent,
                client1Id, updatedContent);
        final PatchMessage<DiffMatchPatchEdit> patchMessage = sendEdit(clientEdit, channel1);
        assertThat(patchMessage.documentId(), equalTo(docId));
        assertThat(patchMessage.clientId(), equalTo(client1Id));
        assertThat(patchMessage.edits().size(), is(1));
        assertThat(patchMessage.edits().peek().diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        // client1 should not get an update as it was the one making the change.
        assertThat(channel1.readOutbound(), is(nullValue()));

        // get the update from channel2.
        final TextWebSocketFrame serverUpdate = channel2.readOutbound();
        final PatchMessage<DiffMatchPatchEdit> serverUpdates = fromJson(serverUpdate.text(),
                DiffMatchPatchMessage.class);
        assertThat(serverUpdates.documentId(), equalTo(docId));
        assertThat(serverUpdates.clientId(), equalTo(client2Id));
        final DiffMatchPatchEdit edit = serverUpdates.edits().peek();
        assertThat(edit.clientVersion(), is(0L));
        assertThat(edit.serverVersion(), is(0L));
        assertThat(edit.diff().diffs().size(), is(4));
        assertThat(edit.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
        assertThat(edit.diff().diffs().get(1).operation(), is(Operation.DELETE));
        assertThat(edit.diff().diffs().get(1).text(), equalTo("."));
        assertThat(edit.diff().diffs().get(2).operation(), is(Operation.ADD));
        assertThat(edit.diff().diffs().get(2).text(), equalTo("!"));
        assertThat(edit.diff().diffs().get(3).operation(), is(Operation.UNCHANGED));
    }

    @Test
    public void patchJedi() {
        final ClientSyncEngine<String, DiffMatchPatchEdit> clientSyncEngine = newClientSyncEngine();
        final ServerInMemoryDataStore<String, DiffMatchPatchEdit> dataStore = new ServerInMemoryDataStore<String, DiffMatchPatchEdit>();
        final EmbeddedChannel channel1 = embeddedChannel(dataStore);
        final EmbeddedChannel channel2 = embeddedChannel(dataStore);
        final String docId = UUID.randomUUID().toString();
        final String client1Id = "client1";
        final String client2Id = "client2";
        final String original = "I'm a Jedi";
        final String updateOne = "I'm a Sith";
        final String updateTwo = "Oh Yeah";

        // Add original document using client1/channel1
        final PatchMessage<DiffMatchPatchEdit> addPatchClient1 = sendAddDoc(docId, client1Id, original, channel1);
        assertThat(addPatchClient1.documentId(), equalTo(docId));
        assertThat(addPatchClient1.clientId(), equalTo(client1Id));
        assertThat(addPatchClient1.edits().size(), is(1));
        final DiffMatchPatchEdit patchOne = addPatchClient1.edits().peek();
        assertThat(patchOne.clientVersion(), is(0L));
        assertThat(patchOne.serverVersion(), is(1L));
        assertThat(patchOne.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        // Add document using client2/channel2
        final PatchMessage<DiffMatchPatchEdit> addPatchClient2 = sendAddDoc(docId, client2Id, original, channel2);
        assertThat(addPatchClient2.documentId(), equalTo(docId));
        assertThat(addPatchClient2.clientId(), equalTo(client2Id));
        assertThat(addPatchClient2.edits().size(), is(1));
        final DiffMatchPatchEdit patchTwo = addPatchClient2.edits().peek();
        assertThat(patchTwo.clientVersion(), is(-1L));
        assertThat(patchTwo.serverVersion(), is(1L));
        assertThat(patchTwo.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        // Add the document to the client sync engine. Only used to help produce diffs.
        clientSyncEngine.addDocument(new DefaultClientDocument<String>(docId, client1Id, original));
        final PatchMessage<DiffMatchPatchEdit> clientEdit = clientSyncEngine
                .diff(new DefaultClientDocument<String>(docId, client1Id, updateOne));

        final PatchMessage<DiffMatchPatchEdit> patchMessage = sendEdit(clientEdit, channel1);
        assertThat(patchMessage.documentId(), equalTo(docId));
        assertThat(patchMessage.clientId(), equalTo(client1Id));
        assertThat(patchMessage.edits().size(), is(1));
        assertThat(patchMessage.edits().peek().diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
        assertThat(patchMessage.edits().peek().clientVersion(), is(1L));
        assertThat(patchMessage.edits().peek().serverVersion(), is(0L));

        // patch the client engine so that version are updated and edits cleared
        clientSyncEngine.patch(patchMessage);

        // get the update from channel2.
        final TextWebSocketFrame serverUpdateOne = channel2.readOutbound();
        final PatchMessage<DiffMatchPatchEdit> serverUpdates = fromJson(serverUpdateOne.text(),
                DiffMatchPatchMessage.class);
        assertThat(serverUpdates.documentId(), equalTo(docId));
        assertThat(serverUpdates.clientId(), equalTo(client2Id));
        final DiffMatchPatchEdit editOne = serverUpdates.edits().peek();
        assertThat(editOne.clientVersion(), is(0L));
        assertThat(editOne.serverVersion(), is(0L));
        assertThat(editOne.diff().diffs().size(), is(5));
        assertThat(editOne.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));
        assertThat(editOne.diff().diffs().get(0).text(), equalTo("I'm a "));
        assertThat(editOne.diff().diffs().get(1).operation(), is(Operation.DELETE));
        assertThat(editOne.diff().diffs().get(1).text(), equalTo("Jed"));
        assertThat(editOne.diff().diffs().get(2).operation(), is(Operation.ADD));
        assertThat(editOne.diff().diffs().get(2).text(), equalTo("S"));
        assertThat(editOne.diff().diffs().get(3).operation(), is(Operation.UNCHANGED));
        assertThat(editOne.diff().diffs().get(3).text(), equalTo("i"));
        assertThat(editOne.diff().diffs().get(4).operation(), is(Operation.ADD));
        assertThat(editOne.diff().diffs().get(4).text(), equalTo("th"));

        final PatchMessage<DiffMatchPatchEdit> clientEditTwo = clientSyncEngine
                .diff(new DefaultClientDocument<String>(docId, client1Id, updateTwo));
        final PatchMessage<DiffMatchPatchEdit> patchMessageTwo = sendEdit(clientEditTwo, channel1);
        assertThat(patchMessageTwo.edits().size(), is(1));
        assertThat(patchMessageTwo.edits().peek().diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        final TextWebSocketFrame serverUpdateTwo = channel2.readOutbound();
        final PatchMessage<DiffMatchPatchEdit> serverUpdatesTwo = fromJson(serverUpdateTwo.text(),
                DiffMatchPatchMessage.class);
        assertThat(serverUpdatesTwo.documentId(), equalTo(docId));
        assertThat(serverUpdatesTwo.clientId(), equalTo(client2Id));
        assertThat(serverUpdatesTwo.edits().size(), is(2));
        // just remove the first edit. We have already verified it but since we have not
        // sent an acknowledgement to the server, the server thinks that we never got it.
        serverUpdatesTwo.edits().remove();

        final DiffMatchPatchEdit editTwo = serverUpdatesTwo.edits().remove();
        assertThat(editTwo.clientVersion(), is(0L));
        assertThat(editTwo.serverVersion(), is(1L));

        assertThat(editTwo.diff().diffs().size(), is(7));
        assertThat(editTwo.diff().diffs().get(0).operation(), is(Operation.DELETE));
        assertThat(editTwo.diff().diffs().get(0).text(), equalTo("I'm"));
        assertThat(editTwo.diff().diffs().get(1).operation(), is(Operation.ADD));
        assertThat(editTwo.diff().diffs().get(1).text(), equalTo("Oh"));
        assertThat(editTwo.diff().diffs().get(2).operation(), is(Operation.UNCHANGED));
        assertThat(editTwo.diff().diffs().get(2).text(), equalTo(" "));
        assertThat(editTwo.diff().diffs().get(3).operation(), is(Operation.ADD));
        assertThat(editTwo.diff().diffs().get(3).text(), equalTo("Ye"));
        assertThat(editTwo.diff().diffs().get(4).operation(), is(Operation.UNCHANGED));
        assertThat(editTwo.diff().diffs().get(4).text(), equalTo("a"));
        assertThat(editTwo.diff().diffs().get(5).operation(), is(Operation.DELETE));
        assertThat(editTwo.diff().diffs().get(5).text(), equalTo(" Sit"));
        assertThat(editTwo.diff().diffs().get(6).operation(), is(Operation.UNCHANGED));
        assertThat(editTwo.diff().diffs().get(6).text(), equalTo("h"));
    }

    @Test
    public void patchCompletReplacementOfContent() {
        final ClientSyncEngine<String, DiffMatchPatchEdit> clientSyncEngine = newClientSyncEngine();
        final ServerInMemoryDataStore<String, DiffMatchPatchEdit> dataStore = new ServerInMemoryDataStore<String, DiffMatchPatchEdit>();
        final EmbeddedChannel channel1 = embeddedChannel(dataStore);
        final EmbeddedChannel channel2 = embeddedChannel(dataStore);
        final String docId = UUID.randomUUID().toString();
        final String client1Id = "client1";
        final String client2Id = "client2";
        final String original = "Beve";
        final String updateOne = "I'm the man";

        // Add original document using client1/channal1
        final PatchMessage<DiffMatchPatchEdit> addPatchClient1 = sendAddDoc(docId, client1Id, original, channel1);
        assertThat(addPatchClient1.documentId(), equalTo(docId));
        assertThat(addPatchClient1.clientId(), equalTo(client1Id));
        assertThat(addPatchClient1.edits().size(), is(1));
        final DiffMatchPatchEdit patchOne = addPatchClient1.edits().peek();
        assertThat(patchOne.clientVersion(), is(0L));
        assertThat(patchOne.serverVersion(), is(1L));
        assertThat(patchOne.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        // Add document using client2/channel2
        final PatchMessage<DiffMatchPatchEdit> addPatchClient2 = sendAddDoc(docId, client2Id, original, channel2);
        assertThat(addPatchClient2.documentId(), equalTo(docId));
        assertThat(addPatchClient2.clientId(), equalTo(client2Id));
        assertThat(addPatchClient2.edits().size(), is(1));
        final DiffMatchPatchEdit patchTwo = addPatchClient2.edits().peek();
        assertThat(patchTwo.clientVersion(), is(-1L));
        assertThat(patchTwo.serverVersion(), is(1L));
        assertThat(patchTwo.diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        // Add the document to the client sync engine. Only used to help produce diffs.
        clientSyncEngine.addDocument(new DefaultClientDocument<String>(docId, client1Id, original));
        final PatchMessage<DiffMatchPatchEdit> clientEdit = clientSyncEngine
                .diff(new DefaultClientDocument<String>(docId, client1Id, updateOne));

        final PatchMessage<DiffMatchPatchEdit> patchMessage = sendEdit(clientEdit, channel1);
        assertThat(patchMessage.documentId(), equalTo(docId));
        assertThat(patchMessage.clientId(), equalTo(client1Id));
        assertThat(patchMessage.edits().size(), is(1));
        assertThat(patchMessage.edits().peek().diff().diffs().get(0).operation(), is(Operation.UNCHANGED));

        // patch the client engine so that version are updated and edits cleared
        clientSyncEngine.patch(patchMessage);

        // get the update from channel2.
        final TextWebSocketFrame serverUpdateOne = channel2.readOutbound();
        final PatchMessage<DiffMatchPatchEdit> serverUpdates = fromJson(serverUpdateOne.text(),
                DiffMatchPatchMessage.class);
        assertThat(serverUpdates.documentId(), equalTo(docId));
        assertThat(serverUpdates.clientId(), equalTo(client2Id));
        final DiffMatchPatchEdit editOne = serverUpdates.edits().peek();
        assertThat(editOne.clientVersion(), is(0L));
        assertThat(editOne.serverVersion(), is(0L));

        assertThat(editOne.diff().diffs().size(), is(5));
        assertThat(editOne.diff().diffs().get(0).operation(), is(Operation.DELETE));
        assertThat(editOne.diff().diffs().get(0).text(), equalTo("B"));
        assertThat(editOne.diff().diffs().get(1).operation(), is(Operation.ADD));
        assertThat(editOne.diff().diffs().get(1).text(), equalTo("I'm th"));
        assertThat(editOne.diff().diffs().get(2).operation(), is(Operation.UNCHANGED));
        assertThat(editOne.diff().diffs().get(2).text(), equalTo("e"));
        assertThat(editOne.diff().diffs().get(3).operation(), is(Operation.DELETE));
        assertThat(editOne.diff().diffs().get(3).text(), equalTo("ve"));
        assertThat(editOne.diff().diffs().get(4).operation(), is(Operation.ADD));
        assertThat(editOne.diff().diffs().get(4).text(), equalTo(" man"));
    }

    private static PatchMessage<DiffMatchPatchEdit> sendEdit(final PatchMessage<DiffMatchPatchEdit> patchMessage,
            final EmbeddedChannel ch) {
        return fromJson(writeFrame(JsonMapper.toJson(patchMessage), ch), DiffMatchPatchMessage.class);
    }

    private static JsonNode sendAddDocMsg(final String docId, final String clientId, final String content,
            final EmbeddedChannel ch) {
        final ObjectNode docMsg = message("add");
        docMsg.put("msgType", "add");
        docMsg.put("id", docId);
        docMsg.put("clientId", clientId);
        docMsg.put("content", content);
        return writeTextFrame(docMsg.toString(), ch);
    }

    private static PatchMessage<DiffMatchPatchEdit> sendAddDoc(final String docId, final String clientId,
            final String content, final EmbeddedChannel ch) {
        final ObjectNode docMsg = message("add");
        docMsg.put("msgType", "add");
        docMsg.put("id", docId);
        docMsg.put("clientId", clientId);
        docMsg.put("content", content);
        return fromJson(writeFrame(docMsg.toString(), ch), DiffMatchPatchMessage.class);
    }

    private static PatchMessage<DiffMatchPatchEdit> sendAddDoc(final String docId, final String clientId,
            final EmbeddedChannel ch) {
        final ObjectNode docMsg = message("add");
        docMsg.put("msgType", "add");
        docMsg.put("id", docId);
        docMsg.put("clientId", clientId);
        return fromJson(writeFrame(docMsg.toString(), ch), DiffMatchPatchMessage.class);
    }

    private static ObjectNode message(final String type) {
        final ObjectNode jsonNode = JsonMapper.newObjectNode();
        jsonNode.put("msgType", type);
        return jsonNode;
    }

    private static JsonNode writeTextFrame(final String content, final EmbeddedChannel ch) {
        ch.writeInbound(textFrame(content));
        final TextWebSocketFrame textFrame = ch.readOutbound();
        return JsonMapper.asJsonNode(textFrame.text());
    }

    private static String writeFrame(final String content, final EmbeddedChannel ch) {
        ch.writeInbound(textFrame(content));
        final TextWebSocketFrame textFrame = ch.readOutbound();
        return textFrame.text();
    }

    private static TextWebSocketFrame textFrame(final String content) {
        return new TextWebSocketFrame(content);
    }

    private static EmbeddedChannel embeddedChannel() {
        return embeddedChannel(new ServerInMemoryDataStore<String, DiffMatchPatchEdit>());
    }

    private static EmbeddedChannel embeddedChannel(
            final ServerInMemoryDataStore<String, DiffMatchPatchEdit> dataStore) {
        final ServerSynchronizer<String, DiffMatchPatchEdit> synchronizer = new DiffMatchPatchServerSynchronizer();
        final ServerSyncEngine<String, DiffMatchPatchEdit> syncEngine = new ServerSyncEngine<String, DiffMatchPatchEdit>(
                synchronizer, dataStore);
        return new EmbeddedChannel(new DiffSyncHandler<String, DiffMatchPatchEdit>(syncEngine));
    }

    private static PatchMessage<DiffMatchPatchEdit> generateClientSideEdits(final String documentId,
            final String originalContent, final String clientId, final String updatedContent) {
        final ClientSyncEngine<String, DiffMatchPatchEdit> clientSyncEngine = newClientSyncEngine();
        clientSyncEngine.addDocument(new DefaultClientDocument<String>(documentId, clientId, originalContent));
        final DefaultClientDocument<String> doc = new DefaultClientDocument<String>(documentId, clientId,
                updatedContent);
        return clientSyncEngine.diff(doc);
    }

    private static ClientSyncEngine<String, DiffMatchPatchEdit> newClientSyncEngine() {
        return new ClientSyncEngine<String, DiffMatchPatchEdit>(new DiffMatchPatchClientSynchronizer(),
                new ClientInMemoryDataStore<String, DiffMatchPatchEdit>(), new DefaultPatchObservable<String>());
    }

}