From 7b483aa20f4ca8209f8e91017957f41cc0416647 Mon Sep 17 00:00:00 2001 From: Douglas Kaminsky Date: Tue, 18 Dec 2018 18:02:01 -0500 Subject: [PATCH 1/6] STITCH-2263 - remove ability to run l2r first and update tests --- README.md | 8 +- contrib/README.md | 13 +- .../sync/internal/DataSynchronizer.java | 25 +- .../core/testutils/sync/SyncIntTestProxy.kt | 2472 ++++++++--------- 4 files changed, 1234 insertions(+), 1284 deletions(-) diff --git a/README.md b/README.md index 709cee5a1..fc49c91bc 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,13 @@ implementation 'org.mongodb:stitch-server-services-twilio:4.1.3' 1. In Android Studio, go to Tools, Android, AVD manager. 2. Click Create Virtual Device. -3. Select a device that should run your app (the default is fine). +3. Select a device that should run your app. 4. Select and download a recommended system image of your choice (the latest is fine). - * x86_64 images are available in the x86 tab. + * This device must a system image built on an architecture supported by the +```libmongo``` library, e.g. x86_64 + * x86 images are unsupported + * x86_64 images are available in the x86 tab. + * Current minimum OS version: 21 5. Name your device and hit finish. #### Using the SDK diff --git a/contrib/README.md b/contrib/README.md index 38a922ac0..9ff935b07 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -37,4 +37,15 @@ The general publishing flow can be followed using `minor` as the bump type in `b ### Major Versions -The general publishing flow can be followed using `major` as the bump type in `bump_version`. In addition to this, the release on GitHub should be edited for a more readable format of key changes and include any migration steps needed to go from the last major version to this one. \ No newline at end of file +The general publishing flow can be followed using `major` as the bump type in `bump_version`. In addition to this, the release on GitHub should be edited for a more readable format of key changes and include any migration steps needed to go from the last major version to this one. + +### Testing (MongoDB Internal Contributors Only) + +* Before committing, the ```connectedDebugAndroidTest``` suite of integration tests must succeed. +* The tests require the following setup: + # You must enable clear text traffic in the core Android application locally (**do not commit this change**) + * In file *android/core/src/main/AndroidManifest.xml*, change the ```application``` XML tag as follows: + `````` + # You must run at least one ```mongod``` instance with replica sets initiated or a ```mongos``` instance with same locally on port 27000 + # You must run the Stitch server locally using the Android-specific configuration: + ```--configFile ./etc/configs/test_config_sdk_base.json --configFile ./etc/configs/test_config_sdk_android.json``` diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 9ee10ad71..28d61af81 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -432,22 +432,6 @@ public void close() { // ---- Core Synchronization Logic ----- - private boolean localToRemoteFirst = false; - - /** - * Swaps which sync direction comes first. Note: this should only be used for testing purposes. - * - * @param localToRemoteFirst whether or not L2R should go first. - */ - public void swapSyncDirection(final boolean localToRemoteFirst) { - syncLock.lock(); - try { - this.localToRemoteFirst = localToRemoteFirst; - } finally { - syncLock.unlock(); - } - } - /** * Performs a single synchronization pass in both the local and remote directions; the order * of which does not matter. If switching the order produces different results after one pass, @@ -485,13 +469,8 @@ public boolean doSyncPass() { return false; } - if (localToRemoteFirst) { - syncLocalToRemote(); - syncRemoteToLocal(); - } else { - syncRemoteToLocal(); - syncLocalToRemote(); - } + syncRemoteToLocal(); + syncLocalToRemote(); logger.info(String.format( Locale.US, diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt index 6086914e5..3dff419f6 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt @@ -52,845 +52,828 @@ import java.util.concurrent.atomic.AtomicInteger class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { @Test fun testSync() { - testSyncInBothDirections { - val remoteMethods = syncTestRunner.remoteMethods() - val syncOperations = syncTestRunner.syncMethods() - - val doc1 = Document("hello", "world") - val doc2 = Document("hello", "friend") - doc2["proj"] = "field" - remoteMethods.insertMany(listOf(doc1, doc2)) - - // get the document - val doc = remoteMethods.find(doc1).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // start watching it and always set the value to hello world in a conflict - syncOperations.configure(ConflictHandler { id: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> - // ensure that there is no version information on the documents in the conflict handler - assertNoVersionFieldsInDoc(localEvent.fullDocument) - assertNoVersionFieldsInDoc(remoteEvent.fullDocument) - - if (id == doc1Id) { - val merged = localEvent.fullDocument.getInteger("foo") + - remoteEvent.fullDocument.getInteger("foo") - val newDocument = Document(HashMap(remoteEvent.fullDocument)) - newDocument["foo"] = merged - newDocument - } else { - Document("hello", "world") - } - }, null, null) - - // sync on the remote document - syncOperations.syncOne(doc1Id) - streamAndSync() - - // 1. updating a document remotely should not be reflected until coming back online. - goOffline() - val doc1Update = Document("\$inc", Document("foo", 1)) - // document should successfully update locally. - // then sync - val result = remoteMethods.updateOne(doc1Filter, doc1Update) - assertEquals(1, result.matchedCount) - streamAndSync() - // because we are offline, the remote doc should not have updated - Assert.assertEquals(doc, syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()) - // go back online, and sync - // the remote document should now equal our expected update - goOnline() - streamAndSync() - val expectedDocument = Document(doc) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()) - - // 2. insertOne should work offline and then sync the document when online. - goOffline() - val doc3 = Document("so", "syncy") - val insResult = syncOperations.insertOne(doc3) - Assert.assertEquals( - doc3, - syncOperations.find(documentIdFilter(insResult.insertedId)).firstOrNull()!!) - streamAndSync() - Assert.assertNull(remoteMethods.find(Document("_id", doc3["_id"])).firstOrNull()) - goOnline() - streamAndSync() - Assert.assertEquals(doc3, withoutSyncVersion(remoteMethods.find(Document("_id", doc3["_id"])).first()!!)) - - // 3. updating a document locally that has been updated remotely should invoke the conflict - // resolver. - val sem = watchForEvents(syncTestRunner.namespace) - val result2 = remoteMethods.updateOne( - doc1Filter, - withNewSyncVersionSet(doc1Update)) - sem.acquire() - Assert.assertEquals(1, result2.matchedCount) - expectedDocument["foo"] = 2 - Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) - val result3 = syncOperations.updateOne( - documentIdFilter(doc1Id), - doc1Update) - Assert.assertEquals(1, result3.matchedCount) - expectedDocument["foo"] = 2 - Assert.assertEquals( - expectedDocument, - syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()!!) - // first pass will invoke the conflict handler and update locally but not remotely yet - streamAndSync() - Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) - expectedDocument["foo"] = 4 - expectedDocument.remove("fooOps") - Assert.assertEquals(expectedDocument, syncOperations.find(doc1Filter).first()!!) - // second pass will update with the ack'd version id - streamAndSync() - Assert.assertEquals(expectedDocument, syncOperations.find(doc1Filter).first()!!) - Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) - } + val remoteMethods = syncTestRunner.remoteMethods() + val syncOperations = syncTestRunner.syncMethods() + + val doc1 = Document("hello", "world") + val doc2 = Document("hello", "friend") + doc2["proj"] = "field" + remoteMethods.insertMany(listOf(doc1, doc2)) + + // get the document + val doc = remoteMethods.find(doc1).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // start watching it and always set the value to hello world in a conflict + syncOperations.configure(ConflictHandler { id: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> + // ensure that there is no version information on the documents in the conflict handler + assertNoVersionFieldsInDoc(localEvent.fullDocument) + assertNoVersionFieldsInDoc(remoteEvent.fullDocument) + + if (id == doc1Id) { + val merged = localEvent.fullDocument.getInteger("foo") + + remoteEvent.fullDocument.getInteger("foo") + val newDocument = Document(HashMap(remoteEvent.fullDocument)) + newDocument["foo"] = merged + newDocument + } else { + Document("hello", "world") + } + }, null, null) + + // sync on the remote document + syncOperations.syncOne(doc1Id) + streamAndSync() + + // 1. updating a document remotely should not be reflected until coming back online. + goOffline() + val doc1Update = Document("\$inc", Document("foo", 1)) + // document should successfully update locally. + // then sync + val result = remoteMethods.updateOne(doc1Filter, doc1Update) + assertEquals(1, result.matchedCount) + streamAndSync() + // because we are offline, the remote doc should not have updated + Assert.assertEquals(doc, syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()) + // go back online, and sync + // the remote document should now equal our expected update + goOnline() + streamAndSync() + val expectedDocument = Document(doc) + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()) + + // 2. insertOne should work offline and then sync the document when online. + goOffline() + val doc3 = Document("so", "syncy") + val insResult = syncOperations.insertOne(doc3) + Assert.assertEquals( + doc3, + syncOperations.find(documentIdFilter(insResult.insertedId)).firstOrNull()!!) + streamAndSync() + Assert.assertNull(remoteMethods.find(Document("_id", doc3["_id"])).firstOrNull()) + goOnline() + streamAndSync() + Assert.assertEquals(doc3, withoutSyncVersion(remoteMethods.find(Document("_id", doc3["_id"])).first()!!)) + + // 3. updating a document locally that has been updated remotely should invoke the conflict + // resolver. + val sem = watchForEvents(syncTestRunner.namespace) + val result2 = remoteMethods.updateOne( + doc1Filter, + withNewSyncVersionSet(doc1Update)) + sem.acquire() + Assert.assertEquals(1, result2.matchedCount) + expectedDocument["foo"] = 2 + Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) + val result3 = syncOperations.updateOne( + documentIdFilter(doc1Id), + doc1Update) + Assert.assertEquals(1, result3.matchedCount) + expectedDocument["foo"] = 2 + Assert.assertEquals( + expectedDocument, + syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()!!) + // first pass will invoke the conflict handler and update locally but not remotely yet + streamAndSync() + Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) + expectedDocument["foo"] = 4 + expectedDocument.remove("fooOps") + Assert.assertEquals(expectedDocument, syncOperations.find(doc1Filter).first()!!) + // second pass will update with the ack'd version id + streamAndSync() + Assert.assertEquals(expectedDocument, syncOperations.find(doc1Filter).first()!!) + Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testUpdateConflicts() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - val docToInsert = Document("hello", "world") - remoteColl.insertOne(docToInsert) - - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(ConflictHandler { _: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> - val merged = Document(localEvent.fullDocument) - remoteEvent.fullDocument.forEach { - if (localEvent.fullDocument.containsKey(it.key)) { - return@forEach - } - merged[it.key] = it.value + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + val docToInsert = Document("hello", "world") + remoteColl.insertOne(docToInsert) + + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + coll.configure(ConflictHandler { _: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> + val merged = Document(localEvent.fullDocument) + remoteEvent.fullDocument.forEach { + if (localEvent.fullDocument.containsKey(it.key)) { + return@forEach } - merged - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // Update remote - val remoteUpdate = withNewSyncVersionSet(Document("\$set", Document("remote", "update"))) - val sem = watchForEvents(syncTestRunner.namespace) - var result = remoteColl.updateOne(doc1Filter, remoteUpdate) - sem.acquire() - assertEquals(1, result.matchedCount) - val expectedRemoteDocument = Document(doc) - expectedRemoteDocument["remote"] = "update" - assertEquals(expectedRemoteDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - - // Update local - val localUpdate = Document("\$set", Document("local", "updateWow")) - result = coll.updateOne(doc1Filter, localUpdate) - assertEquals(1, result.matchedCount) - val expectedLocalDocument = Document(doc) - expectedLocalDocument["local"] = "updateWow" - assertEquals(expectedLocalDocument, coll.find(doc1Filter).first()!!) - - // first pass will invoke the conflict handler and update locally but not remotely yet - streamAndSync() - assertEquals(expectedRemoteDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedLocalDocument["remote"] = "update" - assertEquals(expectedLocalDocument, coll.find(doc1Filter).first()!!) - - // second pass will update with the ack'd version id - streamAndSync() - assertEquals(expectedLocalDocument, coll.find(doc1Filter).first()!!) - assertEquals(expectedLocalDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + merged[it.key] = it.value + } + merged + }, null, null) + coll.syncOne(doc1Id) + streamAndSync() + + // Update remote + val remoteUpdate = withNewSyncVersionSet(Document("\$set", Document("remote", "update"))) + val sem = watchForEvents(syncTestRunner.namespace) + var result = remoteColl.updateOne(doc1Filter, remoteUpdate) + sem.acquire() + assertEquals(1, result.matchedCount) + val expectedRemoteDocument = Document(doc) + expectedRemoteDocument["remote"] = "update" + assertEquals(expectedRemoteDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + + // Update local + val localUpdate = Document("\$set", Document("local", "updateWow")) + result = coll.updateOne(doc1Filter, localUpdate) + assertEquals(1, result.matchedCount) + val expectedLocalDocument = Document(doc) + expectedLocalDocument["local"] = "updateWow" + assertEquals(expectedLocalDocument, coll.find(doc1Filter).first()!!) + + // first pass will invoke the conflict handler and update locally but not remotely yet + streamAndSync() + assertEquals(expectedRemoteDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + expectedLocalDocument["remote"] = "update" + assertEquals(expectedLocalDocument, coll.find(doc1Filter).first()!!) + + // second pass will update with the ack'd version id + streamAndSync() + assertEquals(expectedLocalDocument, coll.find(doc1Filter).first()!!) + assertEquals(expectedLocalDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testUpdateRemoteWins() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a new document remotely - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - remoteColl.insertOne(docToInsert) - - // find the document we've just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to resolve conflicts with remote winning, - // synchronize the document, and stream events and do a sync pass - coll.configure(DefaultSyncConflictResolvers.remoteWins(), null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // update the document remotely while watching for an update - val expectedDocument = Document(doc) - val sem = watchForEvents(syncTestRunner.namespace) - var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - // once the event has been stored, - // fetch the remote document and assert that it has properly updated - sem.acquire() - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - - // update the local collection. - // the count field locally should be 2 - // the count field remotely should be 3 - result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // sync the collection. the remote document should be accepted - // and this resolution should be reflected locally and remotely - streamAndSync() - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document("hello", "world") + docToInsert["foo"] = 1 + remoteColl.insertOne(docToInsert) + + // find the document we've just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to resolve conflicts with remote winning, + // synchronize the document, and stream events and do a sync pass + coll.configure(DefaultSyncConflictResolvers.remoteWins(), null, null) + coll.syncOne(doc1Id) + streamAndSync() + + // update the document remotely while watching for an update + val expectedDocument = Document(doc) + val sem = watchForEvents(syncTestRunner.namespace) + var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) + // once the event has been stored, + // fetch the remote document and assert that it has properly updated + sem.acquire() + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 3 + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + + // update the local collection. + // the count field locally should be 2 + // the count field remotely should be 3 + result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // sync the collection. the remote document should be accepted + // and this resolution should be reflected locally and remotely + streamAndSync() + expectedDocument["foo"] = 3 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testUpdateLocalWins() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a new document remotely - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to resolve conflicts with local winning, - // synchronize the document, and stream events and do a sync pass - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // update the document remotely while watching for an update - val expectedDocument = Document(doc) - val sem = watchForEvents(syncTestRunner.namespace) - var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - // once the event has been stored, - // fetch the remote document and assert that it has properly updated - sem.acquire() - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - - // update the local collection. - // the count field locally should be 2 - // the count field remotely should be 3 - result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // sync the collection. the local document should be accepted - // and this resolution should be reflected locally and remotely - streamAndSync() - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document("hello", "world") + docToInsert["foo"] = 1 + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to resolve conflicts with local winning, + // synchronize the document, and stream events and do a sync pass + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + coll.syncOne(doc1Id) + streamAndSync() + + // update the document remotely while watching for an update + val expectedDocument = Document(doc) + val sem = watchForEvents(syncTestRunner.namespace) + var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) + // once the event has been stored, + // fetch the remote document and assert that it has properly updated + sem.acquire() + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 3 + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + + // update the local collection. + // the count field locally should be 2 + // the count field remotely should be 3 + result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // sync the collection. the local document should be accepted + // and this resolution should be reflected locally and remotely + streamAndSync() + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testDeleteOneByIdNoConflict() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - var doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to fail this test if a conflict occurs. - // sync on the id, and do a sync pass - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // update the document so it has a sync version (if we don't do this, then deleting - // the document will result in a conflict because a remote document with no version - // and a local document with no version are treated as documents with different - // versions) - coll.updateOne(doc1Filter, Document("\$set", Document("hello", "universe"))) - streamAndSync() - doc = remoteColl.find(doc1Filter).first()!! - - // go offline to avoid processing events. - // delete the document locally - goOffline() - val result = coll.deleteOne(doc1Filter) - assertEquals(1, result.deletedCount) - - // assert that, while the remote document remains - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - - // go online to begin the syncing process. - // when syncing, our local delete will be synced to the remote. - // assert that this is reflected remotely and locally - goOnline() - streamAndSync() - Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + var doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to fail this test if a conflict occurs. + // sync on the id, and do a sync pass + coll.configure(failingConflictHandler, null, null) + coll.syncOne(doc1Id) + streamAndSync() + + // update the document so it has a sync version (if we don't do this, then deleting + // the document will result in a conflict because a remote document with no version + // and a local document with no version are treated as documents with different + // versions) + coll.updateOne(doc1Filter, Document("\$set", Document("hello", "universe"))) + streamAndSync() + doc = remoteColl.find(doc1Filter).first()!! + + // go offline to avoid processing events. + // delete the document locally + goOffline() + val result = coll.deleteOne(doc1Filter) + assertEquals(1, result.deletedCount) + + // assert that, while the remote document remains + val expectedDocument = withoutSyncVersion(Document(doc)) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + + // go online to begin the syncing process. + // when syncing, our local delete will be synced to the remote. + // assert that this is reflected remotely and locally + goOnline() + streamAndSync() + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testDeleteOneByIdConflict() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to resolve a custom document on conflict. - // sync on the id, and do a sync pass - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("well", "shoot") - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // update the document remotely - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, remoteColl.updateOne( - doc1Filter, - withNewSyncVersionSet(doc1Update)).matchedCount) - - // go offline, and delete the document locally - goOffline() - val result = coll.deleteOne(doc1Filter) - assertEquals(1, result.deletedCount) - - // assert that the remote document has not been deleted, - // while the local document has been - val expectedDocument = Document(doc) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - - // go back online and sync. assert that the remote document has been updated - // while the local document reflects the resolution of the conflict - goOnline() - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedDocument.remove("hello") - expectedDocument.remove("foo") - expectedDocument["well"] = "shoot" - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to resolve a custom document on conflict. + // sync on the id, and do a sync pass + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + Document("well", "shoot") + }, null, null) + coll.syncOne(doc1Id) + streamAndSync() + + // update the document remotely + val doc1Update = Document("\$inc", Document("foo", 1)) + assertEquals(1, remoteColl.updateOne( + doc1Filter, + withNewSyncVersionSet(doc1Update)).matchedCount) + + // go offline, and delete the document locally + goOffline() + val result = coll.deleteOne(doc1Filter) + assertEquals(1, result.deletedCount) + + // assert that the remote document has not been deleted, + // while the local document has been + val expectedDocument = Document(doc) + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + + // go back online and sync. assert that the remote document has been updated + // while the local document reflects the resolution of the conflict + goOnline() + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + expectedDocument.remove("hello") + expectedDocument.remove("foo") + expectedDocument["well"] = "shoot" + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testInsertThenUpdateThenSync() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // configure Sync to fail this test if there is a conflict. - // insert and sync the new document locally - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOne(docToInsert) - - // find the local document we just inserted - val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // update the document locally - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) - - // assert that nothing has been inserting remotely - val expectedDocument = Document(doc) - expectedDocument["foo"] = 1 - Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // go online (in case we weren't already). sync. - goOnline() - streamAndSync() - - // assert that the local insertion reflects remotely - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // configure Sync to fail this test if there is a conflict. + // insert and sync the new document locally + val docToInsert = Document("hello", "world") + coll.configure(failingConflictHandler, null, null) + val insertResult = coll.insertOne(docToInsert) + + // find the local document we just inserted + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // update the document locally + val doc1Update = Document("\$inc", Document("foo", 1)) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) + + // assert that nothing has been inserting remotely + val expectedDocument = Document(doc) + expectedDocument["foo"] = 1 + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // go online (in case we weren't already). sync. + goOnline() + streamAndSync() + + // assert that the local insertion reflects remotely + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testInsertThenSyncUpdateThenUpdate() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // configure Sync to fail this test if there is a conflict. - // insert and sync the new document locally - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOne(docToInsert) - - // find the document we just inserted - val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // go online (in case we weren't already). sync. - // assert that the local insertion reflects remotely - goOnline() - streamAndSync() - val expectedDocument = Document(doc) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // update the document locally - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) - - // assert that this update has not been reflected remotely, but has locally - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // sync. assert that our update is reflected locally and remotely - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // configure Sync to fail this test if there is a conflict. + // insert and sync the new document locally + val docToInsert = Document("hello", "world") + coll.configure(failingConflictHandler, null, null) + val insertResult = coll.insertOne(docToInsert) + + // find the document we just inserted + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // go online (in case we weren't already). sync. + // assert that the local insertion reflects remotely + goOnline() + streamAndSync() + val expectedDocument = Document(doc) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // update the document locally + val doc1Update = Document("\$inc", Document("foo", 1)) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) + + // assert that this update has not been reflected remotely, but has locally + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // sync. assert that our update is reflected locally and remotely + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testInsertThenSyncThenRemoveThenInsertThenUpdate() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // configure Sync to fail this test if there is a conflict. - // insert and sync the new document locally. sync. - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOne(docToInsert) - streamAndSync() - - // assert the sync'd document is found locally and remotely - val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - val expectedDocument = Document(doc) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // delete the doc locally, then re-insert it. - // assert the document is still the same locally and remotely - assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) - coll.insertOne(doc) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // update the document locally - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) - - // assert that the document has not been updated remotely yet, - // but has locally - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // sync. assert that the update has been reflected remotely and locally - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // configure Sync to fail this test if there is a conflict. + // insert and sync the new document locally. sync. + val docToInsert = Document("hello", "world") + coll.configure(failingConflictHandler, null, null) + val insertResult = coll.insertOne(docToInsert) + streamAndSync() + + // assert the sync'd document is found locally and remotely + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + val expectedDocument = Document(doc) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // delete the doc locally, then re-insert it. + // assert the document is still the same locally and remotely + assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) + coll.insertOne(doc) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // update the document locally + val doc1Update = Document("\$inc", Document("foo", 1)) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) + + // assert that the document has not been updated remotely yet, + // but has locally + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // sync. assert that the update has been reflected remotely and locally + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testRemoteDeletesLocalNoConflict() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a new document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync with a conflict handler that fails this test - // in the event of conflict. sync the document, and sync. - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(coll.getSyncedIds().size, 1) - - // do a remote delete. wait for the event to be stored. sync. - val sem = watchForEvents(syncTestRunner.namespace) - remoteColl.deleteOne(doc1Filter) - sem.acquire() - streamAndSync() - - // assert that the remote deletion is reflected locally - Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - - // sync. this should not re-sync the document - streamAndSync() - - // insert the document again. sync. - remoteColl.insertOne(doc) - streamAndSync() - - // assert that the remote insertion is NOT reflected locally - assertEquals(doc, remoteColl.find(doc1Filter).first()) - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync with a conflict handler that fails this test + // in the event of conflict. sync the document, and sync. + coll.configure(failingConflictHandler, null, null) + coll.syncOne(doc1Id) + streamAndSync() + assertEquals(coll.getSyncedIds().size, 1) + + // do a remote delete. wait for the event to be stored. sync. + val sem = watchForEvents(syncTestRunner.namespace) + remoteColl.deleteOne(doc1Filter) + sem.acquire() + streamAndSync() + + // assert that the remote deletion is reflected locally + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + + // sync. this should not re-sync the document + streamAndSync() + + // insert the document again. sync. + remoteColl.insertOne(doc) + streamAndSync() + + // assert that the remote insertion is NOT reflected locally + assertEquals(doc, remoteColl.find(doc1Filter).first()) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testRemoteDeletesLocalConflict() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a new document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to resolve a custom document on conflict. - // sync on the id, do a sync pass, and assert that the remote - // insertion has been reflected locally - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("hello", "world") - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, coll.find(doc1Filter).firstOrNull()) - Assert.assertNotNull(coll.find(doc1Filter)) - - // go offline. - // delete the document remotely. - // update the document locally. - goOffline() - remoteColl.deleteOne(doc1Filter) - assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) - - // go back online and sync. assert that the document remains deleted remotely, - // but has not been reflected locally yet - goOnline() - streamAndSync() - Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) - - // sync again. assert that the resolution is reflected locally and remotely - streamAndSync() - Assert.assertNotNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to resolve a custom document on conflict. + // sync on the id, do a sync pass, and assert that the remote + // insertion has been reflected locally + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + Document("hello", "world") + }, null, null) + coll.syncOne(doc1Id) + streamAndSync() + assertEquals(doc, coll.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter)) + + // go offline. + // delete the document remotely. + // update the document locally. + goOffline() + remoteColl.deleteOne(doc1Filter) + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) + + // go back online and sync. assert that the document remains deleted remotely, + // but has not been reflected locally yet + goOnline() + streamAndSync() + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) + + // sync again. assert that the resolution is reflected locally and remotely + streamAndSync() + Assert.assertNotNull(remoteColl.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testRemoteInsertsLocalUpdates() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a new document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to resolve a custom document on conflict. - // sync on the id, do a sync pass, and assert that the remote - // insertion has been reflected locally - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("hello", "again") - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, coll.find(doc1Filter).firstOrNull()) - Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) - - // delete the document remotely, then reinsert it. - // wait for the events to stream - val wait = watchForEvents(syncTestRunner.namespace, 2) - remoteColl.deleteOne(doc1Filter) - remoteColl.insertOne(withNewSyncVersion(doc)) - wait.acquire() - - // update the local document concurrently. sync. - assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) - streamAndSync() - - // assert that the remote doc has not reflected the update. - // assert that the local document has received the resolution - // from the conflict handled - assertEquals(doc, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - val expectedDocument = Document("_id", doc1Id.value) - expectedDocument["hello"] = "again" - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // do another sync pass. assert that the local and remote docs are in sync - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to resolve a custom document on conflict. + // sync on the id, do a sync pass, and assert that the remote + // insertion has been reflected locally + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + Document("hello", "again") + }, null, null) + coll.syncOne(doc1Id) + streamAndSync() + assertEquals(doc, coll.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) + + // delete the document remotely, then reinsert it. + // wait for the events to stream + val wait = watchForEvents(syncTestRunner.namespace, 2) + remoteColl.deleteOne(doc1Filter) + remoteColl.insertOne(withNewSyncVersion(doc)) + wait.acquire() + + // update the local document concurrently. sync. + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) + streamAndSync() + + // assert that the remote doc has not reflected the update. + // assert that the local document has received the resolution + // from the conflict handled + assertEquals(doc, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + val expectedDocument = Document("_id", doc1Id.value) + expectedDocument["hello"] = "again" + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // do another sync pass. assert that the local and remote docs are in sync + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testRemoteInsertsWithVersionLocalUpdates() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(withNewSyncVersion(docToInsert)) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to fail this test if there is a conflict. - // sync the document, and do a sync pass. - // assert the remote insertion is reflected locally. - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(withoutSyncVersion(doc), coll.find(doc1Filter).firstOrNull()) - - // update the document locally. sync. - assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) - streamAndSync() - - // assert that the local update has been reflected remotely. - val expectedDocument = Document(withoutSyncVersion(doc)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(withNewSyncVersion(docToInsert)) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to fail this test if there is a conflict. + // sync the document, and do a sync pass. + // assert the remote insertion is reflected locally. + coll.configure(failingConflictHandler, null, null) + coll.syncOne(doc1Id) + streamAndSync() + assertEquals(withoutSyncVersion(doc), coll.find(doc1Filter).firstOrNull()) + + // update the document locally. sync. + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) + streamAndSync() + + // assert that the local update has been reflected remotely. + val expectedDocument = Document(withoutSyncVersion(doc)) + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testResolveConflictWithDelete() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a new document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(withNewSyncVersion(docToInsert)) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to resolve null when conflicted, effectively deleting - // the conflicted document. - // sync the docId, and do a sync pass. - // assert the remote insert is reflected locally - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - null - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(withoutSyncVersion(doc), coll.find(doc1Filter).firstOrNull()) - Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) - - // update the document remotely. wait for the update event to store. - val sem = watchForEvents(syncTestRunner.namespace) - assertEquals(1, remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 1)))).matchedCount) - sem.acquire() - - // update the document locally. - assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) - - // sync. assert that the remote document has received that update, - // but locally the document has resolved to deletion - streamAndSync() - val expectedDocument = Document(withoutSyncVersion(doc)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - goOffline() - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - - // go online and sync. the deletion should be reflected remotely and locally now - goOnline() - streamAndSync() - Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(withNewSyncVersion(docToInsert)) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to resolve null when conflicted, effectively deleting + // the conflicted document. + // sync the docId, and do a sync pass. + // assert the remote insert is reflected locally + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + null + }, null, null) + coll.syncOne(doc1Id) + streamAndSync() + assertEquals(withoutSyncVersion(doc), coll.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) + + // update the document remotely. wait for the update event to store. + val sem = watchForEvents(syncTestRunner.namespace) + assertEquals(1, remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 1)))).matchedCount) + sem.acquire() + + // update the document locally. + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) + + // sync. assert that the remote document has received that update, + // but locally the document has resolved to deletion + streamAndSync() + val expectedDocument = Document(withoutSyncVersion(doc)) + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + goOffline() + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + + // go online and sync. the deletion should be reflected remotely and locally now + goOnline() + streamAndSync() + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testTurnDeviceOffAndOn() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a document remotely - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // reload our configuration - powerCycleDevice() - - // configure Sync to resolve conflicts with a local win. - // sync the docId - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - coll.syncOne(doc1Id) - - // reload our configuration again. - // reconfigure sync and the same way. do a sync pass. - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - val sem = watchForEvents(syncTestRunner.namespace) - streamAndSync() - - // update the document remotely. assert the update is reflected remotely. - // reload our configuration again. reconfigure Sync again. - val expectedDocument = Document(doc) - var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - assertTrue(sem.tryAcquire(10, TimeUnit.SECONDS)) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - // update the document locally. assert its success, after reconfiguration. - result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - - // reconfigure again. - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - // sync. - streamAndSync() // does nothing with no conflict handler - - // assert we are still synced on one id. - // reconfigure again. - assertEquals(1, coll.getSyncedIds().size) - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - streamAndSync() // resolves the conflict - - // assert the update was reflected locally. reconfigure again. - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - // sync. assert that the update was reflected remotely - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a document remotely + val docToInsert = Document("hello", "world") + docToInsert["foo"] = 1 + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // reload our configuration + powerCycleDevice() + + // configure Sync to resolve conflicts with a local win. + // sync the docId + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + coll.syncOne(doc1Id) + + // reload our configuration again. + // reconfigure sync and the same way. do a sync pass. + powerCycleDevice() + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + val sem = watchForEvents(syncTestRunner.namespace) + streamAndSync() + + // update the document remotely. assert the update is reflected remotely. + // reload our configuration again. reconfigure Sync again. + val expectedDocument = Document(doc) + var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) + assertTrue(sem.tryAcquire(10, TimeUnit.SECONDS)) + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 3 + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + powerCycleDevice() + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + + // update the document locally. assert its success, after reconfiguration. + result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + + // reconfigure again. + powerCycleDevice() + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + + // sync. + streamAndSync() // does nothing with no conflict handler + + // assert we are still synced on one id. + // reconfigure again. + assertEquals(1, coll.getSyncedIds().size) + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + streamAndSync() // resolves the conflict + + // assert the update was reflected locally. reconfigure again. + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + powerCycleDevice() + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + + // sync. assert that the update was reflected remotely + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testDesync() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - - // insert and sync a new document. - // configure Sync to fail this test if there is a conflict. - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val doc1Id = coll.insertOne(docToInsert).insertedId - - // assert the document exists locally. desync it. - assertEquals(docToInsert, coll.find(documentIdFilter(doc1Id)).first()!!) - coll.desyncOne(doc1Id) - - // sync. assert that the desync'd document no longer exists locally - streamAndSync() - Assert.assertNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) - } + val coll = syncTestRunner.syncMethods() + + // insert and sync a new document. + // configure Sync to fail this test if there is a conflict. + val docToInsert = Document("hello", "world") + coll.configure(failingConflictHandler, null, null) + val doc1Id = coll.insertOne(docToInsert).insertedId + + // assert the document exists locally. desync it. + assertEquals(docToInsert, coll.find(documentIdFilter(doc1Id)).first()!!) + coll.desyncOne(doc1Id) + + // sync. assert that the desync'd document no longer exists locally + streamAndSync() + Assert.assertNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testInsertInsertConflict() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a new document remotely - val docToInsert = Document() - remoteColl.insertOne(docToInsert) - - // configure Sync to resolve a custom document when handling a conflict - // insert and sync the same document locally, creating a conflict - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("friend", "welcome") - }, null, null) - val doc1Id = coll.insertOne(docToInsert).insertedId - val doc1Filter = Document("_id", doc1Id) - - // sync. assert that the resolution is reflected locally, - // but not yet remotely. - streamAndSync() - val expectedDocument = Document(docToInsert) - expectedDocument["friend"] = "welcome" - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - assertEquals(docToInsert, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - - // sync again. assert that the resolution is reflected - // locally and remotely. - streamAndSync() - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document() + remoteColl.insertOne(docToInsert) + + // configure Sync to resolve a custom document when handling a conflict + // insert and sync the same document locally, creating a conflict + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + Document("friend", "welcome") + }, null, null) + val doc1Id = coll.insertOne(docToInsert).insertedId + val doc1Filter = Document("_id", doc1Id) + + // sync. assert that the resolution is reflected locally, + // but not yet remotely. + streamAndSync() + val expectedDocument = Document(docToInsert) + expectedDocument["friend"] = "welcome" + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertEquals(docToInsert, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + + // sync again. assert that the resolution is reflected + // locally and remotely. + streamAndSync() + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test @@ -937,94 +920,93 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { @Test fun testSyncVersioningScheme() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() + val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() + val remoteColl = syncTestRunner.remoteMethods() - val docToInsert = Document("hello", "world") + val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOne(docToInsert) + coll.configure(failingConflictHandler, null, null) + val insertResult = coll.insertOne(docToInsert) - val doc = coll.find(documentIdFilter(insertResult.insertedId)).first() - val doc1Id = BsonObjectId(doc?.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first() + val doc1Id = BsonObjectId(doc?.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) - goOnline() - streamAndSync() - val expectedDocument = Document(doc) + goOnline() + streamAndSync() + val expectedDocument = Document(doc) - // the remote document after an initial insert should have a fresh instance ID, and a - // version counter of 0 - val firstRemoteDoc = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(firstRemoteDoc)) + // the remote document after an initial insert should have a fresh instance ID, and a + // version counter of 0 + val firstRemoteDoc = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(firstRemoteDoc)) - assertEquals(0, versionCounterOf(firstRemoteDoc)) + assertEquals(0, versionCounterOf(firstRemoteDoc)) - assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) + assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) - // the remote document after a local update, but before a sync pass, should have the - // same version as the original document, and be equivalent to the unupdated document - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) + // the remote document after a local update, but before a sync pass, should have the + // same version as the original document, and be equivalent to the unupdated document + val doc1Update = Document("\$inc", Document("foo", 1)) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) - val secondRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDocBeforeSyncPass)) - assertEquals(versionOf(firstRemoteDoc), versionOf(secondRemoteDocBeforeSyncPass)) + val secondRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDocBeforeSyncPass)) + assertEquals(versionOf(firstRemoteDoc), versionOf(secondRemoteDocBeforeSyncPass)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) - // the remote document after a local update, and after a sync pass, should have a new - // version with the same instance ID as the original document, a version counter - // incremented by 1, and be equivalent to the updated document. - streamAndSync() - val secondRemoteDoc = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDoc)) - assertEquals(instanceIdOf(firstRemoteDoc), instanceIdOf(secondRemoteDoc)) - assertEquals(1, versionCounterOf(secondRemoteDoc)) + // the remote document after a local update, and after a sync pass, should have a new + // version with the same instance ID as the original document, a version counter + // incremented by 1, and be equivalent to the updated document. + streamAndSync() + val secondRemoteDoc = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDoc)) + assertEquals(instanceIdOf(firstRemoteDoc), instanceIdOf(secondRemoteDoc)) + assertEquals(1, versionCounterOf(secondRemoteDoc)) - assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) + assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) - // the remote document after a local delete and local insert, but before a sync pass, - // should have the same version as the previous document - assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) - coll.insertOne(doc!!) + // the remote document after a local delete and local insert, but before a sync pass, + // should have the same version as the previous document + assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) + coll.insertOne(doc!!) - val thirdRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDocBeforeSyncPass)) + val thirdRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDocBeforeSyncPass)) - expectedDocument.remove("foo") - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + expectedDocument.remove("foo") + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - // the remote document after a local delete and local insert, and after a sync pass, - // should have the same instance ID as before and a version count, since the change - // events are coalesced into a single update event - streamAndSync() + // the remote document after a local delete and local insert, and after a sync pass, + // should have the same instance ID as before and a version count, since the change + // events are coalesced into a single update event + streamAndSync() - val thirdRemoteDoc = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + val thirdRemoteDoc = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - assertEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(thirdRemoteDoc)) - assertEquals(2, versionCounterOf(thirdRemoteDoc)) + assertEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(thirdRemoteDoc)) + assertEquals(2, versionCounterOf(thirdRemoteDoc)) - // the remote document after a local delete, a sync pass, a local insert, and after - // another sync pass should have a new instance ID, with a version counter of zero, - // since the change events are not coalesced - assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) - streamAndSync() - coll.insertOne(doc) - streamAndSync() + // the remote document after a local delete, a sync pass, a local insert, and after + // another sync pass should have a new instance ID, with a version counter of zero, + // since the change events are not coalesced + assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) + streamAndSync() + coll.insertOne(doc) + streamAndSync() - val fourthRemoteDoc = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) - assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) + val fourthRemoteDoc = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) + assertEquals(expectedDocument, coll.find(doc1Filter).first()!!) - Assert.assertNotEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(fourthRemoteDoc)) - assertEquals(0, versionCounterOf(fourthRemoteDoc)) - } + Assert.assertNotEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(fourthRemoteDoc)) + assertEquals(0, versionCounterOf(fourthRemoteDoc)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test @@ -1060,306 +1042,301 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { @Test fun testStaleFetchSingle() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() - // insert a new document - val doc1 = Document("hello", "world") - remoteColl.insertOne(doc1) + // insert a new document + val doc1 = Document("hello", "world") + remoteColl.insertOne(doc1) - // find the document we just inserted - val doc = remoteColl.find(doc1).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) + // find the document we just inserted + val doc = remoteColl.find(doc1).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) - // configure Sync with a conflict handler that will freeze a document. - // sync the document - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) + // configure Sync with a conflict handler that will freeze a document. + // sync the document + coll.configure(failingConflictHandler, null, null) + coll.syncOne(doc1Id) - // sync. assert the document has been synced. - streamAndSync() - Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) + // sync. assert the document has been synced. + streamAndSync() + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) - // update the document locally. - coll.updateOne(documentIdFilter(doc1Id), Document("\$inc", Document("i", 1))) + // update the document locally. + coll.updateOne(documentIdFilter(doc1Id), Document("\$inc", Document("i", 1))) - // sync. assert the document still exists - streamAndSync() - Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) + // sync. assert the document still exists + streamAndSync() + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) - // sync. assert the document still exists - streamAndSync() - Assert.assertNotNull(coll.find(documentIdFilter(doc1Id))) - } + // sync. assert the document still exists + streamAndSync() + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id))) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testStaleFetchSingleDeleted() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() + val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() + val remoteColl = syncTestRunner.remoteMethods() - val doc1 = Document("hello", "world") - remoteColl.insertOne(doc1) + val doc1 = Document("hello", "world") + remoteColl.insertOne(doc1) - // get the document - val doc = remoteColl.find(doc1).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) + // get the document + val doc = remoteColl.find(doc1).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id) + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + throw IllegalStateException("failure") + }, null, null) + coll.syncOne(doc1Id) - streamAndSync() - Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) + streamAndSync() + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) - coll.updateOne(doc1Filter, Document("\$inc", Document("i", 1))) - streamAndSync() - Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) + coll.updateOne(doc1Filter, Document("\$inc", Document("i", 1))) + streamAndSync() + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) - assertEquals(1, remoteColl.deleteOne(doc1Filter).deletedCount) - powerCycleDevice() - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) + assertEquals(1, remoteColl.deleteOne(doc1Filter).deletedCount) + powerCycleDevice() + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + throw IllegalStateException("failure") + }, null, null) - streamAndSync() - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - } + streamAndSync() + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testStaleFetchMultiple() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() + val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() + val remoteColl = syncTestRunner.remoteMethods() - val insertResult = - remoteColl.insertMany(listOf( - Document("hello", "world"), - Document("hello", "friend"))) + val insertResult = + remoteColl.insertMany(listOf( + Document("hello", "world"), + Document("hello", "friend"))) - // get the document - val doc1Id = insertResult.insertedIds[0] - val doc2Id = insertResult.insertedIds[1] + // get the document + val doc1Id = insertResult.insertedIds[0] + val doc2Id = insertResult.insertedIds[1] - coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id!!) + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + throw IllegalStateException("failure") + }, null, null) + coll.syncOne(doc1Id!!) - streamAndSync() - Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) + streamAndSync() + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) - coll.updateOne(documentIdFilter(doc1Id), Document("\$inc", Document("i", 1))) - streamAndSync() - Assert.assertNotNull(coll.find(documentIdFilter(doc1Id))) + coll.updateOne(documentIdFilter(doc1Id), Document("\$inc", Document("i", 1))) + streamAndSync() + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id))) - coll.syncOne(doc2Id!!) - streamAndSync() - Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) - Assert.assertNotNull(coll.find(documentIdFilter(doc2Id)).firstOrNull()) - } + coll.syncOne(doc2Id!!) + streamAndSync() + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) + Assert.assertNotNull(coll.find(documentIdFilter(doc2Id)).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testShouldUpdateUsingUpdateDescription() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - val docToInsert = Document( - mapOf( - "i_am" to "the walrus", - "they_are" to "the egg men", - "members" to listOf( - "paul", "john", "george", "pete" - ), - "where_to_be" to mapOf( - "under_the_sea" to mapOf( - "octopus_garden" to "in the shade" - ), - "the_land_of_submarines" to mapOf( - "a_yellow_submarine" to "a yellow submarine" - ) + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + val docToInsert = Document( + mapOf( + "i_am" to "the walrus", + "they_are" to "the egg men", + "members" to listOf( + "paul", "john", "george", "pete" + ), + "where_to_be" to mapOf( + "under_the_sea" to mapOf( + "octopus_garden" to "in the shade" ), - "year" to 1960 - )) - val docAfterUpdate = Document.parse(""" - { - "i_am": "the egg men", - "they_are": "the egg men", - "members": [ "paul", "john", "george", "ringo" ], - "where_to_be": { - "under_the_sea": { - "octopus_garden": "near a cave" - }, - "the_land_of_submarines": { - "a_yellow_submarine": "a yellow submarine" - } - } - } - """) - val updateDoc = BsonDocument.parse(""" - { - "${'$'}set": { - "i_am": "the egg men", - "members": [ "paul", "john", "george", "ringo" ], - "where_to_be.under_the_sea.octopus_garden": "near a cave" + "the_land_of_submarines" to mapOf( + "a_yellow_submarine" to "a yellow submarine" + ) + ), + "year" to 1960 + )) + val docAfterUpdate = Document.parse(""" + { + "i_am": "the egg men", + "they_are": "the egg men", + "members": [ "paul", "john", "george", "ringo" ], + "where_to_be": { + "under_the_sea": { + "octopus_garden": "near a cave" }, - "${'$'}unset": { - "year": true + "the_land_of_submarines": { + "a_yellow_submarine": "a yellow submarine" } } - """) - - remoteColl.insertOne(docToInsert) - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - val eventSemaphore = Semaphore(0) - coll.configure(failingConflictHandler, ChangeEventListener { _, event -> - // ensure that there is no version information in the event document. - assertNoVersionFieldsInDoc(event.fullDocument) - - if (event.operationType == ChangeEvent.OperationType.UPDATE && - !event.hasUncommittedWrites()) { - assertEquals( - updateDoc["\$set"], - event.updateDescription.updatedFields) - assertEquals( - updateDoc["\$unset"], - BsonDocument( - event.updateDescription.removedFields.map { BsonElement(it, BsonBoolean(true)) })) - eventSemaphore.release() + } + """) + val updateDoc = BsonDocument.parse(""" + { + "${'$'}set": { + "i_am": "the egg men", + "members": [ "paul", "john", "george", "ringo" ], + "where_to_be.under_the_sea.octopus_garden": "near a cave" + }, + "${'$'}unset": { + "year": true } - }, null) - coll.syncOne(doc1Id) - streamAndSync() - - // because the "they_are" field has already been added, set - // a rule that prevents writing to the "they_are" field that we've added. - // a full replace would therefore break our rule, preventing validation. - // only an actual update document (with $set and $unset) - // can work for the rest of this test - syncTestRunner.mdbService.rules.rule(syncTestRunner.mdbRule._id).remove() - val result = coll.updateOne(doc1Filter, updateDoc) - assertEquals(1, result.matchedCount) - assertEquals(docAfterUpdate, withoutId(coll.find(doc1Filter).first()!!)) - - // set they_are to unwriteable. the update should only update i_am - // setting i_am to false and they_are to true would fail this test - syncTestRunner.mdbService.rules.create( - RuleCreator.MongoDb( - database = syncTestRunner.namespace.databaseName, - collection = syncTestRunner.namespace.collectionName, - roles = listOf( - RuleCreator.MongoDb.Role( - fields = Document( - "i_am", Document("write", true) - ).append( - "they_are", Document("write", false) - ).append( - "where_to_be.the_land_of_submarines", Document("write", false) - ) + } + """) + + remoteColl.insertOne(docToInsert) + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + val eventSemaphore = Semaphore(0) + coll.configure(failingConflictHandler, ChangeEventListener { _, event -> + // ensure that there is no version information in the event document. + assertNoVersionFieldsInDoc(event.fullDocument) + + if (event.operationType == ChangeEvent.OperationType.UPDATE && + !event.hasUncommittedWrites()) { + assertEquals( + updateDoc["\$set"], + event.updateDescription.updatedFields) + assertEquals( + updateDoc["\$unset"], + BsonDocument( + event.updateDescription.removedFields.map { BsonElement(it, BsonBoolean(true)) })) + eventSemaphore.release() + } + }, null) + coll.syncOne(doc1Id) + streamAndSync() + + // because the "they_are" field has already been added, set + // a rule that prevents writing to the "they_are" field that we've added. + // a full replace would therefore break our rule, preventing validation. + // only an actual update document (with $set and $unset) + // can work for the rest of this test + syncTestRunner.mdbService.rules.rule(syncTestRunner.mdbRule._id).remove() + val result = coll.updateOne(doc1Filter, updateDoc) + assertEquals(1, result.matchedCount) + assertEquals(docAfterUpdate, withoutId(coll.find(doc1Filter).first()!!)) + + // set they_are to unwriteable. the update should only update i_am + // setting i_am to false and they_are to true would fail this test + syncTestRunner.mdbService.rules.create( + RuleCreator.MongoDb( + database = syncTestRunner.namespace.databaseName, + collection = syncTestRunner.namespace.collectionName, + roles = listOf( + RuleCreator.MongoDb.Role( + fields = Document( + "i_am", Document("write", true) + ).append( + "they_are", Document("write", false) + ).append( + "where_to_be.the_land_of_submarines", Document("write", false) ) - ), - schema = RuleCreator.MongoDb.Schema() - ) + ) + ), + schema = RuleCreator.MongoDb.Schema() ) + ) - streamAndSync() - assertEquals(docAfterUpdate, withoutId(coll.find(doc1Filter).first()!!)) - assertEquals(docAfterUpdate, withoutId(withoutSyncVersion(remoteColl.find(doc1Filter).first()!!))) - assertTrue(eventSemaphore.tryAcquire(10, TimeUnit.SECONDS)) - } + streamAndSync() + assertEquals(docAfterUpdate, withoutId(coll.find(doc1Filter).first()!!)) + assertEquals(docAfterUpdate, withoutId(withoutSyncVersion(remoteColl.find(doc1Filter).first()!!))) + assertTrue(eventSemaphore.tryAcquire(10, TimeUnit.SECONDS)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testResumeSyncForDocumentResumesSync() { - testSyncInBothDirections { - val testSync = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - var errorEmitted = false - - var conflictCounter = 0 - - testSync.configure( - ConflictHandler { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> - if (conflictCounter == 0) { - conflictCounter++ - errorEmitted = true - throw Exception("ouch") - } - remoteEvent.fullDocument - }, - ChangeEventListener { _: BsonValue, _: ChangeEvent -> - }, - ErrorListener { _, _ -> - }) + val testSync = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + var errorEmitted = false + + var conflictCounter = 0 + + testSync.configure( + ConflictHandler { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> + if (conflictCounter == 0) { + conflictCounter++ + errorEmitted = true + throw Exception("ouch") + } + remoteEvent.fullDocument + }, + ChangeEventListener { _: BsonValue, _: ChangeEvent -> + }, + ErrorListener { _, _ -> + }) - // insert an initial doc - val testDoc = Document("hello", "world") - val result = testSync.insertOne(testDoc) + // insert an initial doc + val testDoc = Document("hello", "world") + val result = testSync.insertOne(testDoc) - // do a sync pass, synchronizing the doc - streamAndSync() + // do a sync pass, synchronizing the doc + streamAndSync() - Assert.assertNotNull(remoteColl.find(Document("_id", testDoc["_id"])).first()) + Assert.assertNotNull(remoteColl.find(Document("_id", testDoc["_id"])).first()) - // update the doc - val expectedDoc = Document("hello", "computer") - testSync.updateOne(documentIdFilter(result.insertedId), Document("\$set", expectedDoc)) + // update the doc + val expectedDoc = Document("hello", "computer") + testSync.updateOne(documentIdFilter(result.insertedId), Document("\$set", expectedDoc)) - // create a conflict - var sem = watchForEvents(syncTestRunner.namespace) - remoteColl.updateOne(Document("_id", result.insertedId), withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - sem.acquire() + // create a conflict + var sem = watchForEvents(syncTestRunner.namespace) + remoteColl.updateOne(Document("_id", result.insertedId), withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) + sem.acquire() - // do a sync pass, and throw an error during the conflict resolver - // freezing the document - streamAndSync() - Assert.assertTrue(errorEmitted) - assertEquals(result.insertedId, testSync.getPausedDocumentIds().first()) + // do a sync pass, and throw an error during the conflict resolver + // freezing the document + streamAndSync() + Assert.assertTrue(errorEmitted) + assertEquals(result.insertedId, testSync.getPausedDocumentIds().first()) - // update the doc remotely - val nextDoc = Document("hello", "friend") + // update the doc remotely + val nextDoc = Document("hello", "friend") - sem = watchForEvents(syncTestRunner.namespace) - remoteColl.updateOne(Document("_id", result.insertedId), nextDoc) - sem.acquire() - streamAndSync() + sem = watchForEvents(syncTestRunner.namespace) + remoteColl.updateOne(Document("_id", result.insertedId), nextDoc) + sem.acquire() + streamAndSync() - // it should not have updated the local doc, as the local doc should be paused - assertEquals( - withoutId(expectedDoc), - withoutId(testSync.find(Document("_id", result.insertedId)).first()!!)) + // it should not have updated the local doc, as the local doc should be paused + assertEquals( + withoutId(expectedDoc), + withoutId(testSync.find(Document("_id", result.insertedId)).first()!!)) - // resume syncing here - assertTrue(testSync.resumeSyncForDocument(result.insertedId)) - streamAndSync() + // resume syncing here + assertTrue(testSync.resumeSyncForDocument(result.insertedId)) + streamAndSync() - // update the doc remotely - val lastDoc = Document("good night", "computer") + // update the doc remotely + val lastDoc = Document("good night", "computer") - sem = watchForEvents(syncTestRunner.namespace) - remoteColl.updateOne(Document("_id", result.insertedId), withNewSyncVersion(lastDoc)) - sem.acquire() + sem = watchForEvents(syncTestRunner.namespace) + remoteColl.updateOne(Document("_id", result.insertedId), withNewSyncVersion(lastDoc)) + sem.acquire() - // now that we're sync'd and resumed, it should be reflected locally - streamAndSync() + // now that we're sync'd and resumed, it should be reflected locally + streamAndSync() - assertTrue(testSync.getPausedDocumentIds().isEmpty()) - assertEquals( - withoutId(lastDoc), - withoutId(testSync.find(Document("_id", result.insertedId)).first()!!)) - } + assertTrue(testSync.getPausedDocumentIds().isEmpty()) + assertEquals( + withoutId(lastDoc), + withoutId(testSync.find(Document("_id", result.insertedId)).first()!!)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test @@ -1546,175 +1523,168 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { @Test fun testSyncVersionFieldNotEditable() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // configure Sync to fail this test if there is a conflict. - - // 0. insert with bad version - // insert and sync a new document locally with a bad version field, and make sure it - // doesn't exist after the insert - val badVersionDoc = Document("bad", "version") - val docToInsert = Document("hello", "world") - .append("__stitch_sync_version", badVersionDoc) - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOne(docToInsert) - val localDocBeforeSync0 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - assertNoVersionFieldsInDoc(localDocBeforeSync0) - - streamAndSync() - - // assert the sync'd document is found locally and remotely, and that the version - // doesn't exist locally, and isn't the bad version doc remotely - val localDocAfterSync0 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - val docId = BsonObjectId(localDocAfterSync0.getObjectId("_id")) - val docFilter = Document("_id", docId) - - val remoteDoc0 = remoteColl.find(docFilter).first()!! - val remoteVersion0 = versionOf(remoteDoc0) - - val expectedDocument0 = Document(localDocAfterSync0) - assertEquals(expectedDocument0, withoutSyncVersion(remoteDoc0)) - assertEquals(expectedDocument0, localDocAfterSync0) - assertNotEquals(badVersionDoc, remoteVersion0) - assertEquals(0, versionCounterOf(remoteDoc0)) - - // 1. $set bad version counter - - // update the document, setting the version counter to 10, and a future version that - // we'll try to maliciously set but verify that before and after syncing, there is no - // version on the local doc, and that the version on the remote doc after syncing is - // correctly incremented by only one. - coll.updateOne( - docFilter, - Document("\$set", - Document() - .append("__stitch_sync_version.v", 10) - .append("futureVersion", badVersionDoc))) - - val localDocBeforeSync1 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - assertNoVersionFieldsInDoc(localDocBeforeSync1) - streamAndSync() - - val localDocAfterSync1 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - val remoteDoc1 = remoteColl.find(docFilter).first()!! - val expectedDocument1 = Document(localDocAfterSync1) - assertEquals(expectedDocument1, withoutSyncVersion(remoteDoc1)) - assertEquals(expectedDocument1, localDocAfterSync1) - - // verify the version only got incremented once - assertEquals(1, versionCounterOf(remoteDoc1)) - - // 2. $rename bad version doc - - // update the document, renaming our bad "futureVersion" field to - // "__stitch_sync_version", and assert that there is no version on the local doc, and - // that the version on the remote doc after syncing is correctly not incremented - coll.updateOne( - docFilter, - Document("\$rename", Document("futureVersion", "__stitch_sync_version"))) - - val localDocBeforeSync2 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - assertNoVersionFieldsInDoc(localDocBeforeSync2) - streamAndSync() - - val localDocAfterSync2 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - val remoteDoc2 = remoteColl.find(docFilter).first()!! - - // the expected doc is the doc without the futureVersion field (localDocAfterSync0) - assertEquals(localDocAfterSync0, withoutSyncVersion(remoteDoc2)) - assertEquals(localDocAfterSync0, localDocAfterSync2) - - // verify the version did get incremented - assertEquals(2, versionCounterOf(remoteDoc2)) - - // 3. unset - - // update the document, unsetting "__stitch_sync_version", and assert that there is no - // version on the local doc, and that the version on the remote doc after syncing - // is correctly not incremented because is basically a noop. - coll.updateOne( - docFilter, - Document("\$unset", Document("__stitch_sync_version", 1))) - - val localDocBeforeSync3 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - assertNoVersionFieldsInDoc(localDocBeforeSync3) - streamAndSync() - - val localDocAfterSync3 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! - val remoteDoc3 = remoteColl.find(docFilter).first()!! - - // the expected doc is the doc without the futureVersion field (localDocAfterSync0) - assertEquals(localDocAfterSync0, withoutSyncVersion(remoteDoc3)) - assertEquals(localDocAfterSync0, localDocAfterSync3) - - // verify the version did not get incremented, because this update was a noop - assertEquals(2, versionCounterOf(remoteDoc3)) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // configure Sync to fail this test if there is a conflict. + + // 0. insert with bad version + // insert and sync a new document locally with a bad version field, and make sure it + // doesn't exist after the insert + val badVersionDoc = Document("bad", "version") + val docToInsert = Document("hello", "world") + .append("__stitch_sync_version", badVersionDoc) + coll.configure(failingConflictHandler, null, null) + val insertResult = coll.insertOne(docToInsert) + val localDocBeforeSync0 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + assertNoVersionFieldsInDoc(localDocBeforeSync0) + + streamAndSync() + + // assert the sync'd document is found locally and remotely, and that the version + // doesn't exist locally, and isn't the bad version doc remotely + val localDocAfterSync0 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + val docId = BsonObjectId(localDocAfterSync0.getObjectId("_id")) + val docFilter = Document("_id", docId) + + val remoteDoc0 = remoteColl.find(docFilter).first()!! + val remoteVersion0 = versionOf(remoteDoc0) + + val expectedDocument0 = Document(localDocAfterSync0) + assertEquals(expectedDocument0, withoutSyncVersion(remoteDoc0)) + assertEquals(expectedDocument0, localDocAfterSync0) + assertNotEquals(badVersionDoc, remoteVersion0) + assertEquals(0, versionCounterOf(remoteDoc0)) + + // 1. $set bad version counter + + // update the document, setting the version counter to 10, and a future version that + // we'll try to maliciously set but verify that before and after syncing, there is no + // version on the local doc, and that the version on the remote doc after syncing is + // correctly incremented by only one. + coll.updateOne( + docFilter, + Document("\$set", + Document() + .append("__stitch_sync_version.v", 10) + .append("futureVersion", badVersionDoc))) + + val localDocBeforeSync1 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + assertNoVersionFieldsInDoc(localDocBeforeSync1) + streamAndSync() + + val localDocAfterSync1 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + val remoteDoc1 = remoteColl.find(docFilter).first()!! + val expectedDocument1 = Document(localDocAfterSync1) + assertEquals(expectedDocument1, withoutSyncVersion(remoteDoc1)) + assertEquals(expectedDocument1, localDocAfterSync1) + + // verify the version only got incremented once + assertEquals(1, versionCounterOf(remoteDoc1)) + + // 2. $rename bad version doc + + // update the document, renaming our bad "futureVersion" field to + // "__stitch_sync_version", and assert that there is no version on the local doc, and + // that the version on the remote doc after syncing is correctly not incremented + coll.updateOne( + docFilter, + Document("\$rename", Document("futureVersion", "__stitch_sync_version"))) + + val localDocBeforeSync2 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + assertNoVersionFieldsInDoc(localDocBeforeSync2) + streamAndSync() + + val localDocAfterSync2 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + val remoteDoc2 = remoteColl.find(docFilter).first()!! + + // the expected doc is the doc without the futureVersion field (localDocAfterSync0) + assertEquals(localDocAfterSync0, withoutSyncVersion(remoteDoc2)) + assertEquals(localDocAfterSync0, localDocAfterSync2) + + // verify the version did get incremented + assertEquals(2, versionCounterOf(remoteDoc2)) + + // 3. unset + + // update the document, unsetting "__stitch_sync_version", and assert that there is no + // version on the local doc, and that the version on the remote doc after syncing + // is correctly not incremented because is basically a noop. + coll.updateOne( + docFilter, + Document("\$unset", Document("__stitch_sync_version", 1))) + + val localDocBeforeSync3 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + assertNoVersionFieldsInDoc(localDocBeforeSync3) + streamAndSync() + + val localDocAfterSync3 = coll.find(documentIdFilter(insertResult.insertedId)).first()!! + val remoteDoc3 = remoteColl.find(docFilter).first()!! + + // the expected doc is the doc without the futureVersion field (localDocAfterSync0) + assertEquals(localDocAfterSync0, withoutSyncVersion(remoteDoc3)) + assertEquals(localDocAfterSync0, localDocAfterSync3) + + // verify the version did not get incremented, because this update was a noop + assertEquals(2, versionCounterOf(remoteDoc3)) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } @Test fun testConflictForEmptyVersionDocuments() { - testSyncInBothDirections { - val coll = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - - // insert a document remotely - val docToInsert = Document("hello", "world") - remoteColl.insertOne(docToInsert) - - // find the document we just inserted - var doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - // configure Sync to have local documents win conflicts - var conflictRaised = false - coll.configure( - ConflictHandler { _, localEvent, _ -> - conflictRaised = true - localEvent.fullDocument - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // go offline to avoid processing events. - // delete the document locally - goOffline() - val result = coll.deleteOne(doc1Filter) - assertEquals(1, result.deletedCount) - - // assert that the remote document remains - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - - // go online to begin the syncing process. When doing R2L first, a conflict should have - // occurred because both the local and remote instance of this document have no version - // information, meaning that the sync pass was forced to raise a conflict. our local - // delete should be synced to the remote, because we set up the conflict handler to - // have local always win. assert that this is reflected remotely and locally. - // When doing L2R first, no conflict will be raised since we didn't get a chance to - // fetch stale documents. this could result in the loss of events, but we are not - // making any guarantees about missing events if versions are not set until we - // always do R2L first - goOnline() - // do one sync pass to get the local delete to happen via conflict resolution - streamAndSync() - // do another sync pass to get the local delete resolution committed to the remote - streamAndSync() - - // if we did R2L first, make sure that a conflict was raised - val l2rFirstField = DataSynchronizer::class.java.getDeclaredField("localToRemoteFirst") - l2rFirstField.isAccessible = true - if (!l2rFirstField.getBoolean(syncTestRunner.dataSynchronizer)) { - Assert.assertTrue(conflictRaised) - } + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() - Assert.assertNull(coll.find(doc1Filter).firstOrNull()) - Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - } + // insert a document remotely + val docToInsert = Document("hello", "world") + remoteColl.insertOne(docToInsert) + + // find the document we just inserted + var doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + // configure Sync to have local documents win conflicts + var conflictRaised = false + coll.configure( + ConflictHandler { _, localEvent, _ -> + conflictRaised = true + localEvent.fullDocument + }, null, null) + coll.syncOne(doc1Id) + streamAndSync() + + // go offline to avoid processing events. + // delete the document locally + goOffline() + val result = coll.deleteOne(doc1Filter) + assertEquals(1, result.deletedCount) + + // assert that the remote document remains + val expectedDocument = withoutSyncVersion(Document(doc)) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + + // go online to begin the syncing process. When doing R2L first, a conflict should have + // occurred because both the local and remote instance of this document have no version + // information, meaning that the sync pass was forced to raise a conflict. our local + // delete should be synced to the remote, because we set up the conflict handler to + // have local always win. assert that this is reflected remotely and locally. + // (As a historical note, when we used to permit L2R first, no conflict would be raised + // since we didn't get a chance to fetch stale documents, potentially resulting in the + // loss of events + goOnline() + // do one sync pass to get the local delete to happen via conflict resolution + streamAndSync() + // do another sync pass to get the local delete resolution committed to the remote + streamAndSync() + + // make sure that a conflict was raised + Assert.assertTrue(conflictRaised) + + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) } private fun watchForEvents(namespace: MongoNamespace, n: Int = 1): Semaphore { @@ -1855,20 +1825,6 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { throw IllegalStateException("unreachable") } - private fun testSyncInBothDirections(testFun: () -> Unit) { - println("running tests with L2R going first") - syncTestRunner.dataSynchronizer.swapSyncDirection(true) - testFun() - assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) - - syncTestRunner.teardown() - syncTestRunner.setup() - println("running tests with R2L going first") - syncTestRunner.dataSynchronizer.swapSyncDirection(false) - testFun() - assertNoVersionFieldsInLocalColl(syncTestRunner.syncMethods()) - } - private fun assertNoVersionFieldsInLocalColl(coll: ProxySyncMethods) { coll.find().forEach { assertFalse(it!!.containsKey("__stitch_sync_version")) } } From feb99c24f49683fc11093bc04fafb01412b9e963 Mon Sep 17 00:00:00 2001 From: Douglas Kaminsky Date: Tue, 18 Dec 2018 18:22:13 -0500 Subject: [PATCH 2/6] STITCH-2263 - fix readme --- contrib/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/README.md b/contrib/README.md index 9ff935b07..8d41f00de 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -43,9 +43,9 @@ The general publishing flow can be followed using `major` as the bump type in `b * Before committing, the ```connectedDebugAndroidTest``` suite of integration tests must succeed. * The tests require the following setup: - # You must enable clear text traffic in the core Android application locally (**do not commit this change**) + * You must enable clear text traffic in the core Android application locally (**do not commit this change**) * In file *android/core/src/main/AndroidManifest.xml*, change the ```application``` XML tag as follows: `````` - # You must run at least one ```mongod``` instance with replica sets initiated or a ```mongos``` instance with same locally on port 27000 - # You must run the Stitch server locally using the Android-specific configuration: + * You must run at least one ```mongod``` instance with replica sets initiated or a ```mongos``` instance with same locally on port 27000 + * You must run the Stitch server locally using the Android-specific configuration: ```--configFile ./etc/configs/test_config_sdk_base.json --configFile ./etc/configs/test_config_sdk_android.json``` From 4889b632e63fef5fd95ccc40353b7f5632b15253 Mon Sep 17 00:00:00 2001 From: Douglas Kaminsky Date: Tue, 18 Dec 2018 18:27:37 -0500 Subject: [PATCH 3/6] STITCH-2263 - fix readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fc49c91bc..be982241c 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,14 @@ implementation 'org.mongodb:stitch-server-services-twilio:4.1.3' 1. In Android Studio, go to Tools, Android, AVD manager. 2. Click Create Virtual Device. -3. Select a device that should run your app. +3. Select a device that should run your app (as of Android Studio 3.2, the default device will no longer work properly for this purpose). 4. Select and download a recommended system image of your choice (the latest is fine). * This device must a system image built on an architecture supported by the ```libmongo``` library, e.g. x86_64 * x86 images are unsupported * x86_64 images are available in the x86 tab. * Current minimum OS version: 21 + * Please consult the latest information from the [MonboDB Mobile Documentation](https://docs.mongodb.com/stitch/mongodb/mobile/getting-started/) 5. Name your device and hit finish. #### Using the SDK From 672f8d27e6a93f895f3a120c6ac5247de4010206 Mon Sep 17 00:00:00 2001 From: Douglas Kaminsky Date: Tue, 18 Dec 2018 18:29:42 -0500 Subject: [PATCH 4/6] STITCH-2263 - fix readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be982241c..9f80db0a8 100644 --- a/README.md +++ b/README.md @@ -113,9 +113,9 @@ implementation 'org.mongodb:stitch-server-services-twilio:4.1.3' * This device must a system image built on an architecture supported by the ```libmongo``` library, e.g. x86_64 * x86 images are unsupported - * x86_64 images are available in the x86 tab. - * Current minimum OS version: 21 - * Please consult the latest information from the [MonboDB Mobile Documentation](https://docs.mongodb.com/stitch/mongodb/mobile/getting-started/) + * x86_64 images are available in the x86 tab. + * Current minimum OS version: 21 + * Please consult the latest information from the [MongoDB Mobile Documentation](https://docs.mongodb.com/stitch/mongodb/mobile/getting-started/) 5. Name your device and hit finish. #### Using the SDK From c1f549906d7641e8f4c64f692a70e802fdd9f8c6 Mon Sep 17 00:00:00 2001 From: Douglas Kaminsky Date: Tue, 18 Dec 2018 18:30:50 -0500 Subject: [PATCH 5/6] STITCH-2263 - fix readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9f80db0a8..14b72391e 100644 --- a/README.md +++ b/README.md @@ -110,12 +110,12 @@ implementation 'org.mongodb:stitch-server-services-twilio:4.1.3' 2. Click Create Virtual Device. 3. Select a device that should run your app (as of Android Studio 3.2, the default device will no longer work properly for this purpose). 4. Select and download a recommended system image of your choice (the latest is fine). - * This device must a system image built on an architecture supported by the + * This device must use a system image built on an architecture supported by the ```libmongo``` library, e.g. x86_64 * x86 images are unsupported * x86_64 images are available in the x86 tab. - * Current minimum OS version: 21 - * Please consult the latest information from the [MongoDB Mobile Documentation](https://docs.mongodb.com/stitch/mongodb/mobile/getting-started/) + * The current minimum OS version: 21 + * Please consult the the [MongoDB Mobile Documentation](https://docs.mongodb.com/stitch/mongodb/mobile/getting-started/) for more information about minimum device requirements. 5. Name your device and hit finish. #### Using the SDK From 31ea680feaecd5b0c6bd24cda7f49336fbdd0423 Mon Sep 17 00:00:00 2001 From: Douglas Kaminsky Date: Wed, 19 Dec 2018 12:17:42 -0500 Subject: [PATCH 6/6] STITCH-2263 - fix readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 14b72391e..aeffc8a4b 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,13 @@ implementation 'org.mongodb:stitch-server-services-twilio:4.1.3' 1. In Android Studio, go to Tools, Android, AVD manager. 2. Click Create Virtual Device. -3. Select a device that should run your app (as of Android Studio 3.2, the default device will no longer work properly for this purpose). +3. Select a device that should run your app (as of Android Studio 3.2, the default device does not work properly for this purpose). 4. Select and download a recommended system image of your choice (the latest is fine). * This device must use a system image built on an architecture supported by the ```libmongo``` library, e.g. x86_64 * x86 images are unsupported * x86_64 images are available in the x86 tab. - * The current minimum OS version: 21 + * The current minimum Android API version: 21 * Please consult the the [MongoDB Mobile Documentation](https://docs.mongodb.com/stitch/mongodb/mobile/getting-started/) for more information about minimum device requirements. 5. Name your device and hit finish.