// // Copyright (c) 2016 Couchbase, Inc. All rights reserved. // // 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 com.couchbase.lite.replicator; import com.couchbase.lite.CouchbaseLiteException; import com.couchbase.lite.Database; import com.couchbase.lite.Document; import com.couchbase.lite.DocumentChange; import com.couchbase.lite.Emitter; import com.couchbase.lite.LiteTestCaseWithDB; import com.couchbase.lite.LiveQuery; import com.couchbase.lite.Manager; import com.couchbase.lite.Mapper; import com.couchbase.lite.Query; import com.couchbase.lite.QueryEnumerator; import com.couchbase.lite.QueryOptions; import com.couchbase.lite.QueryRow; import com.couchbase.lite.ReplicationFilter; import com.couchbase.lite.Revision; import com.couchbase.lite.SavedRevision; import com.couchbase.lite.Status; import com.couchbase.lite.TransactionalTask; import com.couchbase.lite.UnsavedRevision; import com.couchbase.lite.ValidationContext; import com.couchbase.lite.Validator; import com.couchbase.lite.View; import com.couchbase.lite.auth.Authenticator; import com.couchbase.lite.auth.AuthenticatorFactory; import com.couchbase.lite.auth.FacebookAuthorizer; import com.couchbase.lite.internal.RevisionInternal; import com.couchbase.lite.mockserver.MockBulkDocs; import com.couchbase.lite.mockserver.MockChangesFeed; import com.couchbase.lite.mockserver.MockChangesFeedNoResponse; import com.couchbase.lite.mockserver.MockCheckpointGet; import com.couchbase.lite.mockserver.MockCheckpointPut; import com.couchbase.lite.mockserver.MockCreateDB; import com.couchbase.lite.mockserver.MockDispatcher; import com.couchbase.lite.mockserver.MockDocumentAllDocs; import com.couchbase.lite.mockserver.MockDocumentBulkGet; import com.couchbase.lite.mockserver.MockDocumentGet; import com.couchbase.lite.mockserver.MockDocumentPut; import com.couchbase.lite.mockserver.MockFacebookAuthPost; import com.couchbase.lite.mockserver.MockHelper; import com.couchbase.lite.mockserver.MockRevsDiff; import com.couchbase.lite.mockserver.MockSessionGet; import com.couchbase.lite.mockserver.SmartMockResponseImpl; import com.couchbase.lite.mockserver.WrappedSmartMockResponse; import com.couchbase.lite.support.Base64; import com.couchbase.lite.support.CouchbaseLiteHttpClientFactory; import com.couchbase.lite.support.MultipartReader; import com.couchbase.lite.support.MultipartReaderDelegate; import com.couchbase.lite.util.Log; import com.couchbase.lite.util.Utils; import junit.framework.Assert; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLDecoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import okio.Buffer; /** * Created by hideki on 6/8/16. */ public class ReplicationMockWebServerTest extends LiteTestCaseWithDB { /** * Continuous puller starts offline * Wait for a while .. (til what?) * Add remote document (simulate w/ mock webserver) * Put replication online * Make sure doc is pulled */ public void testGoOnlinePuller() throws Exception { Log.d(Log.TAG, "testGoOnlinePuller"); // create mock server MockWebServer server = new MockWebServer(); MockDispatcher dispatcher = new MockDispatcher(); try { dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); server.setDispatcher(dispatcher); server.start(); // mock documents to be pulled MockDocumentGet.MockDocument mockDoc1 = new MockDocumentGet.MockDocument("doc1", "1-5e38", 1); mockDoc1.setJsonMap(MockHelper.generateRandomJsonMap()); // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _changes response 503 error (sticky) WrappedSmartMockResponse wrapped2 = new WrappedSmartMockResponse(new MockResponse().setResponseCode(503)); wrapped2.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, wrapped2); // doc1 response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDoc1); dispatcher.enqueueResponse(mockDoc1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // create and start replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); pullReplication.start(); Log.d(Log.TAG, "Started pullReplication: %s", pullReplication); // wait until a _checkpoint request have been sent dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); // wait until a _changes request has been sent dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHANGES); putReplicationOffline(pullReplication); // NOTE: give 1 sec to settle. I believe bug in this test, mock server, or replicator // code. But currently hard to find out. // https://github.com/couchbase/couchbase-lite-java-core/issues/1494 try { Thread.sleep(1000); // 1 sec } catch (Exception e) { } // clear out existing queued mock responses to make room for new ones dispatcher.clearQueuedResponse(MockHelper.PATH_REGEX_CHANGES); // real _changes response with doc1 MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // long poll changes feed no response MockChangesFeedNoResponse mockChangesFeedNoResponse = new MockChangesFeedNoResponse(); mockChangesFeedNoResponse.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedNoResponse); putReplicationOnline(pullReplication); Log.d(Log.TAG, "Waiting for PUT checkpoint request with seq: %d", mockDoc1.getDocSeq()); waitForPutCheckpointRequestWithSeq(dispatcher, mockDoc1.getDocSeq()); Log.d(Log.TAG, "Got PUT checkpoint request with seq: %d", mockDoc1.getDocSeq()); stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Pull replication test: * <p/> * - Single one-shot pull replication * - Against simulated sync gateway * - Remote docs do not have attachments */ public void testMockSinglePullSyncGw() throws Exception { boolean shutdownMockWebserver = true; boolean addAttachments = false; mockSinglePull(shutdownMockWebserver, MockDispatcher.ServerType.SYNC_GW, addAttachments); } /** * Pull replication test: * <p/> * - Single one-shot pull replication * - Against simulated couchdb * - Remote docs do not have attachments */ public void testMockSinglePullCouchDb() throws Exception { boolean shutdownMockWebserver = true; boolean addAttachments = false; mockSinglePull(shutdownMockWebserver, MockDispatcher.ServerType.COUCHDB, addAttachments); } /** * Pull replication test: * <p/> * - Single one-shot pull replication * - Against simulated couchdb * - Remote docs have attachments */ public void testMockSinglePullCouchDbAttachments() throws Exception { boolean shutdownMockWebserver = true; boolean addAttachments = true; mockSinglePull(shutdownMockWebserver, MockDispatcher.ServerType.COUCHDB, addAttachments); } /** * Pull replication test: * <p/> * - Single one-shot pull replication * - Against simulated sync gateway * - Remote docs have attachments * <p/> * TODO: sporadic assertion failure when checking rev field of PUT checkpoint requests */ public void testMockSinglePullSyncGwAttachments() throws Exception { boolean shutdownMockWebserver = true; boolean addAttachments = true; mockSinglePull(shutdownMockWebserver, MockDispatcher.ServerType.SYNC_GW, addAttachments); } public void testMockMultiplePullSyncGw() throws Exception { boolean shutdownMockWebserver = true; mockMultiplePull(shutdownMockWebserver, MockDispatcher.ServerType.SYNC_GW); } public void testMockMultiplePullCouchDb() throws Exception { boolean shutdownMockWebserver = true; mockMultiplePull(shutdownMockWebserver, MockDispatcher.ServerType.COUCHDB); } public void testMockContinuousPullCouchDb() throws Exception { boolean shutdownMockWebserver = true; mockContinuousPull(shutdownMockWebserver, MockDispatcher.ServerType.COUCHDB); } /** * Do a pull replication * * @param shutdownMockWebserver - should this test shutdown the mockwebserver * when done? if another test wants to pick up * where this left off, you should pass false. * @param serverType - should the mock return the Sync Gateway server type in * the "Server" HTTP Header? this changes the behavior of the * replicator to use bulk_get and POST reqeusts for _changes feeds. * @param addAttachments - should the mock sync gateway return docs with attachments? * @return a map that contains the mockwebserver (key="server") and the mock dispatcher * (key="dispatcher") */ private Map<String, Object> mockSinglePull(boolean shutdownMockWebserver, MockDispatcher.ServerType serverType, boolean addAttachments) throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); try { dispatcher.setServerType(serverType); // mock documents to be pulled MockDocumentGet.MockDocument mockDoc1 = new MockDocumentGet.MockDocument("doc1", "1-5e38", 1); mockDoc1.setJsonMap(MockHelper.generateRandomJsonMap()); if (addAttachments) mockDoc1.setAttachmentName("attachment.png"); MockDocumentGet.MockDocument mockDoc2 = new MockDocumentGet.MockDocument("doc2", "1-563b", 2); mockDoc2.setJsonMap(MockHelper.generateRandomJsonMap()); if (addAttachments) mockDoc2.setAttachmentName("attachment2.png"); // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc1)); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc2)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // Empty _all_docs response to pass unit tests dispatcher.enqueueResponse(MockHelper.PATH_REGEX_ALL_DOCS, new MockDocumentAllDocs()); // doc1 response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDoc1); if (addAttachments) mockDocumentGet.addAttachmentFilename(mockDoc1.getAttachmentName()); dispatcher.enqueueResponse(mockDoc1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // doc2 response mockDocumentGet = new MockDocumentGet(mockDoc2); if (addAttachments) mockDocumentGet.addAttachmentFilename(mockDoc2.getAttachmentName()); dispatcher.enqueueResponse(mockDoc2.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // _bulk_get response MockDocumentBulkGet mockBulkGet = new MockDocumentBulkGet(); mockBulkGet.addDocument(mockDoc1); mockBulkGet.addDocument(mockDoc2); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_GET, mockBulkGet); // respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); Map<String, Object> headers = new HashMap<String, Object>(); headers.put("foo", "bar"); pullReplication.setHeaders(headers); String checkpointId = pullReplication.remoteCheckpointDocID(); runReplication(pullReplication); Log.d(TAG, "pullReplication finished"); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { List<DocumentChange> changes = event.getChanges(); for (DocumentChange documentChange : changes) { Log.d(TAG, "doc change callback: %s", documentChange.getDocumentId()); } } }); // assert that we now have both docs in local db assertNotNull(database); Document doc1 = database.getDocument(mockDoc1.getDocId()); assertNotNull(doc1); assertNotNull(doc1.getCurrentRevisionId()); assertTrue(doc1.getCurrentRevisionId().equals(mockDoc1.getDocRev())); assertNotNull(doc1.getProperties()); assertEquals(mockDoc1.getJsonMap(), doc1.getUserProperties()); Document doc2 = database.getDocument(mockDoc2.getDocId()); assertNotNull(doc2); assertNotNull(doc2.getCurrentRevisionId()); assertNotNull(doc2.getProperties()); assertTrue(doc2.getCurrentRevisionId().equals(mockDoc2.getDocRev())); assertEquals(mockDoc2.getJsonMap(), doc2.getUserProperties()); // assert that docs have attachments (if applicable) if (addAttachments) { attachmentAsserts(mockDoc1.getAttachmentName(), doc1); attachmentAsserts(mockDoc2.getAttachmentName(), doc2); } // make assertions about outgoing requests from replicator -> mock RecordedRequest getCheckpointRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(getCheckpointRequest); assertEquals("bar", getCheckpointRequest.getHeader("foo")); assertTrue(getCheckpointRequest.getMethod().equals("GET")); assertTrue(getCheckpointRequest.getPath().matches(MockHelper.PATH_REGEX_CHECKPOINT)); RecordedRequest getChangesFeedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES); assertTrue(getChangesFeedRequest.getMethod().equals("POST")); assertTrue(getChangesFeedRequest.getPath().matches(MockHelper.PATH_REGEX_CHANGES)); // wait until the mock webserver receives a PUT checkpoint request with doc #2's sequence Log.d(TAG, "waiting for PUT checkpoint %s", mockDoc2.getDocSeq()); List<RecordedRequest> checkpointRequests = waitForPutCheckpointRequestWithSequence(dispatcher, mockDoc2.getDocSeq()); validateCheckpointRequestsRevisions(checkpointRequests); Log.d(TAG, "got PUT checkpoint %s", mockDoc2.getDocSeq()); // assert our local sequence matches what is expected String lastSequence = database.lastSequenceWithCheckpointId(checkpointId); assertEquals(Integer.toString(mockDoc2.getDocSeq()), lastSequence); // assert completed count makes sense assertEquals(pullReplication.getChangesCount(), pullReplication.getCompletedChangesCount()); // allow for either a single _bulk_get request or individual doc requests. // if the server is sync gateway, it is allowable for replicator to use _bulk_get RecordedRequest request = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_GET); if (request != null) { String body = MockHelper.getUtf8Body(request); assertTrue(body.contains(mockDoc1.getDocId())); assertTrue(body.contains(mockDoc2.getDocId())); } else { RecordedRequest doc1Request = dispatcher.takeRequest(mockDoc1.getDocPathRegex()); assertTrue(doc1Request.getMethod().equals("GET")); assertTrue(doc1Request.getPath().matches(mockDoc1.getDocPathRegex())); RecordedRequest doc2Request = dispatcher.takeRequest(mockDoc2.getDocPathRegex()); assertTrue(doc2Request.getMethod().equals("GET")); assertTrue(doc2Request.getPath().matches(mockDoc2.getDocPathRegex())); } } finally { // Shut down the server. Instances cannot be reused. if (shutdownMockWebserver) { assertTrue(MockHelper.shutdown(server, dispatcher)); } } Map<String, Object> returnVal = new HashMap<String, Object>(); returnVal.put("server", server); returnVal.put("dispatcher", dispatcher); return returnVal; } /** * Simulate the following: * <p/> * - Add a few docs and do a pull replication * - One doc on sync gateway is now updated * - Do a second pull replication * - Assert we get the updated doc and save it locally */ private Map<String, Object> mockMultiplePull(boolean shutdownMockWebserver, MockDispatcher.ServerType serverType) throws Exception { String doc1Id = "doc1"; // create mockwebserver and custom dispatcher boolean addAttachments = false; // do a pull replication Map<String, Object> serverAndDispatcher = mockSinglePull(false, serverType, addAttachments); MockWebServer server = (MockWebServer) serverAndDispatcher.get("server"); MockDispatcher dispatcher = (MockDispatcher) serverAndDispatcher.get("dispatcher"); try { // clear out any possible residue left from previous test, eg, mock responses queued up as // any recorded requests that have been logged. dispatcher.reset(); String doc1Rev = "2-2e38"; int doc1Seq = 3; String checkpointRev = "0-1"; String checkpointLastSequence = "2"; // checkpoint GET response w/ seq = 2 MockCheckpointGet mockCheckpointGet = new MockCheckpointGet(); mockCheckpointGet.setOk("true"); mockCheckpointGet.setRev(checkpointRev); mockCheckpointGet.setLastSequence(checkpointLastSequence); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointGet); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); MockChangesFeed.MockChangedDoc mockChangedDoc1 = new MockChangesFeed.MockChangedDoc() .setSeq(doc1Seq) .setDocId(doc1Id) .setChangedRevIds(Arrays.asList(doc1Rev)); mockChangesFeed.add(mockChangedDoc1); MockResponse fakeChangesResponse = mockChangesFeed.generateMockResponse(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, fakeChangesResponse); // Empty _all_docs response to pass unit tests dispatcher.enqueueResponse(MockHelper.PATH_REGEX_ALL_DOCS, new MockDocumentAllDocs()); // doc1 response Map<String, Object> doc1JsonMap = MockHelper.generateRandomJsonMap(); MockDocumentGet mockDocumentGet = new MockDocumentGet() .setDocId(doc1Id) .setRev(doc1Rev) .setJsonMap(doc1JsonMap); String doc1PathRegex = "/db/doc1.*"; dispatcher.enqueueResponse(doc1PathRegex, mockDocumentGet.generateMockResponse()); // checkpoint PUT response MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointGet.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); runReplication(pullReplication); // assert that we now have both docs in local db assertNotNull(database); Document doc1 = database.getDocument(doc1Id); assertNotNull(doc1); assertNotNull(doc1.getCurrentRevisionId()); assertTrue(doc1.getCurrentRevisionId().startsWith("2-")); assertEquals(doc1JsonMap, doc1.getUserProperties()); // make assertions about outgoing requests from replicator -> mock RecordedRequest getCheckpointRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(getCheckpointRequest); assertTrue(getCheckpointRequest.getMethod().equals("GET")); assertTrue(getCheckpointRequest.getPath().matches(MockHelper.PATH_REGEX_CHECKPOINT)); RecordedRequest getChangesFeedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES); assertTrue(getChangesFeedRequest.getMethod().equals("POST")); assertTrue(getChangesFeedRequest.getPath().matches(MockHelper.PATH_REGEX_CHANGES)); if (serverType == MockDispatcher.ServerType.SYNC_GW) { Map<String, Object> jsonMap = Manager.getObjectMapper().readValue(getChangesFeedRequest.getUtf8Body(), Map.class); assertTrue(jsonMap.containsKey("since")); Integer since = (Integer) jsonMap.get("since"); assertEquals(2, since.intValue()); } RecordedRequest doc1Request = dispatcher.takeRequest(doc1PathRegex); assertTrue(doc1Request.getMethod().equals("GET")); assertTrue(doc1Request.getPath().matches("/db/doc1\\?rev=2-2e38.*")); // wait until the mock webserver receives a PUT checkpoint request with doc #2's sequence int expectedLastSequence = doc1Seq; List<RecordedRequest> checkpointRequests = waitForPutCheckpointRequestWithSequence(dispatcher, expectedLastSequence); assertEquals(1, checkpointRequests.size()); // assert our local sequence matches what is expected String lastSequence = database.lastSequenceWithCheckpointId(pullReplication.remoteCheckpointDocID()); assertEquals(Integer.toString(expectedLastSequence), lastSequence); // assert completed count makes sense assertEquals(pullReplication.getChangesCount(), pullReplication.getCompletedChangesCount()); } finally { if (shutdownMockWebserver) { assertTrue(MockHelper.shutdown(server, dispatcher)); } } Map<String, Object> returnVal = new HashMap<String, Object>(); returnVal.put("server", server); returnVal.put("dispatcher", dispatcher); return returnVal; } private Map<String, Object> mockContinuousPull(boolean shutdownMockWebserver, MockDispatcher.ServerType serverType) throws Exception { assertTrue(serverType == MockDispatcher.ServerType.COUCHDB); final int numMockRemoteDocs = 20; // must be multiple of 10! final AtomicInteger numDocsPulledLocally = new AtomicInteger(0); MockDispatcher dispatcher = new MockDispatcher(); dispatcher.setServerType(serverType); int numDocsPerChangesResponse = numMockRemoteDocs / 10; MockWebServer server = MockHelper.getPreloadedPullTargetMockCouchDB(dispatcher, numMockRemoteDocs, numDocsPerChangesResponse); try { server.start(); final CountDownLatch receivedAllDocs = new CountDownLatch(1); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); final CountDownLatch replicationDoneSignal = new CountDownLatch(1); pullReplication.addChangeListener(new ReplicationFinishedObserver(replicationDoneSignal)); final CountDownLatch replicationIdleSignal = new CountDownLatch(1); ReplicationIdleObserver idleObserver = new ReplicationIdleObserver(replicationIdleSignal); pullReplication.addChangeListener(idleObserver); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { List<DocumentChange> changes = event.getChanges(); for (DocumentChange change : changes) { numDocsPulledLocally.addAndGet(1); } if (numDocsPulledLocally.get() == numMockRemoteDocs) { receivedAllDocs.countDown(); } } }); pullReplication.start(); // wait until we received all mock docs or timeout occurs boolean success = receivedAllDocs.await(60, TimeUnit.SECONDS); assertTrue(success); // make sure all docs in local db Map<String, Object> allDocs = database.getAllDocs(new QueryOptions()); Integer totalRows = (Integer) allDocs.get("total_rows"); List rows = (List) allDocs.get("rows"); assertEquals(numMockRemoteDocs, totalRows.intValue()); assertEquals(numMockRemoteDocs, rows.size()); // wait until idle success = replicationIdleSignal.await(30, TimeUnit.SECONDS); assertTrue(success); // cleanup / shutdown pullReplication.stop(); success = replicationDoneSignal.await(30, TimeUnit.SECONDS); assertTrue(success); long lastSeq = database.getLastSequenceNumber(); Log.i(TAG, "lastSequence = %d", lastSeq); // wait until the mock webserver receives a PUT checkpoint request with last do's sequence, // this avoids ugly and confusing exceptions in the logs. List<RecordedRequest> checkpointRequests = waitForPutCheckpointRequestWithSequence(dispatcher, numMockRemoteDocs - 1); validateCheckpointRequestsRevisions(checkpointRequests); } finally { if (shutdownMockWebserver) { assertTrue(MockHelper.shutdown(server, dispatcher)); } } Map<String, Object> returnVal = new HashMap<String, Object>(); returnVal.put("server", server); returnVal.put("dispatcher", dispatcher); return returnVal; } /** * This is essentially a regression test for a deadlock * that was happening when the LiveQuery#onDatabaseChanged() * was calling waitForUpdateThread(), but that thread was * waiting on connection to be released by the thread calling * waitForUpdateThread(). When the deadlock bug was present, * this test would trigger the deadlock and never finish. * <p/> * TODO: sporadic assertion failure when checking rev field of PUT checkpoint requests */ public void testPullerWithLiveQuery() throws Throwable { View view = database.getView("testPullerWithLiveQueryView"); view.setMapReduce(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { if (document.get("_id") != null) { emitter.emit(document.get("_id"), null); } } }, null, "1"); final CountDownLatch countDownLatch = new CountDownLatch(1); LiveQuery allDocsLiveQuery = view.createQuery().toLiveQuery(); allDocsLiveQuery.addChangeListener(new LiveQuery.ChangeListener() { @Override public void changed(LiveQuery.ChangeEvent event) { if (event.getError() != null) { throw new RuntimeException(event.getError()); } if (event.getRows().getCount() == 2) { countDownLatch.countDown(); } } }); // kick off live query allDocsLiveQuery.start(); // do pull replication against mock mockSinglePull(true, MockDispatcher.ServerType.SYNC_GW, true); // make sure we were called back with both docs boolean success = countDownLatch.await(30, TimeUnit.SECONDS); assertTrue(success); // clean up allDocsLiveQuery.stop(); } /** * Make sure that if a continuous push gets an error * pushing a doc, it will keep retrying it rather than giving up right away. * * @throws Exception */ public void testContinuousPushRetryBehavior() throws Exception { final int VALIDATION_RETRIES = 3; final int TEMP_RETRY_DELAY_MS = RemoteRequestRetry.RETRY_DELAY_MS; final int TEMP_RETRY_DELAY_SECONDS = ReplicationInternal.RETRY_DELAY_SECONDS; RemoteRequestRetry.RETRY_DELAY_MS = 5; // speed up test execution (inner loop retry delay) ReplicationInternal.RETRY_DELAY_SECONDS = 1; // speed up test execution (outer loop retry delay) try { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint GET response w/ 404 + respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- 503 errors MockResponse mockResponse = new MockResponse().setResponseCode(503); WrappedSmartMockResponse mockBulkDocs = new WrappedSmartMockResponse(mockResponse, false); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); server.start(); // create replication Replication replication = database.createPushReplication(server.url("/db").url()); replication.setContinuous(true); CountDownLatch replicationIdle = new CountDownLatch(1); ReplicationIdleObserver idleObserver = new ReplicationIdleObserver(replicationIdle); replication.addChangeListener(idleObserver); replication.start(); // wait until idle boolean success = replicationIdle.await(30, TimeUnit.SECONDS); assertTrue(success); replication.removeChangeListener(idleObserver); // create a doc in local db Document doc1 = createDocumentForPushReplication("doc1", null, null); // we should expect to at least see numAttempts attempts at doing POST to _bulk_docs // 1st attempt // numAttempts are number of times retry in 1 attempt. int numAttempts = RemoteRequestRetry.MAX_RETRIES + 1; // total number of attempts = 4 (1 initial + MAX_RETRIES) for (int i = 0; i < numAttempts; i++) { RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(request); dispatcher.takeRecordedResponseBlocking(request); } // outer retry loop for (int j = 0; j < VALIDATION_RETRIES; j++) { // inner retry loop for (int i = 0; i < numAttempts; i++) { RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(request); dispatcher.takeRecordedResponseBlocking(request); } } // gave up replication!!! stopReplication(replication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } finally { RemoteRequestRetry.RETRY_DELAY_MS = TEMP_RETRY_DELAY_MS; ReplicationInternal.RETRY_DELAY_SECONDS = TEMP_RETRY_DELAY_SECONDS; } } public void testMockSinglePush() throws Exception { boolean shutdownMockWebserver = true; mockSinglePush(shutdownMockWebserver, MockDispatcher.ServerType.SYNC_GW); } /** * Do a push replication * <p/> * - Create docs in local db * - One with no attachment * - One with small attachment * - One with large attachment */ private Map<String, Object> mockSinglePush(boolean shutdownMockWebserver, MockDispatcher.ServerType serverType) throws Exception { String doc1Id = "doc1"; String doc2Id = "doc2"; String doc3Id = "doc3"; String doc4Id = "doc4"; String doc2PathRegex = String.format(Locale.ENGLISH, "/db/%s.*", doc2Id); String doc3PathRegex = String.format(Locale.ENGLISH, "/db/%s.*", doc3Id); String doc2AttachName = "attachment.png"; String doc3AttachName = "attachment2.png"; String contentType = "image/png"; // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(serverType); try { server.start(); // add some documents Document doc1 = createDocumentForPushReplication(doc1Id, null, null); Document doc2 = createDocumentForPushReplication(doc2Id, doc2AttachName, contentType); Document doc3 = createDocumentForPushReplication(doc3Id, doc3AttachName, contentType); Document doc4 = createDocumentForPushReplication(doc4Id, null, null); doc4.delete(); // checkpoint GET response w/ 404 + respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(50); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // doc PUT responses for docs with attachments MockDocumentPut mockDoc2Put = new MockDocumentPut() .setDocId(doc2Id) .setRev(doc2.getCurrentRevisionId()); dispatcher.enqueueResponse(doc2PathRegex, mockDoc2Put.generateMockResponse()); MockDocumentPut mockDoc3Put = new MockDocumentPut() .setDocId(doc3Id) .setRev(doc3.getCurrentRevisionId()); dispatcher.enqueueResponse(doc3PathRegex, mockDoc3Put.generateMockResponse()); // run replication Replication replication = database.createPushReplication(server.url("/db").url()); replication.setContinuous(false); if (serverType != MockDispatcher.ServerType.SYNC_GW) { replication.setCreateTarget(true); Assert.assertTrue(replication.shouldCreateTarget()); } runReplication(replication); // make assertions about outgoing requests from replicator -> mock RecordedRequest getCheckpointRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHECKPOINT); assertTrue(getCheckpointRequest.getMethod().equals("GET")); assertTrue(getCheckpointRequest.getPath().matches(MockHelper.PATH_REGEX_CHECKPOINT)); RecordedRequest revsDiffRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_REVS_DIFF); assertTrue(MockHelper.getUtf8Body(revsDiffRequest).contains(doc1Id)); RecordedRequest bulkDocsRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_DOCS); assertTrue(MockHelper.getUtf8Body(bulkDocsRequest).contains(doc1Id)); Map<String, Object> bulkDocsJson = Manager.getObjectMapper().readValue(MockHelper.getUtf8Body(bulkDocsRequest), Map.class); Map<String, Object> doc4Map = MockBulkDocs.findDocById(bulkDocsJson, doc4Id); assertTrue(((Boolean) doc4Map.get("_deleted")).booleanValue() == true); String str = MockHelper.getUtf8Body(bulkDocsRequest); Log.i(TAG, str); assertFalse(MockHelper.getUtf8Body(bulkDocsRequest).contains(doc2Id)); RecordedRequest doc2putRequest = dispatcher.takeRequest(doc2PathRegex); CustomMultipartReaderDelegate delegate2 = new CustomMultipartReaderDelegate(); MultipartReader reader2 = new MultipartReader(doc2putRequest.getHeader("Content-Type"), delegate2); reader2.appendData(doc2putRequest.getBody().readByteArray()); String body2 = new String(delegate2.data, "UTF-8"); assertTrue(body2.contains(doc2Id)); assertFalse(body2.contains(doc3Id)); RecordedRequest doc3putRequest = dispatcher.takeRequest(doc3PathRegex); CustomMultipartReaderDelegate delegate3 = new CustomMultipartReaderDelegate(); MultipartReader reader3 = new MultipartReader(doc3putRequest.getHeader("Content-Type"), delegate3); reader3.appendData(doc3putRequest.getBody().readByteArray()); String body3 = new String(delegate3.data, "UTF-8"); assertTrue(body3.contains(doc3Id)); assertFalse(body3.contains(doc2Id)); // wait until the mock webserver receives a PUT checkpoint request int expectedLastSequence = 5; Log.d(TAG, "waiting for put checkpoint with lastSequence: %d", expectedLastSequence); List<RecordedRequest> checkpointRequests = waitForPutCheckpointRequestWithSequence(dispatcher, expectedLastSequence); Log.d(TAG, "done waiting for put checkpoint with lastSequence: %d", expectedLastSequence); validateCheckpointRequestsRevisions(checkpointRequests); // assert our local sequence matches what is expected String lastSequence = database.lastSequenceWithCheckpointId(replication.remoteCheckpointDocID()); assertEquals(Integer.toString(expectedLastSequence), lastSequence); // assert completed count makes sense assertEquals(replication.getChangesCount(), replication.getCompletedChangesCount()); } finally { // Shut down the server. Instances cannot be reused. if (shutdownMockWebserver) { assertTrue(MockHelper.shutdown(server, dispatcher)); } } Map<String, Object> returnVal = new HashMap<String, Object>(); returnVal.put("server", server); returnVal.put("dispatcher", dispatcher); return returnVal; } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/55 */ public void testContinuousPushReplicationGoesIdle() throws Exception { // make sure we are starting empty assertEquals(0, database.getLastSequenceNumber()); // add docs Map<String, Object> properties1 = new HashMap<String, Object>(); properties1.put("doc1", "testContinuousPushReplicationGoesIdle"); final Document doc1 = createDocWithProperties(properties1); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { server.start(); // checkpoint GET response w/ 404. also receives checkpoint PUT's MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // replication to do initial sync up - has to be continuous replication so the checkpoint id // matches the next continuous replication we're gonna do later. Replication firstPusher = database.createPushReplication(server.url("/db").url()); firstPusher.setContinuous(true); final String checkpointId = firstPusher.remoteCheckpointDocID(); // save the checkpoint id for later usage // start the continuous replication CountDownLatch replicationIdleSignal = new CountDownLatch(1); ReplicationIdleObserver replicationIdleObserver = new ReplicationIdleObserver(replicationIdleSignal); firstPusher.addChangeListener(replicationIdleObserver); firstPusher.start(); // wait until we get an IDLE event boolean successful = replicationIdleSignal.await(30, TimeUnit.SECONDS); assertTrue(successful); stopReplication(firstPusher); // wait until replication does PUT checkpoint with lastSequence=1 int expectedLastSequence = 1; waitForPutCheckpointRequestWithSeq(dispatcher, expectedLastSequence); // the last sequence should be "1" at this point. we will use this later final String lastSequence = database.lastSequenceWithCheckpointId(checkpointId); assertEquals("1", lastSequence); // start a second continuous replication Replication secondPusher = database.createPushReplication(server.url("/db").url()); secondPusher.setContinuous(true); final String secondPusherCheckpointId = secondPusher.remoteCheckpointDocID(); assertEquals(checkpointId, secondPusherCheckpointId); // remove current handler for the GET/PUT checkpoint request, and // install a new handler that returns the lastSequence from previous replication dispatcher.clearQueuedResponse(MockHelper.PATH_REGEX_CHECKPOINT); MockCheckpointGet mockCheckpointGet = new MockCheckpointGet(); mockCheckpointGet.setLastSequence(lastSequence); mockCheckpointGet.setRev("0-2"); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointGet); // start second replication replicationIdleSignal = new CountDownLatch(1); replicationIdleObserver = new ReplicationIdleObserver(replicationIdleSignal); secondPusher.addChangeListener(replicationIdleObserver); secondPusher.start(); // wait until we get an IDLE event successful = replicationIdleSignal.await(30, TimeUnit.SECONDS); assertTrue(successful); stopReplication(secondPusher); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/241 * <p/> * - Set the "retry time" to a short number * - Setup mock server to return 404 for all _changes requests * - Start continuous replication * - Sleep for 5X retry time * - Assert that we've received at least two requests to _changes feed * - Stop replication + cleanup */ public void testContinuousReplication404Changes() throws Exception { int previous = PullerInternal.CHANGE_TRACKER_RESTART_DELAY_MS; PullerInternal.CHANGE_TRACKER_RESTART_DELAY_MS = 5; try { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { server.start(); // mock checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // mock _changes response for (int i = 0; i < 100; i++) { MockResponse mockChangesFeed = new MockResponse(); MockHelper.set404NotFoundJson(mockChangesFeed); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed); } // create new replication int retryDelaySeconds = 1; Replication pull = database.createPullReplication(server.url("/db").url()); pull.setContinuous(true); // add done listener to replication CountDownLatch replicationDoneSignal = new CountDownLatch(1); ReplicationFinishedObserver replicationFinishedObserver = new ReplicationFinishedObserver(replicationDoneSignal); pull.addChangeListener(replicationFinishedObserver); // start the replication pull.start(); // wait until we get a few requests Log.d(TAG, "Waiting for a _changes request"); RecordedRequest changesReq = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHANGES); Log.d(TAG, "Got first _changes request, waiting for another _changes request"); changesReq = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHANGES); Log.d(TAG, "Got second _changes request, waiting for another _changes request"); changesReq = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHANGES); Log.d(TAG, "Got third _changes request, stopping replicator"); // the replication should still be running assertEquals(1, replicationDoneSignal.getCount()); // cleanup stopReplication(pull); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } finally { PullerInternal.CHANGE_TRACKER_RESTART_DELAY_MS = previous; } } /** * Verify that when a conflict is resolved on (mock) Sync Gateway * and a pull replication is done, the conflict is resolved locally. * <p/> * - Create local docs in conflict * - Simulate sync gw responses that resolve the conflict * - Do pull replication * - Assert conflict is resolved locally * <p/> * https://github.com/couchbase/couchbase-lite-java-core/issues/77 */ public void testRemoteConflictResolution() throws Exception { // Create a document with two conflicting edits. Document doc = database.createDocument(); SavedRevision rev1 = doc.createRevision().save(); SavedRevision rev2a = createRevisionWithRandomProps(rev1, false); SavedRevision rev2b = createRevisionWithRandomProps(rev1, true); // make sure we can query the db to get the conflict Query allDocsQuery = database.createAllDocumentsQuery(); allDocsQuery.setAllDocsMode(Query.AllDocsMode.ONLY_CONFLICTS); QueryEnumerator rows = allDocsQuery.run(); boolean foundDoc = false; assertEquals(1, rows.getCount()); for (Iterator<QueryRow> it = rows; it.hasNext(); ) { QueryRow row = it.next(); if (row.getDocument().getId().equals(doc.getId())) { foundDoc = true; } } assertTrue(foundDoc); // make sure doc in conflict assertTrue(doc.getConflictingRevisions().size() > 1); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); try { // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); int rev3PromotedGeneration = 3; String rev3PromotedDigest = "d46b"; String rev3Promoted = String.format(Locale.ENGLISH, "%d-%s", rev3PromotedGeneration, rev3PromotedDigest); int rev3DeletedGeneration = 3; String rev3DeletedDigest = "e768"; String rev3Deleted = String.format(Locale.ENGLISH, "%d-%s", rev3DeletedGeneration, rev3DeletedDigest); int seq = 4; // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); MockChangesFeed.MockChangedDoc mockChangedDoc = new MockChangesFeed.MockChangedDoc(); mockChangedDoc.setDocId(doc.getId()); mockChangedDoc.setSeq(seq); mockChangedDoc.setChangedRevIds(Arrays.asList(rev3Promoted, rev3Deleted)); mockChangesFeed.add(mockChangedDoc); MockResponse response = mockChangesFeed.generateMockResponse(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, response); // docRev3Promoted response MockDocumentGet.MockDocument docRev3Promoted = new MockDocumentGet.MockDocument(doc.getId(), rev3Promoted, seq); docRev3Promoted.setJsonMap(MockHelper.generateRandomJsonMap()); MockDocumentGet mockDocRev3PromotedGet = new MockDocumentGet(docRev3Promoted); Map<String, Object> rev3PromotedRevHistory = new HashMap<String, Object>(); rev3PromotedRevHistory.put("start", rev3PromotedGeneration); List ids = Arrays.asList( rev3PromotedDigest, RevisionInternal.digestFromRevID(rev2a.getId()), RevisionInternal.digestFromRevID(rev2b.getId()) ); rev3PromotedRevHistory.put("ids", ids); mockDocRev3PromotedGet.setRevHistoryMap(rev3PromotedRevHistory); dispatcher.enqueueResponse(docRev3Promoted.getDocPathRegex(), mockDocRev3PromotedGet.generateMockResponse()); // docRev3Deleted response MockDocumentGet.MockDocument docRev3Deleted = new MockDocumentGet.MockDocument(doc.getId(), rev3Deleted, seq); Map<String, Object> jsonMap = MockHelper.generateRandomJsonMap(); jsonMap.put("_deleted", true); docRev3Deleted.setJsonMap(jsonMap); MockDocumentGet mockDocRev3DeletedGet = new MockDocumentGet(docRev3Deleted); Map<String, Object> rev3DeletedRevHistory = new HashMap<String, Object>(); rev3DeletedRevHistory.put("start", rev3DeletedGeneration); ids = Arrays.asList( rev3DeletedDigest, RevisionInternal.digestFromRevID(rev2b.getId()), RevisionInternal.digestFromRevID(rev1.getId()) ); rev3DeletedRevHistory.put("ids", ids); mockDocRev3DeletedGet.setRevHistoryMap(rev3DeletedRevHistory); dispatcher.enqueueResponse(docRev3Deleted.getDocPathRegex(), mockDocRev3DeletedGet.generateMockResponse()); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); runReplication(pullReplication); assertNull(pullReplication.getLastError()); // assertions about outgoing requests RecordedRequest changesRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES); assertNotNull(changesRequest); RecordedRequest docRev3DeletedRequest = dispatcher.takeRequest(docRev3Deleted.getDocPathRegex()); assertNotNull(docRev3DeletedRequest); RecordedRequest docRev3PromotedRequest = dispatcher.takeRequest(docRev3Promoted.getDocPathRegex()); assertNotNull(docRev3PromotedRequest); // Make sure the conflict was resolved locally. assertEquals(1, doc.getConflictingRevisions().size()); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-android/issues/376 * <p/> * This test aims to demonstrate that when the changes feed returns purged documents the * replicator is able to fetch all other documents but unable to finish the replication * (STOPPED OR IDLE STATE) */ public void testChangesFeedWithPurgedDoc() throws Exception { //generate documents ids String doc1Id = "doc1-" + System.currentTimeMillis(); String doc2Id = "doc2-" + System.currentTimeMillis(); String doc3Id = "doc3-" + System.currentTimeMillis(); //generate mock documents final MockDocumentGet.MockDocument mockDocument1 = new MockDocumentGet.MockDocument( doc1Id, "1-a000", 1); mockDocument1.setJsonMap(MockHelper.generateRandomJsonMap()); final MockDocumentGet.MockDocument mockDocument2 = new MockDocumentGet.MockDocument( doc2Id, "1-b000", 2); mockDocument2.setJsonMap(MockHelper.generateRandomJsonMap()); final MockDocumentGet.MockDocument mockDocument3 = new MockDocumentGet.MockDocument( doc3Id, "1-c000", 3); mockDocument3.setJsonMap(MockHelper.generateRandomJsonMap()); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); try { //add response to _local request // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // Empty _all_docs response to pass unit tests dispatcher.enqueueResponse(MockHelper.PATH_REGEX_ALL_DOCS, new MockDocumentAllDocs()); //add response to _changes request // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument1)); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument2)); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument3)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // doc1 response MockDocumentGet mockDocumentGet1 = new MockDocumentGet(mockDocument1); dispatcher.enqueueResponse(mockDocument1.getDocPathRegex(), mockDocumentGet1.generateMockResponse()); // doc2 missing reponse MockResponse missingDocumentMockResponse = new MockResponse(); MockHelper.set404NotFoundJson(missingDocumentMockResponse); dispatcher.enqueueResponse(mockDocument2.getDocPathRegex(), missingDocumentMockResponse); // doc3 response MockDocumentGet mockDocumentGet3 = new MockDocumentGet(mockDocument3); dispatcher.enqueueResponse(mockDocument3.getDocPathRegex(), mockDocumentGet3.generateMockResponse()); // checkpoint PUT response MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // start mock server server.start(); //create url for replication URL baseUrl = server.url("/db").url(); //create replication Replication pullReplication = database.createPullReplication(baseUrl); pullReplication.setContinuous(false); //add change listener to notify when the replication is finished CountDownLatch replicationFinishedContCountDownLatch = new CountDownLatch(1); ReplicationFinishedObserver replicationFinishedObserver = new ReplicationFinishedObserver(replicationFinishedContCountDownLatch); pullReplication.addChangeListener(replicationFinishedObserver); //start replication pullReplication.start(); boolean success = replicationFinishedContCountDownLatch.await(100, TimeUnit.SECONDS); assertTrue(success); if (pullReplication.getLastError() != null) { Log.d(TAG, "Replication had error: " + pullReplication.getLastError()); } //assert document 1 was correctly pulled Document doc1 = database.getDocument(doc1Id); assertNotNull(doc1); assertNotNull(doc1.getCurrentRevision()); //assert it was impossible to pull doc2 Document doc2 = database.getDocument(doc2Id); assertNotNull(doc2); assertNull(doc2.getCurrentRevision()); //assert it was possible to pull doc3 Document doc3 = database.getDocument(doc3Id); assertNotNull(doc3); assertNotNull(doc3.getCurrentRevision()); // wait until the replicator PUT's checkpoint with mockDocument3's sequence waitForPutCheckpointRequestWithSeq(dispatcher, mockDocument3.getDocSeq()); //last saved seq must be equal to last pulled document seq String doc3Seq = Integer.toString(mockDocument3.getDocSeq()); String lastSequence = database.lastSequenceWithCheckpointId(pullReplication.remoteCheckpointDocID()); assertEquals(doc3Seq, lastSequence); } finally { //stop mock server assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Verify that validation blocks are called correctly for docs * pulled from the sync gateway. * <p/> * - Add doc to (mock) sync gateway * - Add validation function that will reject that doc * - Do a pull replication * - Assert that the doc does _not_ make it into the db */ public void testValidationBlockCalled() throws Throwable { final MockDocumentGet.MockDocument mockDocument = new MockDocumentGet.MockDocument("doc1", "1-3e28", 1); mockDocument.setJsonMap(MockHelper.generateRandomJsonMap()); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // doc response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDocument); dispatcher.enqueueResponse(mockDocument.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // checkpoint PUT response dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, new MockCheckpointPut()); // start mock server server.start(); // Add Validation block database.setValidation("testValidationBlockCalled", new Validator() { @Override public void validate(Revision newRevision, ValidationContext context) { if (newRevision.getDocument().getId().equals(mockDocument.getDocId())) { context.reject("Reject"); } } }); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); runReplication(pullReplication); waitForPutCheckpointRequestWithSeq(dispatcher, mockDocument.getDocSeq()); // assert doc is not in local db Document doc = database.getDocument(mockDocument.getDocId()); assertNull(doc.getCurrentRevision()); // doc should have been rejected by validation, and therefore not present } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * TODO: This test is incorrectly implemented with current implementation of CBL. * MockServer should not send 406 error. Need to fix. * * Attempting to reproduce couchtalk issue: * <p/> * https://github.com/couchbase/couchbase-lite-android/issues/312 * <p/> * - Start continuous puller against mock SG w/ 50 docs * - After every 10 docs received, restart replication * - Make sure all 50 docs are received and stored in local db * * @throws Exception */ public void testMockPullerRestart() throws Exception { final int numMockRemoteDocs = 20; // must be multiple of 10! final AtomicInteger numDocsPulledLocally = new AtomicInteger(0); MockDispatcher dispatcher = new MockDispatcher(); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); int numDocsPerChangesResponse = numMockRemoteDocs / 10; MockWebServer server = MockHelper.getPreloadedPullTargetMockCouchDB(dispatcher, numMockRemoteDocs, numDocsPerChangesResponse); try { server.start(); final CountDownLatch receivedAllDocs = new CountDownLatch(1); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); // it should go idle twice, hence countdown latch = 2 final CountDownLatch replicationIdleFirstTime = new CountDownLatch(1); final CountDownLatch replicationIdleSecondTime = new CountDownLatch(2); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { replicationIdleFirstTime.countDown(); replicationIdleSecondTime.countDown(); } } }); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { List<DocumentChange> changes = event.getChanges(); for (DocumentChange change : changes) { numDocsPulledLocally.addAndGet(1); } if (numDocsPulledLocally.get() == numMockRemoteDocs) { receivedAllDocs.countDown(); } } }); pullReplication.start(); // wait until we received all mock docs or timeout occurs boolean success = receivedAllDocs.await(60, TimeUnit.SECONDS); assertTrue(success); // wait until replication goes idle success = replicationIdleFirstTime.await(60, TimeUnit.SECONDS); assertTrue(success); pullReplication.restart(); // wait until replication goes idle again success = replicationIdleSecondTime.await(60, TimeUnit.SECONDS); assertTrue(success); stopReplication(pullReplication); } finally { // cleanup / shutdown assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-android/issues/247 */ public void testPushReplicationRecoverableError() throws Exception { boolean expectReplicatorError = false; runPushReplicationWithTransientError("HTTP/1.1 503 Service Unavailable", expectReplicatorError); } /** * https://github.com/couchbase/couchbase-lite-android/issues/247 */ public void testPushReplicationNonRecoverableError() throws Exception { boolean expectReplicatorError = true; runPushReplicationWithTransientError("HTTP/1.1 404 Not Found", expectReplicatorError); } /** * https://github.com/couchbase/couchbase-lite-android/issues/247 */ public void runPushReplicationWithTransientError(String status, boolean expectReplicatorError) throws Exception { String doc1Id = "doc1"; // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { server.start(); // add some documents Document doc1 = createDocumentForPushReplication(doc1Id, null, null); // checkpoint GET response w/ 404 + respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(50); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // 1st _bulk_docs response -- transient error MockResponse response = new MockResponse().setStatus(status); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, response); // 2nd _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // run replication Replication pusher = database.createPushReplication(server.url("/db").url()); pusher.setContinuous(false); runReplication(pusher); if (expectReplicatorError == true) { assertNotNull(pusher.getLastError()); } else { assertNull(pusher.getLastError()); } if (expectReplicatorError == false) { int expectedLastSequence = 1; Log.d(TAG, "waiting for put checkpoint with lastSequence: %d", expectedLastSequence); List<RecordedRequest> checkpointRequests = waitForPutCheckpointRequestWithSequence(dispatcher, expectedLastSequence); Log.d(TAG, "done waiting for put checkpoint with lastSequence: %d", expectedLastSequence); validateCheckpointRequestsRevisions(checkpointRequests); // assert our local sequence matches what is expected String lastSequence = database.lastSequenceWithCheckpointId(pusher.remoteCheckpointDocID()); assertEquals(Integer.toString(expectedLastSequence), lastSequence); // assert completed count makes sense assertEquals(pusher.getChangesCount(), pusher.getCompletedChangesCount()); } } finally { // Shut down the server. Instances cannot be reused. assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Test for the goOffline() method. */ public void testGoOffline() throws Exception { final int numMockDocsToServe = 2; // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); try { server.start(); // mock documents to be pulled MockDocumentGet.MockDocument mockDoc1 = new MockDocumentGet.MockDocument("doc1", "1-5e38", 1); mockDoc1.setJsonMap(MockHelper.generateRandomJsonMap()); mockDoc1.setAttachmentName("attachment.png"); MockDocumentGet.MockDocument mockDoc2 = new MockDocumentGet.MockDocument("doc2", "1-563b", 2); mockDoc2.setJsonMap(MockHelper.generateRandomJsonMap()); mockDoc2.setAttachmentName("attachment2.png"); // fake checkpoint PUT and GET response w/ 404 MockCheckpointPut fakeCheckpointResponse = new MockCheckpointPut(); fakeCheckpointResponse.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // _changes response with docs MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // next _changes response will block (eg, longpoll reponse with no changes to return) MockChangesFeed mockChangesFeedEmpty = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedEmpty.generateMockResponse()); // doc1 response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDoc1); dispatcher.enqueueResponse(mockDoc1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // doc2 response mockDocumentGet = new MockDocumentGet(mockDoc2); dispatcher.enqueueResponse(mockDoc2.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // create replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); // add a change listener final CountDownLatch idleCountdownLatch = new CountDownLatch(1); final CountDownLatch receivedAllDocs = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { Log.i(Log.TAG_SYNC, "event.getCompletedChangeCount() = " + event.getCompletedChangeCount()); if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { idleCountdownLatch.countDown(); } if (event.getCompletedChangeCount() == numMockDocsToServe) { receivedAllDocs.countDown(); } } }); // start replication pullReplication.start(); // wait until it goes into idle state boolean success = idleCountdownLatch.await(60, TimeUnit.SECONDS); assertTrue(success); // WORKAROUND: With CBL Java on Jenkins, Replicator becomes IDLE state before processing doc1. (NOT 100% REPRODUCIBLE) // NOTE: 03/20/2014 This is also observable with on Standard Android emulator with ARM. (NOT 100% REPRODUCIBLE) // TODO: Need to fix: https://github.com/couchbase/couchbase-lite-java-core/issues/446 // NOTE: Build.BRAND.equalsIgnoreCase("generic") is only for Android, not for regular Java. // So, till solve IDLE state issue, always wait 5 seconds. try { Thread.sleep(5 * 1000); } catch (Exception e) { } // put the replication offline putReplicationOffline(pullReplication); // at this point, we shouldn't have received all of the docs yet. assertTrue(receivedAllDocs.getCount() > 0); // return some more docs on _changes feed MockChangesFeed mockChangesFeed2 = new MockChangesFeed(); mockChangesFeed2.add(new MockChangesFeed.MockChangedDoc(mockDoc2)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed2.generateMockResponse()); // put the replication online (should see the new docs) putReplicationOnline(pullReplication); // wait until we receive all the docs success = receivedAllDocs.await(60, TimeUnit.SECONDS); assertTrue(success); // wait until we try to PUT a checkpoint request with doc2's sequence waitForPutCheckpointRequestWithSeq(dispatcher, mockDoc2.getDocSeq()); // make sure all docs in local db Map<String, Object> allDocs = database.getAllDocs(new QueryOptions()); Integer totalRows = (Integer) allDocs.get("total_rows"); List rows = (List) allDocs.get("rows"); assertEquals(numMockDocsToServe, totalRows.intValue()); assertEquals(numMockDocsToServe, rows.size()); // cleanup stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/253 */ public void testReplicationOnlineExtraneousChangeTrackers() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); try { // add sticky checkpoint GET response w/ 404 MockCheckpointGet fakeCheckpointResponse = new MockCheckpointGet(); fakeCheckpointResponse.set404(true); fakeCheckpointResponse.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // add sticky _changes response to feed=longpoll that just blocks for 60 seconds to emulate // server that doesn't have any new changes MockChangesFeedNoResponse mockChangesFeedNoResponse = new MockChangesFeedNoResponse(); mockChangesFeedNoResponse.setDelayMs(60 * 1000); mockChangesFeedNoResponse.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES_LONGPOLL, mockChangesFeedNoResponse); // add _changes response to feed=normal that returns empty _changes feed immediately MockChangesFeed mockChangesFeed = new MockChangesFeed(); MockResponse mockResponse = mockChangesFeed.generateMockResponse(); for (int i = 0; i < 500; i++) { // TODO: use setSticky instead of workaround to add a ton of mock responses dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES_NORMAL, new WrappedSmartMockResponse(mockResponse)); } // start mock server server.start(); //create url for replication URL baseUrl = server.url("/db").url(); //create replication final Replication pullReplication = database.createPullReplication(baseUrl); pullReplication.setContinuous(true); pullReplication.start(); // wait until we get a request to the _changes feed RecordedRequest changesReq = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHANGES_LONGPOLL); assertNotNull(changesReq); putReplicationOffline(pullReplication); // at this point since we called takeRequest earlier, our recorded _changes request queue should be empty assertNull(dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES_LONGPOLL)); // put replication online 10 times for (int i = 0; i < 10; i++) { pullReplication.goOnline(); } // sleep for a while to give things a chance to start Log.d(TAG, "sleeping for 2 seconds"); Thread.sleep(2 * 1000); Log.d(TAG, "done sleeping"); // how many _changes feed requests has the replicator made since going online? int numChangesRequests = 0; while ((changesReq = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES_LONGPOLL)) != null) { Log.d(TAG, "changesReq: %s", changesReq); numChangesRequests += 1; } // assert that there was only one _changes feed request assertEquals(1, numChangesRequests); // shutdown stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Test goOffline() method in the context of a continuous pusher. * <p/> * - 1. Add a local document * - 2. Kick off continuous push replication * - 3. Wait for document to be pushed * - 4. Call goOffline() * - 6. Call goOnline() * - 5. Add a 2nd local document * - 7. Wait for 2nd document to be pushed * * @throws Exception */ public void testGoOfflinePusher() throws Exception { int previous = RemoteRequestRetry.RETRY_DELAY_MS; RemoteRequestRetry.RETRY_DELAY_MS = 5; try { // 1. Add a local document Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testGoOfflinePusher", "1"); Document doc1 = createDocumentWithProperties(database, properties); // create mock server MockWebServer server = new MockWebServer(); MockDispatcher dispatcher = new MockDispatcher(); try { server.setDispatcher(dispatcher); server.start(); // checkpoint PUT response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // 2. Kick off continuous push replication Replication replicator = database.createPushReplication(server.url("/db").url()); replicator.setContinuous(true); CountDownLatch replicationIdleSignal = new CountDownLatch(1); ReplicationIdleObserver replicationIdleObserver = new ReplicationIdleObserver(replicationIdleSignal); replicator.addChangeListener(replicationIdleObserver); replicator.start(); // 3. Wait for document to be pushed // wait until replication goes idle boolean successful = replicationIdleSignal.await(30, TimeUnit.SECONDS); assertTrue(successful); // wait until mock server gets the checkpoint PUT request boolean foundCheckpointPut = false; String expectedLastSequence = "1"; while (!foundCheckpointPut) { RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); if (request.getMethod().equals("PUT")) { foundCheckpointPut = true; Buffer clone = request.getBody().clone(); String body = clone.readUtf8(); Assert.assertTrue(body.indexOf(expectedLastSequence) != -1); // wait until mock server responds to the checkpoint PUT request dispatcher.takeRecordedResponseBlocking(request); } } // make some assertions about the outgoing _bulk_docs requests for first doc RecordedRequest bulkDocsRequest1 = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(bulkDocsRequest1); assertBulkDocJsonContainsDoc(bulkDocsRequest1, doc1); // 4. Call goOffline() putReplicationOffline(replicator); // 5. Add a 2nd local document properties = new HashMap<String, Object>(); properties.put("testGoOfflinePusher", "2"); Document doc2 = createDocumentWithProperties(database, properties); // make sure if push replicator does not send request during offline. try { Thread.sleep(1000 * 3); } catch (Exception ex) { } // make sure not receive _bulk_docs during offline. RecordedRequest bulkDocsRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_DOCS); assertNull(bulkDocsRequest); // 6. Call goOnline() putReplicationOnline(replicator); // wait until mock server gets the 2nd checkpoint PUT request foundCheckpointPut = false; expectedLastSequence = "2"; while (!foundCheckpointPut) { RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); if (request.getMethod().equals("PUT")) { foundCheckpointPut = true; Buffer clone = request.getBody().clone(); String body = clone.readUtf8(); Assert.assertTrue(body.indexOf(expectedLastSequence) != -1); // wait until mock server responds to the checkpoint PUT request dispatcher.takeRecordedResponseBlocking(request); } } // make some assertions about the outgoing _bulk_docs requests for second doc RecordedRequest bulkDocsRequest2 = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(bulkDocsRequest2); assertBulkDocJsonContainsDoc(bulkDocsRequest2, doc2); // cleanup stopReplication(replicator); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } finally { RemoteRequestRetry.RETRY_DELAY_MS = previous; } } private void putReplicationOffline(Replication replication) throws InterruptedException { Log.d(Log.TAG, "putReplicationOffline: %s", replication); // this was a useless test, the replication wasn't even started final CountDownLatch wentOffline = new CountDownLatch(1); Replication.ChangeListener changeListener = new ReplicationOfflineObserver(wentOffline); replication.addChangeListener(changeListener); replication.goOffline(); boolean succeeded = wentOffline.await(30, TimeUnit.SECONDS); assertTrue(succeeded); replication.removeChangeListener(changeListener); Log.d(Log.TAG, "/putReplicationOffline: %s", replication); } private void putReplicationOnline(Replication replication) throws InterruptedException { Log.d(Log.TAG, "putReplicationOnline: %s", replication); // this was a useless test, the replication wasn't even started final CountDownLatch wentOnline = new CountDownLatch(1); Replication.ChangeListener changeListener = new ReplicationRunningObserver(wentOnline); replication.addChangeListener(changeListener); replication.goOnline(); boolean succeeded = wentOnline.await(30, TimeUnit.SECONDS); assertTrue(succeeded); replication.removeChangeListener(changeListener); Log.d(Log.TAG, "/putReplicationOnline: %s", replication); } /** * Verify that when a replication runs into an auth error, it stops * and the lastError() method returns that error. */ public void testReplicatorErrorStatus() throws Exception { // create MockWebServer and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // fake _session response MockSessionGet mockSessionGet = new MockSessionGet(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_SESSION, mockSessionGet.generateMockResponse()); // fake _facebook response MockFacebookAuthPost mockFacebookAuthPost = new MockFacebookAuthPost(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_FACEBOOK_AUTH, mockFacebookAuthPost.generateMockResponseForError()); // start mock server server.start(); // register bogus fb token Authenticator facebookAuthenticator = AuthenticatorFactory.createFacebookAuthenticator("fake_access_token"); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setAuthenticator(facebookAuthenticator); pullReplication.setContinuous(false); runReplication(pullReplication); // run replicator and make sure it has an error assertNotNull(pullReplication.getLastError()); assertTrue(pullReplication.getLastError() instanceof RemoteRequestResponseException); assertEquals(401 /* unauthorized */, ((RemoteRequestResponseException) pullReplication.getLastError()).getCode()); // assert that the replicator sent the requests we expected it to send RecordedRequest sessionReqeust = dispatcher.takeRequest(MockHelper.PATH_REGEX_SESSION); assertNotNull(sessionReqeust); RecordedRequest facebookRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_FACEBOOK_AUTH); assertNotNull(facebookRequest); dispatcher.verifyAllRecordedRequestsTaken(); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } public void testGetReplicatorWithCustomHeader() throws Throwable { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); server.start(); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("source", DEFAULT_TEST_DB); // target with custom headers (cookie) Map<String, Object> headers = new HashMap<String, Object>(); String coolieVal = "SyncGatewaySession=c38687c2696688a"; headers.put("Cookie", coolieVal); Map<String, Object> targetProperties = new HashMap<String, Object>(); targetProperties.put("url", server.url("/db").url().toExternalForm()); targetProperties.put("headers", headers); properties.put("target", targetProperties); Replication replicator = manager.getReplicator(properties); assertNotNull(replicator); assertEquals(server.url("/db").url().toExternalForm(), replicator.getRemoteUrl().toExternalForm()); assertTrue(!replicator.isPull()); assertFalse(replicator.isContinuous()); assertFalse(replicator.isRunning()); // https://github.com/couchbase/couchbase-lite-java-core/pull/1618 // Cookie value is removed from headers. Instead, the Cookie is stored in the CookieJar. //assertTrue(replicator.getHeaders().containsKey("Cookie")); //assertEquals(replicator.getHeaders().get("Cookie"), coolieVal); // add replication observer CountDownLatch replicationDoneSignal = new CountDownLatch(1); ReplicationFinishedObserver replicationFinishedObserver = new ReplicationFinishedObserver(replicationDoneSignal); replicator.addChangeListener(replicationFinishedObserver); // start the replicator Log.d(TAG, "Starting replicator " + replicator); replicator.start(); final CountDownLatch replicationStarted = new CountDownLatch(1); replicator.addChangeListener(new ReplicationRunningObserver(replicationStarted)); boolean success = replicationStarted.await(30, TimeUnit.SECONDS); assertTrue(success); // now lets lookup existing replicator and stop it Log.d(TAG, "Looking up replicator"); properties.put("cancel", true); Replication activeReplicator = manager.getReplicator(properties); Log.d(TAG, "Found replicator " + activeReplicator + " and calling stop()"); activeReplicator.stop(); Log.d(TAG, "called stop(), waiting for it to finish"); // wait for replication to finish boolean didNotTimeOut = replicationDoneSignal.await(180, TimeUnit.SECONDS); Log.d(TAG, "replicationDoneSignal.await done, didNotTimeOut: " + didNotTimeOut); assertTrue(didNotTimeOut); assertFalse(activeReplicator.isRunning()); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } public void testGetReplicator() throws Throwable { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); server.start(); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("source", DEFAULT_TEST_DB); properties.put("target", server.url("/db").url().toExternalForm()); Replication replicator = manager.getReplicator(properties); assertNotNull(replicator); assertEquals(server.url("/db").url().toExternalForm(), replicator.getRemoteUrl().toExternalForm()); assertTrue(!replicator.isPull()); assertFalse(replicator.isContinuous()); assertFalse(replicator.isRunning()); // add replication observer CountDownLatch replicationDoneSignal = new CountDownLatch(1); ReplicationFinishedObserver replicationFinishedObserver = new ReplicationFinishedObserver(replicationDoneSignal); replicator.addChangeListener(replicationFinishedObserver); // start the replicator Log.d(TAG, "Starting replicator " + replicator); replicator.start(); final CountDownLatch replicationStarted = new CountDownLatch(1); replicator.addChangeListener(new ReplicationRunningObserver(replicationStarted)); boolean success = replicationStarted.await(30, TimeUnit.SECONDS); assertTrue(success); // now lets lookup existing replicator and stop it Log.d(TAG, "Looking up replicator"); properties.put("cancel", true); Replication activeReplicator = manager.getReplicator(properties); Log.d(TAG, "Found replicator " + activeReplicator + " and calling stop()"); activeReplicator.stop(); Log.d(TAG, "called stop(), waiting for it to finish"); // wait for replication to finish boolean didNotTimeOut = replicationDoneSignal.await(180, TimeUnit.SECONDS); Log.d(TAG, "replicationDoneSignal.await done, didNotTimeOut: " + didNotTimeOut); assertTrue(didNotTimeOut); assertFalse(activeReplicator.isRunning()); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * When the server returns a 409 error to a PUT checkpoint response, make * sure it does the right thing: * - Pull latest remote checkpoint * - Try to push checkpiont again (this time passing latest rev) * * @throws Exception */ public void testPutCheckpoint409Recovery() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // mock documents to be pulled MockDocumentGet.MockDocument mockDoc1 = new MockDocumentGet.MockDocument("doc1", "1-5e38", 1); mockDoc1.setJsonMap(MockHelper.generateRandomJsonMap()); // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // doc1 response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDoc1); dispatcher.enqueueResponse(mockDoc1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // respond with 409 error to mock checkpoint PUT MockResponse checkpointResponse409 = new MockResponse(); checkpointResponse409.setStatus("HTTP/1.1 409 CONFLICT"); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, checkpointResponse409); // the replicator should then try to do a checkpoint GET, and in this case // it should return a value with a rev id MockCheckpointGet mockCheckpointGet = new MockCheckpointGet(); mockCheckpointGet.setOk("true"); mockCheckpointGet.setRev("0-1"); mockCheckpointGet.setLastSequence("0"); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointGet); // the replicator should then try a checkpoint PUT again // and we should respond with a 201 MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); // I had to set this to continuous, because in a one-shot replication it tries to // save the checkpoint asynchronously as the replicator is shutting down, which // breaks the retry logic in the case a 409 conflict is returned by server. pullReplication.setContinuous(true); pullReplication.start(); // we should have gotten two requests to PATH_REGEX_CHECKPOINT: // PUT -> 409 Conflict // PUT -> 201 Created for (int i = 1; i <= 2; i++) { Log.v(TAG, "waiting for PUT checkpoint: %d", i); waitForPutCheckpointRequestWithSeq(dispatcher, mockDoc1.getDocSeq()); Log.d(TAG, "got PUT checkpoint: %d", i); } stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Verify that Validation based Rejects revert the entire batch that the document is in * even if one of the documents fail the validation. * <p/> * https://github.com/couchbase/couchbase-lite-java-core/issues/242 * * @throws Exception */ public void testVerifyPullerInsertsDocsWithValidation() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); // Empty _all_docs response to pass unit tests dispatcher.enqueueResponse(MockHelper.PATH_REGEX_ALL_DOCS, new MockDocumentAllDocs()); MockWebServer server = MockHelper.getPreloadedPullTargetMockCouchDB(dispatcher, 2, 2); try { server.start(); // Setup validation to reject document with id: doc1 database.setValidation("validateOnlyDoc1", new Validator() { @Override public void validate(Revision newRevision, ValidationContext context) { if ("doc1".equals(newRevision.getDocument().getId())) { context.reject(); } } }); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); runReplication(pullReplication); assertNotNull(database); // doc1 should not be in the store because of validation assertNull(database.getExistingDocument("doc1")); // doc0 should be in the store, but it wont be because of the bug. assertNotNull(database.getExistingDocument("doc0")); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Make sure calling puller.setChannels() causes the changetracker to send the correct * request to the sync gateway. * <p/> * https://github.com/couchbase/couchbase-lite-java-core/issues/292 */ public void testChannelsFilter() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setChannels(Arrays.asList("foo", "bar")); runReplication(pullReplication); // make assertions about outgoing requests from replicator -> mock RecordedRequest getChangesFeedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES); assertTrue(getChangesFeedRequest.getMethod().equals("POST")); String body = getChangesFeedRequest.getUtf8Body(); Map<String, Object> jsonMap = Manager.getObjectMapper().readValue(body, Map.class); assertTrue(jsonMap.containsKey("filter")); String filter = (String) jsonMap.get("filter"); assertEquals("sync_gateway/bychannel", filter); assertTrue(jsonMap.containsKey("channels")); String channels = (String) jsonMap.get("channels"); assertTrue(channels.contains("foo")); assertTrue(channels.contains("bar")); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } // // Channel info is discarded by restarting the replicator // https://github.com/couchbase/couchbase-lite-android/issues/887 // public void testRestartWithChannels() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setChannels(Arrays.asList("foo", "bar")); // 0: initial, 1: restarted for(int i = 0; i < 2; i++) { // run one shot pull replication runReplication(pullReplication); // make assertions about outgoing requests from replicator -> mock RecordedRequest getChangesFeedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES); assertTrue(getChangesFeedRequest.getMethod().equals("POST")); String body = getChangesFeedRequest.getUtf8Body(); Map<String, Object> jsonMap = Manager.getObjectMapper().readValue(body, Map.class); assertTrue(jsonMap.containsKey("filter")); String filter = (String) jsonMap.get("filter"); assertEquals("sync_gateway/bychannel", filter); assertTrue(jsonMap.containsKey("channels")); String channels = (String) jsonMap.get("channels"); assertTrue(channels.contains("foo")); assertTrue(channels.contains("bar")); } } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * - Start continuous pull * - Mockwebserver responds that there are no changes * - Assert that puller goes into IDLE state * <p/> * https://github.com/couchbase/couchbase-lite-android/issues/445 */ public void testContinuousPullEntersIdleState() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // add non-sticky changes response that returns no changes MockChangesFeed mockChangesFeed = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // add sticky _changes response that just blocks for 60 seconds to emulate // server that doesn't have any new changes MockChangesFeedNoResponse mockChangesFeedNoResponse = new MockChangesFeedNoResponse(); mockChangesFeedNoResponse.setDelayMs(60 * 1000); mockChangesFeedNoResponse.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedNoResponse); server.start(); // create pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); final CountDownLatch enteredIdleState = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition().getDestination() == ReplicationState.IDLE) { enteredIdleState.countDown(); } } }); // start pull replication pullReplication.start(); boolean success = enteredIdleState.await(30, TimeUnit.SECONDS); assertTrue(success); Log.d(TAG, "Got IDLE event, stopping replication"); stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Spotted in https://github.com/couchbase/couchbase-lite-java-core/issues/313 * But there is another ticket that is linked off 313 */ public void failingTestMockPullBulkDocsSyncGw() throws Exception { mockPullBulkDocs(MockDispatcher.ServerType.SYNC_GW); } public void mockPullBulkDocs(MockDispatcher.ServerType serverType) throws Exception { // set INBOX_CAPACITY to a smaller value so that processing times don't skew the test int defaultCapacity = ReplicationInternal.INBOX_CAPACITY; ReplicationInternal.INBOX_CAPACITY = 10; int defaultDelay = ReplicationInternal.PROCESSOR_DELAY; ReplicationInternal.PROCESSOR_DELAY = ReplicationInternal.PROCESSOR_DELAY * 10; // serve 25 mock docs int numMockDocsToServe = (ReplicationInternal.INBOX_CAPACITY * 2) + (ReplicationInternal.INBOX_CAPACITY / 2); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(serverType); try { // mock documents to be pulled List<MockDocumentGet.MockDocument> mockDocs = MockHelper.getMockDocuments(numMockDocsToServe); // respond to all GET (responds with 404) and PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); for (MockDocumentGet.MockDocument mockDocument : mockDocs) { mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument)); } dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // individual doc responses (expecting it to call _bulk_docs, but just in case) for (MockDocumentGet.MockDocument mockDocument : mockDocs) { MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDocument); dispatcher.enqueueResponse(mockDocument.getDocPathRegex(), mockDocumentGet.generateMockResponse()); } // _bulk_get response MockDocumentBulkGet mockBulkGet = new MockDocumentBulkGet(); for (MockDocumentGet.MockDocument mockDocument : mockDocs) { mockBulkGet.addDocument(mockDocument); } mockBulkGet.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_GET, mockBulkGet); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); runReplication(pullReplication, 3 * 60); assertTrue(pullReplication.getLastError() == null); // wait until it pushes checkpoint of last doc MockDocumentGet.MockDocument lastDoc = mockDocs.get(mockDocs.size() - 1); waitForPutCheckpointRequestWithSequence(dispatcher, lastDoc.getDocSeq()); // dump out the outgoing requests for bulk docs BlockingQueue<RecordedRequest> bulkGetRequests = dispatcher.getRequestQueueSnapshot(MockHelper.PATH_REGEX_BULK_GET); Iterator<RecordedRequest> iterator = bulkGetRequests.iterator(); boolean first = true; while (iterator.hasNext()) { RecordedRequest request = iterator.next(); byte[] body = MockHelper.getUncompressedBody(request); Map<String, Object> jsonMap = MockHelper.getJsonMapFromRequest(body); List docs = (List) jsonMap.get("docs"); Log.w(TAG, "bulk get request: %s had %d docs", request, docs.size()); // except first one and last one, docs.size() should be (neary) equal with INBOX_CAPACTITY. if (iterator.hasNext() && !first) { // the bulk docs requests except for the last one should have max number of docs // relax this a bit, so that it at least has to have greater than or equal to half max number of docs assertTrue(docs.size() >= (ReplicationInternal.INBOX_CAPACITY / 2)); if (docs.size() != ReplicationInternal.INBOX_CAPACITY) { Log.w(TAG, "docs.size() %d != ReplicationInternal.INBOX_CAPACITY %d", docs.size(), ReplicationInternal.INBOX_CAPACITY); } } first = false; } } finally { ReplicationInternal.INBOX_CAPACITY = defaultCapacity; ReplicationInternal.PROCESSOR_DELAY = defaultDelay; assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Make sure that after trying /db/_session, it should try /_session. * <p/> * Currently there is a bug where it tries /db/_session, and then * tries /db_session. * <p/> * https://github.com/couchbase/couchbase-lite-java-core/issues/208 */ public void testCheckSessionAtPath() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); try { // session GET response w/ 404 to /db/_session MockResponse fakeSessionResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeSessionResponse); WrappedSmartMockResponse wrappedSmartMockResponse = new WrappedSmartMockResponse(fakeSessionResponse); wrappedSmartMockResponse.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_SESSION, wrappedSmartMockResponse); // session GET response w/ 200 OK to /_session MockResponse fakeSessionResponse2 = new MockResponse(); Map<String, Object> responseJson = new HashMap<String, Object>(); Map<String, Object> userCtx = new HashMap<String, Object>(); userCtx.put("name", "foo"); responseJson.put("userCtx", userCtx); fakeSessionResponse2.setBody(new String(Manager.getObjectMapper().writeValueAsBytes(responseJson))); MockHelper.set200OKJson(fakeSessionResponse2); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_SESSION_COUCHDB, fakeSessionResponse2); // respond to all GET/PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setAuthenticator(new FacebookAuthorizer("justinbieber@glam.co")); CountDownLatch replicationDoneSignal = new CountDownLatch(1); ReplicationFinishedObserver replicationFinishedObserver = new ReplicationFinishedObserver(replicationDoneSignal); pullReplication.addChangeListener(replicationFinishedObserver); pullReplication.start(); // it should first try /db/_session dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_SESSION); // and then it should fallback to /_session dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_SESSION_COUCHDB); boolean success = replicationDoneSignal.await(30, TimeUnit.SECONDS); Assert.assertTrue(success); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * - Start one shot replication * - Changes feed request returns error * - Change tracker stops * - Replication stops -- make sure ChangeListener gets error * <p/> * https://github.com/couchbase/couchbase-lite-java-core/issues/334 */ public void testChangeTrackerError() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint GET response w/ 404 + respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // 404 response to _changes feed (sticky) MockResponse mockChangesFeed = new MockResponse(); MockHelper.set404NotFoundJson(mockChangesFeed); WrappedSmartMockResponse wrapped = new WrappedSmartMockResponse(mockChangesFeed); wrapped.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, wrapped); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); final CountDownLatch changeEventError = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getError() != null) { changeEventError.countDown(); } } }); runReplication(pullReplication); Assert.assertTrue(pullReplication.getLastError() != null); boolean success = changeEventError.await(5, TimeUnit.SECONDS); Assert.assertTrue(success); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/575 */ public void testRestartWithStoppedReplicator() throws Exception { MockDispatcher dispatcher = new MockDispatcher(); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); MockWebServer server = MockHelper.getPreloadedPullTargetMockCouchDB(dispatcher, 0, 0); try { server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); // it should go idle twice, hence countdown latch = 2 final CountDownLatch replicationIdleFirstTime = new CountDownLatch(1); final CountDownLatch replicationIdleSecondTime = new CountDownLatch(2); final CountDownLatch replicationStoppedFirstTime = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { Log.i(Log.TAG, "IDLE"); replicationIdleFirstTime.countDown(); replicationIdleSecondTime.countDown(); } else if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.STOPPED) { Log.i(Log.TAG, "STOPPED"); replicationStoppedFirstTime.countDown(); } } }); pullReplication.start(); // wait until replication goes idle boolean success = replicationIdleFirstTime.await(60, TimeUnit.SECONDS); assertTrue(success); pullReplication.stop(); // wait until replication stop success = replicationStoppedFirstTime.await(60, TimeUnit.SECONDS); assertTrue(success); pullReplication.restart(); // wait until replication goes idle again success = replicationIdleSecondTime.await(60, TimeUnit.SECONDS); assertTrue(success); stopReplication(pullReplication); } finally { // cleanup / shutdown assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/696 * in Unit-Tests/Replication_Tests.m * - (void)test17_RemovedRevision */ public void test17_RemovedRevision() throws Exception { MockDispatcher dispatcher = new MockDispatcher(); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); MockWebServer server = MockHelper.getMockWebServer(dispatcher); try { // checkpoint GET response w/ 404. also receives checkpoint PUT's MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); server.start(); Document doc = database.getDocument("doc1"); UnsavedRevision unsaved = doc.createRevision(); Map<String, Object> props = new HashMap<String, Object>(); props.put("_removed", true); unsaved.setProperties(props); SavedRevision rev = unsaved.save(); assertNotNull(rev); // create and start push replication Replication push = database.createPushReplication(server.url("/db").url()); CountDownLatch latch = new CountDownLatch(1); push.addChangeListener(new ReplicationFinishedObserver(latch)); push.start(); assertTrue(push.isDocumentPending(doc)); assertTrue(latch.await(30, TimeUnit.SECONDS)); assertNull(push.lastError); assertEquals(0, push.getCompletedChangesCount()); assertEquals(0, push.getChangesCount()); assertFalse(push.isDocumentPending(doc)); } finally { // cleanup / shutdown assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/696 * https://github.com/couchbase/couchbase-lite-java-core/issues/1396 * in Unit-Tests/Replication_Tests.m * - (void)test18_PendingDocumentIDs */ public void test18_PendingDocumentIDs() throws Exception { // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); server.setDispatcher(dispatcher); try { server.start(); // checkpoint GET response w/ 404 + respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(50); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // Push replication: Replication repl = database.createPushReplication(server.url("/db").url()); assertNull(repl.getPendingDocumentIDs()); assertTrue(database.runInTransaction( new TransactionalTask() { @Override public boolean run() { for (int i = 1; i <= 10; i++) { Document doc = database.getDocument(String.format(Locale.ENGLISH, "doc-%d", i)); Map<String, Object> props = new HashMap<String, Object>(); props.put("index", i); props.put("bar", false); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { fail(e.getMessage()); } } return true; } } )); assertEquals(10, repl.getPendingDocumentIDs().size()); assertTrue(repl.isDocumentPending(database.getDocument("doc-1"))); runReplication(repl); assertNull(repl.getPendingDocumentIDs()); assertFalse(repl.isDocumentPending(database.getDocument("doc-1"))); // Add another set of documents: assertTrue(database.runInTransaction( new TransactionalTask() { @Override public boolean run() { for (int i = 11; i <= 20; i++) { Document doc = database.getDocument(String.format(Locale.ENGLISH, "doc-%d", i)); Map<String, Object> props = new HashMap<String, Object>(); props.put("index", i); props.put("bar", false); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { fail(e.getMessage()); } } return true; } } )); // Make sure newly-added documents are considered pending: (#1396) assertTrue(repl.isDocumentPending(database.getDocument("doc-11"))); assertEquals(10, repl.getPendingDocumentIDs().size()); // Create a new replicator: repl = database.createPushReplication(server.url("/db").url()); assertNotNull(repl.getPendingDocumentIDs()); assertEquals(10, repl.getPendingDocumentIDs().size()); assertTrue(repl.isDocumentPending(database.getDocument("doc-11"))); assertFalse(repl.isDocumentPending(database.getDocument("doc-1"))); // pull replication repl = database.createPullReplication(server.url("/db").url()); assertNull(repl.getPendingDocumentIDs()); runReplication(repl); assertNull(repl.getPendingDocumentIDs()); } finally { // cleanup / shutdown assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/328 * <p/> * Without bug fix, we observe extra PUT /{db}/_local/xxx for each _bulk_docs request * <p/> * 1. Create 200 docs * 2. Start push replicator * 3. GET /{db}/_local/xxx * 4. PUSH /{db}/_revs_diff x 2 * 5. PUSH /{db}/_bulk_docs x 2 * 6. PUT /{db}/_local/xxx */ public void testExcessiveCheckpointingDuringPushReplication() throws Exception { final int NUM_DOCS = 199; List<Document> docs = new ArrayList<Document>(); // 1. Add more than 100 docs, as chunk size is 100 for (int i = 0; i < NUM_DOCS; i++) { Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testExcessiveCheckpointingDuringPushReplication", String.valueOf(i)); Document doc = createDocumentWithProperties(database, properties); docs.add(doc); } // create mock server MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = new MockWebServer(); server.setDispatcher(dispatcher); try { server.start(); // checkpoint GET response -> error // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // checkpoint PUT response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // 2. Kick off continuous push replication Replication replicator = database.createPushReplication(server.url("/db").url()); replicator.setContinuous(true); CountDownLatch replicationIdleSignal = new CountDownLatch(1); ReplicationIdleObserver replicationIdleObserver = new ReplicationIdleObserver(replicationIdleSignal); replicator.addChangeListener(replicationIdleObserver); replicator.start(); // 3. Wait for document to be pushed // NOTE: (Not 100% reproducible) With CBL Java on Jenkins (Super slow environment), // Replicator becomes IDLE between batches for this case, after 100 push replicated. // TODO: Need to investigate // wait until replication goes idle boolean successful = replicationIdleSignal.await(60, TimeUnit.SECONDS); assertTrue(successful); // wait until mock server gets the checkpoint PUT request boolean foundCheckpointPut = false; String expectedLastSequence = String.valueOf(NUM_DOCS); while (!foundCheckpointPut) { RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); if (request.getMethod().equals("PUT")) { foundCheckpointPut = true; Buffer clone = request.getBody().clone(); String body = clone.readUtf8(); Log.i("testExcessiveCheckpointingDuringPushReplication", "body => " + body); // TODO: this is not valid if device can not handle all replication data at once if (System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik")) { assertTrue(body.indexOf(expectedLastSequence) != -1); } // wait until mock server responds to the checkpoint PUT request dispatcher.takeRecordedResponseBlocking(request); } } // make some assertions about the outgoing _bulk_docs requests RecordedRequest bulkDocsRequest1 = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(bulkDocsRequest1); if (System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik")) { RecordedRequest bulkDocsRequest2 = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(bulkDocsRequest2); // TODO: this is not valid if device can not handle all replication data at once // order may not be guaranteed assertTrue(isBulkDocJsonContainsDoc(bulkDocsRequest1, docs.get(0)) || isBulkDocJsonContainsDoc(bulkDocsRequest2, docs.get(0))); assertTrue(isBulkDocJsonContainsDoc(bulkDocsRequest1, docs.get(100)) || isBulkDocJsonContainsDoc(bulkDocsRequest2, docs.get(100))); } // check if Android CBL client sent only one PUT /{db}/_local/xxxx request // previous check already consume this request, so queue size should be 0. BlockingQueue<RecordedRequest> queue = dispatcher.getRequestQueueSnapshot(MockHelper.PATH_REGEX_CHECKPOINT); assertEquals(0, queue.size()); // cleanup stopReplication(replicator); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } // NOTE: This test should be manually tested. This test uses delay, timeout, wait,... // this could break test on Jenkins because it run on VM with ARM emulator. // To run test, please remove "manual" from test method name. // // https://github.com/couchbase/couchbase-lite-java-core/issues/736 // https://github.com/couchbase/couchbase-lite-net/issues/356 public void manualTestBulkGetTimeout() throws Exception { final int TEMP_DEFAULT_CONNECTION_TIMEOUT_SECONDS = CouchbaseLiteHttpClientFactory.DEFAULT_CONNECTION_TIMEOUT_SECONDS; final int TEMP_DEFAULT_SO_TIMEOUT_SECONDS = CouchbaseLiteHttpClientFactory.DEFAULT_SO_TIMEOUT_SECONDS; final int TEMP_RETRY_DELAY_SECONDS = ReplicationInternal.RETRY_DELAY_SECONDS; try { // TIMEOUT 1 SEC CouchbaseLiteHttpClientFactory.DEFAULT_CONNECTION_TIMEOUT_SECONDS = 1; CouchbaseLiteHttpClientFactory.DEFAULT_SO_TIMEOUT_SECONDS = 1; ReplicationInternal.RETRY_DELAY_SECONDS = 0; // serve 3 mock docs int numMockDocsToServe = 2; // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // mock documents to be pulled List<MockDocumentGet.MockDocument> mockDocs = MockHelper.getMockDocuments(numMockDocsToServe); // respond to all GET (responds with 404) and PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _changes response MockChangesFeed mockChangesFeed = new MockChangesFeed(); for (MockDocumentGet.MockDocument mockDocument : mockDocs) { mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument)); } SmartMockResponseImpl smartMockResponse = new SmartMockResponseImpl(mockChangesFeed.generateMockResponse()); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, smartMockResponse); // _bulk_get response MockDocumentBulkGet mockBulkGet = new MockDocumentBulkGet(); for (MockDocumentGet.MockDocument mockDocument : mockDocs) { mockBulkGet.addDocument(mockDocument); } // _bulk_get delays 4 SEC, which is longer custom timeout 5sec. // so this cause timeout. mockBulkGet.setDelayMs(4 * 1000); // makes sticky for retry reponse mockBulkGet.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_GET, mockBulkGet); // start mock server server.start(); // run pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); runReplication(pullReplication, 3 * 60); assertNotNull(pullReplication.getLastError()); assertTrue(pullReplication.getLastError() instanceof java.net.SocketTimeoutException); // dump out the outgoing requests for bulk docs BlockingQueue<RecordedRequest> bulkGetRequests = dispatcher.getRequestQueueSnapshot(MockHelper.PATH_REGEX_BULK_GET); assertTrue(bulkGetRequests.size() > 3); // verify at least 3 times retried } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } finally { CouchbaseLiteHttpClientFactory.DEFAULT_CONNECTION_TIMEOUT_SECONDS = TEMP_DEFAULT_CONNECTION_TIMEOUT_SECONDS; CouchbaseLiteHttpClientFactory.DEFAULT_SO_TIMEOUT_SECONDS = TEMP_DEFAULT_SO_TIMEOUT_SECONDS; ReplicationInternal.RETRY_DELAY_SECONDS = TEMP_RETRY_DELAY_SECONDS; } } /** * This test is almost identical with * TestCase(CBL_Pusher_DocIDs) in CBLReplicator_Tests.m */ public void testPushReplicationSetDocumentIDs() throws Exception { // Create documents: createDocumentForPushReplication("doc1", null, null); createDocumentForPushReplication("doc2", null, null); createDocumentForPushReplication("doc3", null, null); createDocumentForPushReplication("doc4", null, null); MockWebServer server = null; MockDispatcher dispatcher = new MockDispatcher(); try { // Create mock server and play: server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); server.start(); ; // Checkpoint GET response w/ 404 + respond to all PUT Checkpoint requests: MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(50); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing: MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // Create push replication: Replication replication = database.createPushReplication(server.url("/db").url()); replication.setDocIds(Arrays.asList(new String[]{"doc2", "doc3"})); // check pending document IDs: Set<String> pendingDocIDs = replication.getPendingDocumentIDs(); assertEquals(2, pendingDocIDs.size()); assertFalse(pendingDocIDs.contains("doc1")); assertTrue(pendingDocIDs.contains("doc2")); assertTrue(pendingDocIDs.contains("doc3")); assertFalse(pendingDocIDs.contains("doc4")); // Run replication: runReplication(replication); // Check result: RecordedRequest bulkDocsRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(bulkDocsRequest); assertFalse(MockHelper.getUtf8Body(bulkDocsRequest).contains("doc1")); assertTrue(MockHelper.getUtf8Body(bulkDocsRequest).contains("doc2")); assertTrue(MockHelper.getUtf8Body(bulkDocsRequest).contains("doc3")); assertFalse(MockHelper.getUtf8Body(bulkDocsRequest).contains("doc4")); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } public void testPullReplicationSetDocumentIDs() throws Exception { MockWebServer server = null; MockDispatcher dispatcher = new MockDispatcher(); try { // Create mock server and play: server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); server.start(); // checkpoint PUT or GET response (sticky): MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _changes response: MockChangesFeed mockChangesFeed = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // Run pull replication: Replication replication = database.createPullReplication(server.url("/db").url()); replication.setDocIds(Arrays.asList(new String[]{"doc2", "doc3"})); runReplication(replication); // Check changes feed request: RecordedRequest getChangesFeedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES); assertTrue(getChangesFeedRequest.getMethod().equals("POST")); String body = getChangesFeedRequest.getUtf8Body(); Map<String, Object> jsonMap = Manager.getObjectMapper().readValue(body, Map.class); assertTrue(jsonMap.containsKey("filter")); String filter = (String) jsonMap.get("filter"); assertEquals("_doc_ids", filter); List<String> docIDs = (List<String>) jsonMap.get("doc_ids"); assertNotNull(docIDs); assertEquals(2, docIDs.size()); assertTrue(docIDs.contains("doc2")); assertTrue(docIDs.contains("doc3")); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } public void testPullWithGzippedChangesFeed() throws Exception { MockWebServer server = null; try { // Create mock server and play: MockDispatcher dispatcher = new MockDispatcher(); server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); server.start(); // Mock documents to be pulled: MockDocumentGet.MockDocument mockDoc1 = new MockDocumentGet.MockDocument("doc1", "1-5e38", 1); mockDoc1.setJsonMap(MockHelper.generateRandomJsonMap()); MockDocumentGet.MockDocument mockDoc2 = new MockDocumentGet.MockDocument("doc2", "1-563b", 2); mockDoc2.setJsonMap(MockHelper.generateRandomJsonMap()); // // checkpoint GET response w/ 404: MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // _changes response: MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc1)); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc2)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse(/*gzip*/true)); // doc1 response: MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDoc1); dispatcher.enqueueResponse(mockDoc1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // doc2 response: mockDocumentGet = new MockDocumentGet(mockDoc2); dispatcher.enqueueResponse(mockDoc2.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // _bulk_get response: MockDocumentBulkGet mockBulkGet = new MockDocumentBulkGet(); mockBulkGet.addDocument(mockDoc1); mockBulkGet.addDocument(mockDoc2); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_GET, mockBulkGet); // Respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // Setup database change listener: final List<String> changeDocIDs = new ArrayList<String>(); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { for (DocumentChange change : event.getChanges()) { changeDocIDs.add(change.getDocumentId()); } } }); // Run pull replication: Replication replication = database.createPullReplication(server.url("/db").url()); runReplication(replication); // Check result: assertEquals(2, changeDocIDs.size()); String[] docIDs = changeDocIDs.toArray(new String[changeDocIDs.size()]); Arrays.sort(docIDs); assertTrue(Arrays.equals(new String[]{"doc1", "doc2"}, docIDs)); // Check changes feed request: RecordedRequest changesFeedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHANGES); String acceptEncoding = changesFeedRequest.getHeader("Accept-Encoding"); assertNotNull(acceptEncoding); assertTrue(acceptEncoding.contains("gzip")); } finally { MockDispatcher dispatcher = new MockDispatcher(); } } /** * Push Replication, never receive REPLICATION_ACTIVE status * https://github.com/couchbase/couchbase-lite-android/issues/451 */ public void testPushReplActiveState() throws Exception { Log.d(TAG, "TEST START: testPushReplActiveState()"); // make sure we are starting empty assertEquals(0, database.getLastSequenceNumber()); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { server.start(); // checkpoint GET response w/ 404. also receives checkpoint PUT's MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // Replication pullReplication = database.createPushReplication(server.url("/db").url()); pullReplication.setContinuous(true); final String checkpointId = pullReplication.remoteCheckpointDocID(); // save the checkpoint id for later usage // Event handler for IDLE CountDownLatch idleSignal = new CountDownLatch(1); ReplicationIdleObserver idleObserver = new ReplicationIdleObserver(idleSignal); pullReplication.addChangeListener(idleObserver); // start the continuous replication pullReplication.start(); // wait until we get an IDLE event boolean successful = idleSignal.await(30, TimeUnit.SECONDS); assertTrue(successful); pullReplication.removeChangeListener(idleObserver); // Event handler for ACTIVE CountDownLatch activeSignal = new CountDownLatch(1); ReplicationRunningObserver activeObserver = new ReplicationRunningObserver(activeSignal); pullReplication.addChangeListener(activeObserver); // Event handler for IDLE2 CountDownLatch idleSignal2 = new CountDownLatch(1); ReplicationIdleObserver idleObserver2 = new ReplicationIdleObserver(idleSignal2); pullReplication.addChangeListener(idleObserver2); // add docs Map<String, Object> properties1 = new HashMap<String, Object>(); properties1.put("doc1", "testPushReplActiveState"); final Document doc1 = createDocWithProperties(properties1); // wait until we get an ACTIVE event successful = activeSignal.await(30, TimeUnit.SECONDS); assertTrue(successful); pullReplication.removeChangeListener(activeObserver); // check _bulk_docs RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(request); assertTrue(MockHelper.getUtf8Body(request).contains("testPushReplActiveState")); // wait until we get an IDLE event successful = idleSignal2.await(30, TimeUnit.SECONDS); assertTrue(successful); pullReplication.removeChangeListener(idleObserver2); // stop pull replication stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } Log.d(TAG, "TEST END: testPushReplActiveState()"); } /** * Error after close DB client * https://github.com/couchbase/couchbase-lite-java/issues/52 */ public void testStop() throws Exception { Log.d(Log.TAG, "START testStop()"); boolean success = false; // create mock server MockDispatcher dispatcher = new MockDispatcher(); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); MockWebServer server = new MockWebServer(); server.setDispatcher(dispatcher); try { server.start(); // checkpoint PUT or GET response (sticky) (for both push and pull) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // create pull replication & start it Replication pull = database.createPullReplication(server.url("/db").url()); pull.setContinuous(true); final CountDownLatch pullIdleState = new CountDownLatch(1); ReplicationIdleObserver pullIdleObserver = new ReplicationIdleObserver(pullIdleState); pull.addChangeListener(pullIdleObserver); pull.start(); // create push replication & start it Replication push = database.createPullReplication(server.url("/db").url()); push.setContinuous(true); final CountDownLatch pushIdleState = new CountDownLatch(1); ReplicationIdleObserver pushIdleObserver = new ReplicationIdleObserver(pushIdleState); push.addChangeListener(pushIdleObserver); push.start(); // wait till both push and pull replicators become idle. success = pullIdleState.await(30, TimeUnit.SECONDS); assertTrue(success); pull.removeChangeListener(pullIdleObserver); success = pushIdleState.await(30, TimeUnit.SECONDS); assertTrue(success); push.removeChangeListener(pushIdleObserver); // stop both pull and push replicators stopReplication(pull); stopReplication(push); boolean observedCBLRequestWorker = false; // First give 5 sec to clean thread status. try { Thread.sleep(5 * 1000); } catch (Exception e) { } // all threads which are associated with replicators should be terminated. Set<Thread> threadSet = Thread.getAllStackTraces().keySet(); for (Thread t : threadSet) { if (t.isAlive()) { observedCBLRequestWorker = true; if (t.getName().indexOf("CBLRequestWorker") != -1) { observedCBLRequestWorker = true; break; } } } // second attemtpt, if still observe CBLRequestWorker thread, makes error if (observedCBLRequestWorker) { // give 10 sec to clean thread status. try { Thread.sleep(10 * 1000); } catch (Exception e) { } // all threads which are associated with replicators should be terminated. Set<Thread> threadSet2 = Thread.getAllStackTraces().keySet(); for (Thread t : threadSet2) { if (t.isAlive()) { assertEquals(-1, t.getName().indexOf("CBLRequestWorker")); } } } } finally { // shutdown mock server assertTrue(MockHelper.shutdown(server, dispatcher)); } Log.d(Log.TAG, "END testStop()"); } /** * http://developer.couchbase.com/mobile/develop/references/couchbase-lite/couchbase-lite/replication/replication/index.html#mapstring-string-filterparams--get-set- * <p/> * Params passed in filtered push throw a null exception in the filter function * https://github.com/couchbase/couchbase-lite-java-core/issues/533 */ public void testSetFilterParams() throws CouchbaseLiteException, IOException, InterruptedException { // make sure we are starting empty assertEquals(0, database.getLastSequenceNumber()); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { server.start(); // checkpoint GET response w/ 404. also receives checkpoint PUT's MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // create 10 documents and delete 5 for (int i = 0; i < 10; i++) { Document doc = null; if (i % 2 == 0) { doc = createDocument(i, true); } else { doc = createDocument(i, false); } if (i % 2 == 0) { try { doc.delete(); } catch (CouchbaseLiteException e) { e.printStackTrace(); } } } final CountDownLatch latch = new CountDownLatch(10); final CountDownLatch check = new CountDownLatch(10); database.setFilter("unDeleted", new ReplicationFilter() { @Override public boolean filter(SavedRevision savedRevision, Map<String, Object> params) { if (params == null || !"hello".equals(params.get("name"))) { check.countDown(); } latch.countDown(); return !savedRevision.isDeletion(); } }); Replication pushReplication = database.createPushReplication(server.url("/db").url()); pushReplication.setContinuous(false); pushReplication.setFilter("unDeleted"); pushReplication.setFilterParams(Collections.<String, Object>singletonMap("name", "hello")); pushReplication.start(); boolean success = latch.await(30, TimeUnit.SECONDS); assertTrue(success); assertEquals(10, check.getCount()); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Sync (pull replication) fails on document with a lot of revisions and attachments * https://github.com/couchbase/couchbase-lite-java-core/issues/415 */ public void testPullReplicatonWithManyAttachmentRevisions() throws Exception { Log.d(TAG, "TEST START: testPullReplicatonWithManyAttachmentRevisions()"); String docID = "11111"; String key = "key"; String value = "one-one-one-one"; String attachmentName = "attachment.png"; // create initial document (Revision 1-xxxx) Map<String, Object> props1 = new HashMap<String, Object>(); props1.put("_id", docID); props1.put(key, value); RevisionInternal rev = new RevisionInternal(props1); Status status = new Status(); RevisionInternal savedRev = database.putRevision(rev, null, false, status); String rev1ID = savedRev.getRevID(); // add attachment to doc (Revision 2-xxxx) Document doc = database.getDocument(docID); UnsavedRevision newRev = doc.createRevision(); InputStream attachmentStream = getAsset(attachmentName); newRev.setAttachment(attachmentName, "image/png", attachmentStream); SavedRevision saved = newRev.save(true); String rev2ID = doc.getCurrentRevisionId(); // Create 5 revisions with 50 conflicts each int j = 3; for (; j < 5; j++) { // Create a conflict, won by the new revision: Map<String, Object> props = new HashMap<String, Object>(); props.put("_id", docID); props.put("_rev", j + "-0000"); props.put(key, value); RevisionInternal leaf = new RevisionInternal(props); database.forceInsert(leaf, new ArrayList<String>(), null); for (int i = 0; i < 49; i++) { // Create a conflict, won by the new revision: Map<String, Object> props_conflict = new HashMap<String, Object>(); props_conflict.put("_id", docID); String revStr = String.format(Locale.ENGLISH, "%d-%04d", j, i); props_conflict.put("_rev", revStr); props_conflict.put(key, value); // attachment byte[] attach1 = "This is the body of attach1".getBytes(); String base64 = Base64.encodeBytes(attach1); Map<String, Object> attachment = new HashMap<String, Object>(); attachment.put("content_type", "text/plain"); attachment.put("data", base64); Map<String, Object> attachmentDict = new HashMap<String, Object>(); attachmentDict.put("test_attachment", attachment); props_conflict.put("_attachments", attachmentDict); // end of attachment RevisionInternal leaf_conflict = new RevisionInternal(props_conflict); List<String> revHistory = new ArrayList<String>(); revHistory.add(leaf_conflict.getRevID()); for (int k = j - 1; k > 2; k--) { revHistory.add(String.format(Locale.ENGLISH, "%d-0000", k)); } revHistory.add(rev2ID); revHistory.add(rev1ID); database.forceInsert(leaf_conflict, revHistory, null); } } String docId = doc.getId(); String revId = j + "-00"; int lastSeq = (int) database.getLastSequenceNumber(); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) (for both push and pull) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); MockChangesFeed mockChangesFeedEmpty = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedEmpty.generateMockResponse()); // start mock server server.start(); // create pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); final CountDownLatch idleSignal1 = new CountDownLatch(1); final CountDownLatch idleSignal2 = new CountDownLatch(2); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { Log.i(TAG, event.toString()); if (event.getError() != null) { Assert.fail("Should not have any error...."); } if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { idleSignal1.countDown(); idleSignal2.countDown(); } } }); // start pull replication pullReplication.start(); boolean success = idleSignal1.await(30, TimeUnit.SECONDS); assertTrue(success); // MockDocumentGet.MockDocument mockDocument1 = new MockDocumentGet.MockDocument(docId, revId, lastSeq + 1); mockDocument1.setJsonMap(MockHelper.generateRandomJsonMap()); MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // doc response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDocument1); dispatcher.enqueueResponse(mockDocument1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // check /db/docid?... RecordedRequest request = dispatcher.takeRequestBlocking(mockDocument1.getDocPathRegex(), 30 * 1000); Log.i(TAG, request.toString()); Map<String, String> queries = query2map(request.getPath()); String atts_since = URLDecoder.decode(queries.get("atts_since"), "UTF-8"); List<String> json = (List<String>) str2json(atts_since); Log.i(TAG, json.toString()); assertNotNull(json); // atts_since parameter should be limit to PullerInternal.MAX_NUMBER_OF_ATTS_SINCE assertTrue(json.size() == PullerInternal.MAX_NUMBER_OF_ATTS_SINCE); boolean success2 = idleSignal2.await(30, TimeUnit.SECONDS); assertTrue(success2); // stop pull replication stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } Log.d(TAG, "TEST END: testPullReplicatonWithManyAttachmentRevisions()"); } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/358 * * @related: https://github.com/couchbase/couchbase-lite-java-core/issues/55 * related: testContinuousPushReplicationGoesIdle() * <p/> * test steps: * - start replicator * - make sure replicator becomes idle state * - add N docs * - when callback state == idle * - assert that mock has received N docs */ public void testContinuousPushReplicationGoesIdleTwice() throws Exception { // /_local/* // /_revs_diff // /_bulk_docs // /_local/* final int EXPECTED_REQUEST_COUNT = 4; // make sure we are starting empty assertEquals(0, database.getLastSequenceNumber()); // 1. Setup MockWebServer // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); final MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint GET response w/ 404. also receives checkpoint PUT's MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); server.start(); // 2. Create replication Replication replication = database.createPushReplication(server.url("/db").url()); replication.setContinuous(true); CountDownLatch replicationIdle = new CountDownLatch(1); ReplicationIdleObserver idleObserver = new ReplicationIdleObserver(replicationIdle); replication.addChangeListener(idleObserver); replication.start(); // 3. Wait until idle (make sure replicator becomes IDLE state) boolean success = replicationIdle.await(30, TimeUnit.SECONDS); assertTrue(success); replication.removeChangeListener(idleObserver); // 4. make sure if /_local was called by replicator after start and before idle RecordedRequest request1 = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(request1); dispatcher.takeRecordedResponseBlocking(request1); assertEquals(1, server.getRequestCount()); // 5. Add replication change listener for transition to IDLE class ReplicationTransitionToIdleObserver implements Replication.ChangeListener { private CountDownLatch doneSignal; private CountDownLatch checkSignal; public ReplicationTransitionToIdleObserver(CountDownLatch doneSignal, CountDownLatch checkSignal) { this.doneSignal = doneSignal; this.checkSignal = checkSignal; } public void changed(Replication.ChangeEvent event) { Log.w(Log.TAG_SYNC, "[ChangeListener.changed()] event => " + event.toString()); if (event.getTransition() != null) { if (event.getTransition().getSource() != event.getTransition().getDestination() && event.getTransition().getDestination() == ReplicationState.IDLE) { Log.w(Log.TAG_SYNC, "[ChangeListener.changed()] Transition to IDLE"); Log.w(Log.TAG_SYNC, "[ChangeListener.changed()] Request Count => " + server.getRequestCount()); this.doneSignal.countDown(); // When replicator becomes IDLE state, check if all requests are completed // assertEquals in inner class does not work.... // Note: sometimes server.getRequestCount() returns expected number - 1. // Is it timing issue? if (EXPECTED_REQUEST_COUNT == server.getRequestCount() || EXPECTED_REQUEST_COUNT - 1 == server.getRequestCount()) { this.checkSignal.countDown(); } } } } } CountDownLatch checkStateToIdle = new CountDownLatch(1); CountDownLatch checkRequestCount = new CountDownLatch(1); ReplicationTransitionToIdleObserver replicationTransitionToIdleObserver = new ReplicationTransitionToIdleObserver(checkStateToIdle, checkRequestCount); replication.addChangeListener(replicationTransitionToIdleObserver); Log.w(Log.TAG_SYNC, "Added listener for transition to IDLE"); // 6. Add doc(s) for (int i = 1; i <= 1; i++) { Map<String, Object> properties1 = new HashMap<String, Object>(); properties1.put("doc" + String.valueOf(i), "testContinuousPushReplicationGoesIdleTooSoon " + String.valueOf(i)); final Document doc = createDocWithProperties(properties1); } // 7. Wait until idle (make sure replicator becomes IDLE state from other state) // NOTE: 12/17/2014 - current code fails here because after adding listener, state never changed from IDLE // By implementing stateMachine for Replication completely, address this failure. success = checkStateToIdle.await(20, TimeUnit.SECONDS); // check if state becomes IDLE from other state assertTrue(success); success = checkRequestCount.await(20, TimeUnit.SECONDS); // check if request count is 4 when state becomes IDLE assertTrue(success); // 8. Make sure some of requests are called // _bulk_docs RecordedRequest request3 = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(request3); dispatcher.takeRecordedResponseBlocking(request3); // double check total request Log.w(Log.TAG_SYNC, "Total Requested Count before stop replicator => " + server.getRequestCount()); assertTrue(EXPECTED_REQUEST_COUNT == server.getRequestCount() || EXPECTED_REQUEST_COUNT - 1 == server.getRequestCount()); // 9. Stop replicator replication.removeChangeListener(replicationTransitionToIdleObserver); stopReplication(replication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/358 * <p/> * related: testContinuousPushReplicationGoesIdleTooSoon() * testContinuousPushReplicationGoesIdle() * <p/> * test steps: * - add N docs * - start replicator * - when callback state == idle * - assert that mock has received N docs */ public void failingTestContinuousPushReplicationGoesIdleTooSoon() throws Exception { // smaller batch size so there are multiple requests to _bulk_docs int previous = ReplicationInternal.INBOX_CAPACITY; ReplicationInternal.INBOX_CAPACITY = 5; int numDocs = ReplicationInternal.INBOX_CAPACITY * 5; // make sure we are starting empty assertEquals(0, database.getLastSequenceNumber()); // Add doc(s) // NOTE: more documents causes more HTTP calls. It could be more than 4 times... for (int i = 1; i <= numDocs; i++) { Map<String, Object> properties = new HashMap<String, Object>(); properties.put("doc" + String.valueOf(i), "testContinuousPushReplicationGoesIdleTooSoon " + String.valueOf(i)); final Document doc = createDocWithProperties(properties); } // Setup MockWebServer // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); final MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint GET response w/ 404. also receives checkpoint PUT's MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); server.start(); // Create replicator Replication replication = database.createPushReplication(server.url("/db").url()); replication.setContinuous(true); // special change listener for this test case. class ReplicationTransitionToIdleObserver implements Replication.ChangeListener { private CountDownLatch enterIdleStateSignal; public ReplicationTransitionToIdleObserver(CountDownLatch enterIdleStateSignal) { this.enterIdleStateSignal = enterIdleStateSignal; } public void changed(Replication.ChangeEvent event) { Log.w(Log.TAG_SYNC, "[ChangeListener.changed()] event => " + event.toString()); if (event.getTransition() != null) { if (event.getTransition().getSource() != event.getTransition().getDestination() && event.getTransition().getDestination() == ReplicationState.IDLE) { Log.w(Log.TAG_SYNC, "[ChangeListener.changed()] Transition to IDLE"); Log.w(Log.TAG_SYNC, "[ChangeListener.changed()] Request Count => " + server.getRequestCount()); this.enterIdleStateSignal.countDown(); } } } } CountDownLatch enterIdleStateSignal = new CountDownLatch(1); ReplicationTransitionToIdleObserver replicationTransitionToIdleObserver = new ReplicationTransitionToIdleObserver(enterIdleStateSignal); replication.addChangeListener(replicationTransitionToIdleObserver); replication.start(); // Wait until idle (make sure replicator becomes IDLE state from other state) boolean success = enterIdleStateSignal.await(20, TimeUnit.SECONDS); assertTrue(success); // Once the replicator is idle get a snapshot of all the requests its made to _bulk_docs endpoint int numDocsPushed = 0; BlockingQueue<RecordedRequest> requests = dispatcher.getRequestQueueSnapshot(MockHelper.PATH_REGEX_BULK_DOCS); for (RecordedRequest request : requests) { Log.i(Log.TAG_SYNC, "request: %s", request); byte[] body = MockHelper.getUncompressedBody(request); Map<String, Object> jsonMap = MockHelper.getJsonMapFromRequest(body); List docs = (List) jsonMap.get("docs"); numDocsPushed += docs.size(); } // WORKAROUND: CBL Java Unit Test on Jenkins rarely fails following. // TODO: Need to fix: https://github.com/couchbase/couchbase-lite-java-core/issues/446 // It seems threading issue exists, and replicator becomes IDLE even tasks in batcher. if (System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik")) { // Assert that all docs have already been pushed by the time it goes IDLE assertEquals(numDocs, numDocsPushed); } // Stop replicator and MockWebServer stopReplication(replication); // wait until checkpoint is pushed, since it can happen _after_ replication is finished. // if this isn't done, there can be IOExceptions when calling server.shutdown() waitForPutCheckpointRequestWithSeq(dispatcher, (int) database.getLastSequenceNumber()); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); ReplicationInternal.INBOX_CAPACITY = previous; } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/352 * <p/> * When retrying a replication, make sure to get session & checkpoint. */ public void testCheckSessionAndCheckpointWhenRetryingReplication() throws Exception { final int VALIDATION_RETRIES = 3; final int TEMP_RETRY_DELAY_MS = RemoteRequestRetry.RETRY_DELAY_MS; final int TEMP_RETRY_DELAY_SECONDS = ReplicationInternal.RETRY_DELAY_SECONDS; try { RemoteRequestRetry.RETRY_DELAY_MS = 5; // speed up test execution (inner loop retry delay) ReplicationInternal.RETRY_DELAY_SECONDS = 1; // speed up test execution (outer loop retry delay) String fakeEmail = "myfacebook@gmail.com"; // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // set up request { // response for /db/_session MockSessionGet mockSessionGet = new MockSessionGet(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_SESSION, mockSessionGet.generateMockResponse()); // response for /db/_facebook MockFacebookAuthPost mockFacebookAuthPost = new MockFacebookAuthPost(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_FACEBOOK_AUTH, mockFacebookAuthPost.generateMockResponseForSuccess(fakeEmail)); // response for /db/_local/.* MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // response for /db/_revs_diff MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // response for /db/_bulk_docs -- 503 errors MockResponse mockResponse = new MockResponse().setResponseCode(503); WrappedSmartMockResponse mockBulkDocs = new WrappedSmartMockResponse(mockResponse, false); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); } server.start(); // register bogus fb token Authenticator facebookAuthenticator = AuthenticatorFactory.createFacebookAuthenticator("fake_access_token"); // create replication Replication replication = database.createPushReplication(server.url("/db").url()); replication.setAuthenticator(facebookAuthenticator); replication.setContinuous(true); CountDownLatch replicationIdle = new CountDownLatch(1); ReplicationIdleObserver idleObserver = new ReplicationIdleObserver(replicationIdle); replication.addChangeListener(idleObserver); replication.start(); // wait until idle boolean success = replicationIdle.await(30, TimeUnit.SECONDS); assertTrue(success); replication.removeChangeListener(idleObserver); // create a doc in local db Document doc1 = createDocumentForPushReplication("doc1", null, null); // initial request { // check /db/_session RecordedRequest sessionRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_SESSION); assertNotNull(sessionRequest); dispatcher.takeRecordedResponseBlocking(sessionRequest); // check /db/_facebook RecordedRequest facebookSessionRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_FACEBOOK_AUTH); assertNotNull(facebookSessionRequest); dispatcher.takeRecordedResponseBlocking(facebookSessionRequest); // check /db/_local/.* RecordedRequest checkPointRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(checkPointRequest); dispatcher.takeRecordedResponseBlocking(checkPointRequest); // check /db/_revs_diff RecordedRequest revsDiffRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_REVS_DIFF); assertNotNull(revsDiffRequest); dispatcher.takeRecordedResponseBlocking(revsDiffRequest); // we should expect to at least see numAttempts attempts at doing POST to _bulk_docs // 1st attempt // numAttempts are number of times retry in 1 attempt. int numAttempts = RemoteRequestRetry.MAX_RETRIES + 1; // total number of attempts = 4 (1 initial + MAX_RETRIES) for (int i = 0; i < numAttempts; i++) { RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(request); dispatcher.takeRecordedResponseBlocking(request); } } // To test following, requires to fix #299 (improve retry behavior) // Retry requests // outer retry loop for (int j = 0; j < VALIDATION_RETRIES; j++) { // MockSessionGet does not support isSticky MockSessionGet mockSessionGet = new MockSessionGet(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_SESSION, mockSessionGet.generateMockResponse()); // MockFacebookAuthPost does not support isSticky MockFacebookAuthPost mockFacebookAuthPost = new MockFacebookAuthPost(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_FACEBOOK_AUTH, mockFacebookAuthPost.generateMockResponseForSuccess(fakeEmail)); // *** Retry must include session & check point *** // check /db/_session RecordedRequest sessionRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_SESSION); assertNotNull(sessionRequest); dispatcher.takeRecordedResponseBlocking(sessionRequest); // check /db/_facebook RecordedRequest facebookSessionRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_FACEBOOK_AUTH); assertNotNull(facebookSessionRequest); dispatcher.takeRecordedResponseBlocking(facebookSessionRequest); // check /db/_local/.* RecordedRequest checkPointRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(checkPointRequest); dispatcher.takeRecordedResponseBlocking(checkPointRequest); // check /db/_revs_diff RecordedRequest revsDiffRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_REVS_DIFF); assertNotNull(revsDiffRequest); dispatcher.takeRecordedResponseBlocking(revsDiffRequest); // we should expect to at least see numAttempts attempts at doing POST to _bulk_docs // 1st attempt // numAttempts are number of times retry in 1 attempt. int numAttempts = RemoteRequestRetry.MAX_RETRIES + 1; // total number of attempts = 4 (1 initial + MAX_RETRIES) for (int i = 0; i < numAttempts; i++) { RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(request); dispatcher.takeRecordedResponseBlocking(request); } } stopReplication(replication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } finally { RemoteRequestRetry.RETRY_DELAY_MS = TEMP_RETRY_DELAY_MS; ReplicationInternal.RETRY_DELAY_SECONDS = TEMP_RETRY_DELAY_SECONDS; } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/352 * <p/> * Makes the replicator stop, even if it is continuous, when it receives a permanent-type error */ public void testStopReplicatorWhenRetryingReplicationWithPermanentError() throws Exception { final int TEMP_RETRY_DELAY_MS = RemoteRequestRetry.RETRY_DELAY_MS; final int TEMP_RETRY_DELAY_SECONDS = ReplicationInternal.RETRY_DELAY_SECONDS; RemoteRequestRetry.RETRY_DELAY_MS = 5; // speed up test execution (inner loop retry delay) ReplicationInternal.RETRY_DELAY_SECONDS = 1; // speed up test execution (outer loop retry delay) // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); try { dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); // set up request { // response for /db/_local/.* MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // response for /db/_revs_diff MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // response for /db/_bulk_docs -- 400 Bad Request (not transient error) MockResponse mockResponse = new MockResponse().setResponseCode(400); WrappedSmartMockResponse mockBulkDocs = new WrappedSmartMockResponse(mockResponse, false); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); } server.start(); // create replication Replication replication = database.createPushReplication(server.url("/db").url()); replication.setContinuous(true); // add replication observer for IDLE state CountDownLatch replicationIdle = new CountDownLatch(1); ReplicationIdleObserver idleObserver = new ReplicationIdleObserver(replicationIdle); replication.addChangeListener(idleObserver); // add replication observer for finished CountDownLatch replicationDoneSignal = new CountDownLatch(1); ReplicationFinishedObserver replicationFinishedObserver = new ReplicationFinishedObserver(replicationDoneSignal); replication.addChangeListener(replicationFinishedObserver); replication.start(); // wait until idle boolean success = replicationIdle.await(30, TimeUnit.SECONDS); assertTrue(success); replication.removeChangeListener(idleObserver); // create a doc in local db Document doc1 = createDocumentForPushReplication("doc1", null, null); // initial request { // check /db/_local/.* RecordedRequest checkPointRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(checkPointRequest); dispatcher.takeRecordedResponseBlocking(checkPointRequest); // check /db/_revs_diff RecordedRequest revsDiffRequest = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_REVS_DIFF); assertNotNull(revsDiffRequest); dispatcher.takeRecordedResponseBlocking(revsDiffRequest); // we should observe only one POST to _bulk_docs request because error is not transient error RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_BULK_DOCS); assertNotNull(request); dispatcher.takeRecordedResponseBlocking(request); } // Without fixing CBL Java Core #352, following code causes hang. // wait for replication to finish boolean didNotTimeOut = replicationDoneSignal.await(180, TimeUnit.SECONDS); Log.d(TAG, "replicationDoneSignal.await done, didNotTimeOut: " + didNotTimeOut); assertFalse(replication.isRunning()); } finally { RemoteRequestRetry.RETRY_DELAY_MS = TEMP_RETRY_DELAY_MS; ReplicationInternal.RETRY_DELAY_SECONDS = TEMP_RETRY_DELAY_SECONDS; assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * TODO: MockServer sends 406 error with this test case. It should not be. * Need to fix. * * https://github.com/couchbase/couchbase-lite-java-core/issues/356 */ public void testReplicationRestartPreservesValues() throws Exception { // make sure we are starting empty assertEquals(0, database.getLastSequenceNumber()); // add docs Map<String, Object> properties1 = new HashMap<String, Object>(); properties1.put("doc1", "testContinuousPushReplicationGoesIdle"); final Document doc1 = createDocWithProperties(properties1); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { server.start(); // create db response with 201 MockCreateDB mockCreateDB = new MockCreateDB(); mockCreateDB.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_DB, mockCreateDB); // checkpoint GET response w/ 404. also receives checkpoint PUT's MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); // _bulk_docs response -- everything stored MockBulkDocs mockBulkDocs = new MockBulkDocs(); mockBulkDocs.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_DOCS, mockBulkDocs); // create continuos replication Replication pusher = database.createPushReplication(server.url("/db").url()); pusher.setContinuous(true); // add filter properties to the replicator String filterName = "app/clientIdAndTablesSchemeDocIdFilter"; pusher.setFilter(filterName); Map<String, Object> filterParams = new HashMap<String, Object>(); String filterParam = "tablesSchemeDocId"; String filterVal = "foo"; filterParams.put(filterParam, filterVal); pusher.setFilterParams(filterParams); // doc ids pusher.setDocIds(Arrays.asList(doc1.getId())); // custom authenticator Authenticator authenticator = AuthenticatorFactory.createBasicAuthenticator("foo", "bar"); pusher.setAuthenticator(authenticator); // custom request headers Map<String, Object> requestHeaders = new HashMap<String, Object>(); requestHeaders.put("foo", "bar"); pusher.setHeaders(requestHeaders); // start the continuous replication CountDownLatch replicationIdleSignal = new CountDownLatch(1); ReplicationIdleObserver replicationIdleObserver = new ReplicationIdleObserver(replicationIdleSignal); pusher.addChangeListener(replicationIdleObserver); pusher.start(); // wait until we get an IDLE event boolean successful = replicationIdleSignal.await(30, TimeUnit.SECONDS); assertTrue(successful); // restart the replication CountDownLatch replicationIdleSignal2 = new CountDownLatch(1); ReplicationIdleObserver replicationIdleObserver2 = new ReplicationIdleObserver(replicationIdleSignal2); pusher.addChangeListener(replicationIdleObserver2); pusher.restart(); // wait until we get another IDLE event successful = replicationIdleSignal2.await(30, TimeUnit.SECONDS); assertTrue(successful); // verify the restarted replication still has the values we set up earlier assertEquals(filterName, pusher.getFilter()); assertTrue(pusher.getFilterParams().size() == 1); assertEquals(filterVal, pusher.getFilterParams().get(filterParam)); assertTrue(pusher.isContinuous()); assertEquals(Arrays.asList(doc1.getId()), pusher.getDocIds()); assertEquals(authenticator, pusher.getAuthenticator()); assertEquals(requestHeaders, pusher.getHeaders()); // NOTE: after db created, createTarget flag set false. assertFalse(pusher.shouldCreateTarget()); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * The observed problem: * <p/> * - 1. Start continuous pull * - 2. Wait until it goes IDLE (this works fine) * - 3. Add a new document directly to the Sync Gateway * - 4. The continuous pull goes from IDLE -> RUNNING * - 5. Wait until it goes IDLE again (this doesn't work, it never goes back to IDLE) * <p/> * The test case below simulates the above scenario using a mock sync gateway. * <p/> * https://github.com/couchbase/couchbase-lite-java-core/issues/383 */ public void testContinuousPullReplicationGoesIdleTwice() throws Exception { Log.d(TAG, "TEST START"); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // add non-sticky changes response that returns no changes // this will cause the pull replicator to go into the IDLE state MockChangesFeed mockChangesFeed = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // add _changes response that just blocks for a few seconds to emulate // server that doesn't have any new changes. while the puller is blocked on this request // to the _changes feed, the test will add a new changes listener that waits until it goes // into the RUNNING state MockChangesFeedNoResponse mockChangesFeedNoResponse = new MockChangesFeedNoResponse(); // It seems 5 sec delay might not be necessary. It reduce test duration 5 sec //mockChangesFeedNoResponse.setDelayMs(5 * 1000); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedNoResponse); // 3. // after the above changes feed response returns after 5 seconds, the next time // the puller gets the _changes feed, return a response that there is 1 new doc. // this will cause the puller to go from IDLE -> RUNNING MockDocumentGet.MockDocument mockDoc1 = new MockDocumentGet.MockDocument("doc1", "1-5e38", 1); mockDoc1.setJsonMap(MockHelper.generateRandomJsonMap()); mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // at this point, the mock _changes feed is done simulating new docs on the sync gateway // since we've done enough to reproduce the problem. so at this point, just make the changes // feed block for a long time. MockChangesFeedNoResponse mockChangesFeedNoResponse2 = new MockChangesFeedNoResponse(); mockChangesFeedNoResponse2.setDelayMs(6000 * 1000); // block for > 1hr mockChangesFeedNoResponse2.setSticky(true); // continue this behavior indefinitely dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedNoResponse2); // doc1 response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDoc1); dispatcher.enqueueResponse(mockDoc1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // _revs_diff response -- everything missing MockRevsDiff mockRevsDiff = new MockRevsDiff(); mockRevsDiff.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_REVS_DIFF, mockRevsDiff); server.start(); // create pull replication final Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); final CountDownLatch enteredIdleState1 = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { Log.d(TAG, "Replication is IDLE 1"); enteredIdleState1.countDown(); } } }); // 1. start pull replication pullReplication.start(); // 2. wait until its IDLE boolean success = enteredIdleState1.await(30, TimeUnit.SECONDS); assertTrue(success); // 3. see server side preparation // change listener to see if its RUNNING // we can't add this earlier, because the countdown latch would get // triggered too early (the other approach would be to set the countdown // latch to a higher number) final CountDownLatch enteredRunningState = new CountDownLatch(1); final CountDownLatch enteredIdleState2 = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.RUNNING) { if (enteredRunningState.getCount() > 0) { Log.d(TAG, "Replication is RUNNING"); enteredRunningState.countDown(); } } // second IDLE change listener // handling IDLE event here. It seems IDLE event was fired before set IDLE event handler else if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { if (enteredRunningState.getCount() <= 0 && enteredIdleState2.getCount() > 0) { Log.d(TAG, "Replication is IDLE 2"); enteredIdleState2.countDown(); } } } }); // 4. wait until its RUNNING Log.d(TAG, "WAIT for RUNNING"); success = enteredRunningState.await(30, TimeUnit.SECONDS); assertTrue(success); // 5. wait until its IDLE again. before the fix, it would never go IDLE again, and so // this would timeout and the test would fail. Log.d(TAG, "WAIT for IDLE"); success = enteredIdleState2.await(30, TimeUnit.SECONDS); assertTrue(success); Log.d(TAG, "STOP REPLICATOR"); // clean up stopReplication(pullReplication); Log.d(TAG, "STOP MOCK SERVER"); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } Log.d(TAG, "TEST DONE"); } /** * Test case that makes sure STOPPED notification is sent only once with continuous pull replication * https://github.com/couchbase/couchbase-lite-android/issues/442 */ public void testContinuousPullReplicationSendStoppedOnce() throws Exception { Log.d(TAG, "TEST START"); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // add non-sticky changes response that returns no changes // this will cause the pull replicator to go into the IDLE state MockChangesFeed mockChangesFeed = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); server.start(); // create pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); final CountDownLatch enteredIdleState = new CountDownLatch(1); final CountDownLatch enteredStoppedState = new CountDownLatch(2); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition().getDestination() == ReplicationState.IDLE) { Log.d(TAG, "Replication is IDLE"); enteredIdleState.countDown(); } else if (event.getTransition().getDestination() == ReplicationState.STOPPED) { event.getTransition().getDestination(); Log.d(TAG, "Replication is STOPPED"); enteredStoppedState.countDown(); } } }); // 1. start pull replication pullReplication.start(); // 2. wait until its IDLE boolean success = enteredIdleState.await(30, TimeUnit.SECONDS); assertTrue(success); // 3. stop pull replication stopReplication(pullReplication); // 4. wait until its RUNNING Log.d(TAG, "WAIT for STOPPED"); //success = enteredStoppedState.await(Replication.DEFAULT_MAX_TIMEOUT_FOR_SHUTDOWN + 30, TimeUnit.SECONDS); // replicator maximum shutdown timeout 60 sec + additional 30 sec for other stuff // NOTE: 90 sec is too long for unit test. chnaged to 30 sec // NOTE2: 30 sec is still too long for unit test. changed to 15sec. success = enteredStoppedState.await(15, TimeUnit.SECONDS); // replicator maximum shutdown timeout 60 sec + additional 30 sec for other stuff // if STOPPED notification was sent twice, enteredStoppedState becomes 0. assertEquals(1, enteredStoppedState.getCount()); assertFalse(success); } finally { Log.d(TAG, "STOP MOCK SERVER"); assertTrue(MockHelper.shutdown(server, dispatcher)); } Log.d(TAG, "TEST DONE"); } /** * Test case that makes sure STOPPED notification is sent only once with one time pull replication * https://github.com/couchbase/couchbase-lite-android/issues/442 */ public void testOneTimePullReplicationSendStoppedOnce() throws Exception { Log.d(TAG, "TEST START"); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // add non-sticky changes response that returns no changes // this will cause the pull replicator to go into the IDLE state MockChangesFeed mockChangesFeed = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); server.start(); // create pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(false); // handle STOPPED notification final CountDownLatch enteredStoppedState = new CountDownLatch(2); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition().getDestination() == ReplicationState.STOPPED) { Log.d(TAG, "Replication is STOPPED"); enteredStoppedState.countDown(); } } }); // 1. start pull replication pullReplication.start(); // 2. wait until its RUNNING Log.d(TAG, "WAIT for STOPPED"); boolean success = enteredStoppedState.await(15, TimeUnit.SECONDS); // if STOPPED notification was sent twice, enteredStoppedState becomes 0. assertEquals(1, enteredStoppedState.getCount()); assertFalse(success); } finally { Log.d(TAG, "STOP MOCK SERVER"); assertTrue(MockHelper.shutdown(server, dispatcher)); } Log.d(TAG, "TEST DONE"); } /** * Issue: Pull Replicator does not send IDLE state after check point * https://github.com/couchbase/couchbase-lite-java-core/issues/389 * <p/> * 1. Wait till pull replicator becomes IDLE state * 2. Update change event handler for handling ACTIVE and IDLE * 3. Create document into local db * 4. Based on local doc information, prepare mock change response for 1st /_changes request * 5. Prepare next mock change response for 2nd /_changes request (blocking for while) * 6. wait for Replication IDLE -> ACTIVE -> IDLE */ public void testPullReplicatonSendIdleStateAfterCheckPoint() throws Exception { Log.d(TAG, "TEST START"); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { // checkpoint PUT or GET response (sticky) (for both push and pull) MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // add non-sticky changes response that returns no changes (for pull) // this will cause the pull replicator to go into the IDLE state MockChangesFeed mockChangesFeedEmpty = new MockChangesFeed(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedEmpty.generateMockResponse()); // start mock server server.start(); // create pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); // handler to wait for IDLE final CountDownLatch pullInitialIdleState = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { pullInitialIdleState.countDown(); } } }); // start pull replication //pushReplication.start(); pullReplication.start(); // 1. Wait till replicator becomes IDLE boolean success = pullInitialIdleState.await(30, TimeUnit.SECONDS); assertTrue(success); // clear out existing queued mock responses to make room for new ones dispatcher.clearQueuedResponse(MockHelper.PATH_REGEX_CHANGES); // 2. Update change event handler for handling ACTIVE and IDLE final CountDownLatch runningSignal = new CountDownLatch(1); final CountDownLatch idleSignal = new CountDownLatch(1); pullReplication.addChangeListener(new Replication.ChangeListener() { @Override public void changed(Replication.ChangeEvent event) { Log.i(TAG, "[changed] PULL -> " + event); if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) { // make sure pull replicator becomes IDLE after ACTIVE state. // so ignore any IDLE state before ACTIVE. if (runningSignal.getCount() == 0) { idleSignal.countDown(); } } else if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.RUNNING) { runningSignal.countDown(); } } }); // 3. Create document into local db Document doc = database.createDocument(); Map<String, Object> props = new HashMap<String, Object>(); props.put("key", "1"); doc.putProperties(props); // 4. Based on local doc information, prepare mock change response for 1st /_changes request String docId = doc.getId(); String revId = doc.getCurrentRevisionId(); int lastSeq = (int) database.getLastSequenceNumber(); MockDocumentGet.MockDocument mockDocument1 = new MockDocumentGet.MockDocument(docId, revId, lastSeq + 1); mockDocument1.setJsonMap(MockHelper.generateRandomJsonMap()); MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // 5. Prepare next mock change response for 2nd /_changes request (blocking for while) MockChangesFeedNoResponse mockChangesFeedNoResponse2 = new MockChangesFeedNoResponse(); mockChangesFeedNoResponse2.setDelayMs(60 * 1000); mockChangesFeedNoResponse2.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedNoResponse2); // 6. wait for Replication IDLE -> RUNNING -> IDLE success = runningSignal.await(30, TimeUnit.SECONDS); assertTrue(success); success = idleSignal.await(30, TimeUnit.SECONDS); assertTrue(success); // stop pull replication stopReplication(pullReplication); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } Log.d(TAG, "TEST DONE"); } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/257 * <p/> * - Create local document with attachment * - Start continuous pull replication * - MockServer returns _changes with new rev of document * - MockServer returns doc multipart response: https://gist.github.com/tleyden/bf36f688d0b5086372fd * - Delete doc cache (not sure if needed) * - Fetch doc fresh from database * - Verify that it still has attachments */ public void testAttachmentsDeletedOnPull() throws Exception { String doc1Id = "doc1"; int doc1Rev2Generation = 2; String doc1Rev2Digest = "b000"; String doc1Rev2 = String.format(Locale.ENGLISH, "%d-%s", doc1Rev2Generation, doc1Rev2Digest); int doc1Seq1 = 1; String doc1AttachName = "attachment.png"; String contentType = "image/png"; // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); try { dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); server.start(); // add some documents - verify it has an attachment Document doc1 = createDocumentForPushReplication(doc1Id, doc1AttachName, contentType); String doc1Rev1 = doc1.getCurrentRevisionId(); doc1 = database.getDocument(doc1.getId()); assertTrue(doc1.getCurrentRevision().getAttachments().size() > 0); // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // checkpoint PUT response MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // add response to 1st _changes request final MockDocumentGet.MockDocument mockDocument1 = new MockDocumentGet.MockDocument( doc1Id, doc1Rev2, doc1Seq1); Map<String, Object> newProperties = new HashMap<String, Object>(doc1.getProperties()); newProperties.put("_rev", doc1Rev2); mockDocument1.setJsonMap(newProperties); mockDocument1.setAttachmentName(doc1AttachName); MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDocument1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // add sticky _changes response to feed=longpoll that just blocks for 60 seconds to emulate // server that doesn't have any new changes MockChangesFeedNoResponse mockChangesFeedNoResponse = new MockChangesFeedNoResponse(); mockChangesFeedNoResponse.setDelayMs(60 * 1000); mockChangesFeedNoResponse.setSticky(true); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeedNoResponse); // add response to doc get MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDocument1); mockDocumentGet.addAttachmentFilename(mockDocument1.getAttachmentName()); mockDocumentGet.setIncludeAttachmentPart(false); Map<String, Object> revHistory = new HashMap<String, Object>(); revHistory.put("start", doc1Rev2Generation); List ids = Arrays.asList( RevisionInternal.digestFromRevID(doc1Rev2), RevisionInternal.digestFromRevID(doc1Rev1) ); revHistory.put("ids", ids); mockDocumentGet.setRevHistoryMap(revHistory); dispatcher.enqueueResponse(mockDocument1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // create and start pull replication Replication pullReplication = database.createPullReplication(server.url("/db").url()); pullReplication.setContinuous(true); pullReplication.start(); // wait for the next PUT checkpoint request/response waitForPutCheckpointRequestWithSeq(dispatcher, 1); stopReplication(pullReplication); // make sure doc has attachments Document doc1Fetched = database.getDocument(doc1.getId()); assertTrue(doc1Fetched.getCurrentRevision().getAttachments().size() > 0); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } class CustomMultipartReaderDelegate implements MultipartReaderDelegate { public Map<String, String> headers = null; public byte[] data = null; public boolean gzipped = false; public boolean bJson = false; @Override public void startedPart(Map<String, String> headers) { gzipped = headers.get("Content-Encoding") != null && headers.get("Content-Encoding").contains("gzip"); bJson = headers.get("Content-Type") != null && headers.get("Content-Type").contains("application/json"); } @Override public void appendToPart(byte[] data) { if (gzipped && bJson) { this.data = Utils.decompressByGzip(data); } else if (bJson) { this.data = data; } } @Override public void appendToPart(final byte[] data, int off, int len) { byte[] b = Arrays.copyOfRange(data, off, len - off); appendToPart(b); } @Override public void finishedPart() { } } private Document createDocument(int number, boolean flag) { SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); Calendar calendar = GregorianCalendar.getInstance(); String currentTimeString = dateFormatter.format(calendar.getTime()); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("type", "test_doc"); properties.put("created_at", currentTimeString); if (flag == true) { properties.put("name", "Waldo"); } Document document = database.getDocument(String.valueOf(number)); try { document.putProperties(properties); } catch (CouchbaseLiteException e) { e.printStackTrace(); } return document; } private static Object str2json(String value) { Object result = null; try { result = Manager.getObjectMapper().readValue(value, Object.class); } catch (Exception e) { Log.w("Unable to parse JSON Query", e); } return result; } private static Map<String, String> query2map(String queryString) { Map<String, String> queries = new HashMap<String, String>(); for (String component : queryString.split("&")) { int location = component.indexOf('='); if (location > 0) { String key = component.substring(0, location); String value = component.substring(location + 1); queries.put(key, value); } } return queries; } // https://github.com/couchbase/couchbase-lite-java-core/issues/1421 public void testRestart() throws Exception { MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); try { dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); // mock documents to be pulled MockDocumentGet.MockDocument mockDoc1 = new MockDocumentGet.MockDocument("doc1", "1-5e38", 1); mockDoc1.setJsonMap(MockHelper.generateRandomJsonMap()); // checkpoint GET response w/ 404 MockResponse fakeCheckpointResponse = new MockResponse(); MockHelper.set404NotFoundJson(fakeCheckpointResponse); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, fakeCheckpointResponse); // one time 401 (Unauthorized) error: Response 401 for first /_changes API MockResponse response401 = new MockResponse(); response401.setResponseCode(401); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, response401); // _changes response without error: Response OK for second /_changes API. MockChangesFeed mockChangesFeed = new MockChangesFeed(); mockChangesFeed.add(new MockChangesFeed.MockChangedDoc(mockDoc1)); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHANGES, mockChangesFeed.generateMockResponse()); // Empty _all_docs response to pass unit tests dispatcher.enqueueResponse(MockHelper.PATH_REGEX_ALL_DOCS, new MockDocumentAllDocs()); // doc1 response MockDocumentGet mockDocumentGet = new MockDocumentGet(mockDoc1); dispatcher.enqueueResponse(mockDoc1.getDocPathRegex(), mockDocumentGet.generateMockResponse()); // _bulk_get response MockDocumentBulkGet mockBulkGet = new MockDocumentBulkGet(); mockBulkGet.addDocument(mockDoc1); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_BULK_GET, mockBulkGet); // respond to all PUT Checkpoint requests MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); mockCheckpointPut.setSticky(true); mockCheckpointPut.setDelayMs(500); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); // start mock server server.start(); // run replicator 1st round Replication repl = database.createPullReplication(server.url("/db").url()); runReplication(repl); Log.d(TAG, "pullReplication finished with fail"); // last error should not be null assertNotNull(repl.getLastError()); Log.e(TAG, "lastError", repl.getLastError()); assertEquals(0, repl.getChangesCount()); Document doc1 = database.getDocument(mockDoc1.getDocId()); assertNotNull(doc1); assertNull(doc1.getCurrentRevisionId()); // restart replicator 2nd round runReplication(repl); // last error should be null (cleared) assertNull(repl.getLastError()); assertEquals(1, repl.getChangesCount()); // assert that we now have both docs in local db assertNotNull(database); doc1 = database.getDocument(mockDoc1.getDocId()); assertNotNull(doc1); assertNotNull(doc1.getCurrentRevisionId()); assertTrue(doc1.getCurrentRevisionId().equals(mockDoc1.getDocRev())); assertNotNull(doc1.getProperties()); assertEquals(mockDoc1.getJsonMap(), doc1.getUserProperties()); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } }