diff --git a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt index 4be4fd8de..d0f125595 100644 --- a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt +++ b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt @@ -11,51 +11,106 @@ import com.mongodb.stitch.core.admin.authProviders.ProviderConfigs import com.mongodb.stitch.core.admin.services.ServiceConfigs import com.mongodb.stitch.core.admin.services.rules.RuleCreator import com.mongodb.stitch.core.auth.providers.anonymous.AnonymousCredential -import com.mongodb.stitch.core.internal.common.Callback -import com.mongodb.stitch.core.internal.common.OperationResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler -import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent -import com.mongodb.stitch.core.services.mongodb.remote.sync.DefaultSyncConflictResolvers import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener -import org.bson.BsonDocument -import org.bson.BsonObjectId +import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer +import com.mongodb.stitch.core.testutils.BaseStitchIntTest +import com.mongodb.stitch.core.testutils.sync.ProxyRemoteMethods +import com.mongodb.stitch.core.testutils.sync.ProxySyncMethods +import com.mongodb.stitch.core.testutils.sync.SyncIntTestProxy +import com.mongodb.stitch.core.testutils.sync.SyncIntTestRunner import org.bson.BsonValue import org.bson.Document +import org.bson.conversions.Bson import org.bson.types.ObjectId import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Assert.fail +import org.junit.Assert import org.junit.Assume import org.junit.Before import org.junit.Test -import java.lang.Exception -import java.util.UUID -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -class SyncMongoClientIntTests : BaseStitchAndroidIntTest() { +class SyncMongoClientIntTests : BaseStitchAndroidIntTest(), SyncIntTestRunner { + private class RemoteMethods(private val remoteMongoCollection: RemoteMongoCollection) : ProxyRemoteMethods { + override fun insertOne(document: Document): RemoteInsertOneResult { + return Tasks.await(remoteMongoCollection.insertOne(document)) + } + override fun insertMany(documents: List): RemoteInsertManyResult { + return Tasks.await(remoteMongoCollection.insertMany(documents)) + } - private val mongodbUriProp = "test.stitch.mongodbURI" + override fun find(filter: Document): Iterable { + return Tasks.await(remoteMongoCollection.find(filter).into(mutableListOf())) + } + + override fun updateOne(filter: Document, updateDocument: Document): RemoteUpdateResult { + return Tasks.await(remoteMongoCollection.updateOne(filter, updateDocument)) + } + + override fun deleteOne(filter: Bson): RemoteDeleteResult { + return Tasks.await(remoteMongoCollection.deleteOne(filter)) + } + } + + private class SyncMethods(private val sync: Sync) : ProxySyncMethods { + override fun configure( + conflictResolver: ConflictHandler, + changeEventListener: ChangeEventListener?, + errorListener: ErrorListener? + ) { + sync.configure(conflictResolver, changeEventListener, errorListener) + } + + override fun syncOne(id: BsonValue) { + sync.syncOne(id) + } + + override fun insertOneAndSync(document: Document): RemoteInsertOneResult { + return Tasks.await(sync.insertOneAndSync(document)) + } + + override fun findOneById(id: BsonValue): Document? { + return Tasks.await(sync.findOneById(id)) + } + + override fun updateOneById(documentId: BsonValue, update: Bson): RemoteUpdateResult { + return Tasks.await(sync.updateOneById(documentId, update)) + } + + override fun deleteOneById(documentId: BsonValue): RemoteDeleteResult { + return Tasks.await(sync.deleteOneById(documentId)) + } + + override fun desyncOne(id: BsonValue) { + sync.desyncOne(id) + } + + override fun getSyncedIds(): Set { + return sync.syncedIds + } + + override fun find(filter: Bson): Iterable { + return Tasks.await(sync.find(filter).into(mutableListOf())) + } + } - private var remoteMongoClientOpt: RemoteMongoClient? = null - private val remoteMongoClient: RemoteMongoClient - get() = remoteMongoClientOpt!! - private var mongoClientOpt: RemoteMongoClient? = null - private val mongoClient: RemoteMongoClient - get() = mongoClientOpt!! private var dbName = ObjectId().toHexString() private var collName = ObjectId().toHexString() - private var namespace = MongoNamespace(dbName, collName) + override var namespace = MongoNamespace(dbName, collName) + override val dataSynchronizer: DataSynchronizer + get() = (mongoClient as RemoteMongoClientImpl).dataSynchronizer + override val testNetworkMonitor: BaseStitchIntTest.TestNetworkMonitor + get() = BaseStitchAndroidIntTest.testNetworkMonitor - private fun getMongoDbUri(): String { - return InstrumentationRegistry.getArguments().getString(mongodbUriProp, "mongodb://localhost:26000") - } + private val mongodbUriProp = "test.stitch.mongodbURI" + private lateinit var remoteMongoClient: RemoteMongoClient + private lateinit var mongoClient: RemoteMongoClient + + private val testProxy = SyncIntTestProxy(this) @Before override fun setup() { @@ -68,15 +123,15 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest() { addProvider(app.second, ProviderConfigs.Anon) addProvider(app2.second, ProviderConfigs.Anon) val svc = addService( - app.second, - "mongodb", - "mongodb1", - ServiceConfigs.Mongo(getMongoDbUri())) + app.second, + "mongodb", + "mongodb1", + ServiceConfigs.Mongo(getMongoDbUri())) val svc2 = addService( - app2.second, - "mongodb", - "mongodb1", - ServiceConfigs.Mongo(getMongoDbUri())) + app2.second, + "mongodb", + "mongodb1", + ServiceConfigs.Mongo(getMongoDbUri())) val rule = Document() rule["read"] = Document() @@ -91,1214 +146,162 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest() { addRule(svc2.second, RuleCreator.MongoDb("$dbName.$collName", rule)) val client = getAppClient(app.first) - Tasks.await(client.auth.loginWithCredential(AnonymousCredential())) - mongoClientOpt = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") + client.auth.loginWithCredential(AnonymousCredential()) + mongoClient = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") (mongoClient as RemoteMongoClientImpl).dataSynchronizer.stop() (mongoClient as RemoteMongoClientImpl).dataSynchronizer.disableSyncThread() - remoteMongoClientOpt = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") - goOnline() + remoteMongoClient = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") + BaseStitchAndroidIntTest.testNetworkMonitor.connectedState = true } @After override fun teardown() { - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.close() + if (::mongoClient.isInitialized) { + (mongoClient as RemoteMongoClientImpl).dataSynchronizer.close() + } super.teardown() } - private fun getTestSync(): Sync { - val db = mongoClient.getDatabase(dbName) - assertEquals(dbName, db.name) + override fun remoteMethods(): ProxyRemoteMethods { + val db = remoteMongoClient.getDatabase(dbName) + Assert.assertEquals(dbName, db.name) val coll = db.getCollection(collName) - assertEquals(MongoNamespace(dbName, collName), coll.namespace) - return coll.sync() + Assert.assertEquals(MongoNamespace(dbName, collName), coll.namespace) + return RemoteMethods(coll) } - private fun getTestCollRemote(): RemoteMongoCollection { - val db = remoteMongoClient.getDatabase(dbName) - assertEquals(dbName, db.name) + override fun syncMethods(): ProxySyncMethods { + val db = mongoClient.getDatabase(dbName) + Assert.assertEquals(dbName, db.name) val coll = db.getCollection(collName) - assertEquals(MongoNamespace(dbName, collName), coll.namespace) - return coll + Assert.assertEquals(MongoNamespace(dbName, collName), coll.namespace) + return SyncMethods(coll.sync()) } @Test - fun testSync() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val doc1 = Document("hello", "world") - val doc2 = Document("hello", "friend") - doc2["proj"] = "field" - Tasks.await(remoteColl.insertMany(listOf(doc1, doc2))) - - // get the document - val doc = Tasks.await(remoteColl.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 - coll.configure({ id: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> - if (id.equals(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) - coll.syncOne(doc1Id) - streamAndSync() - - // 1. updating a document remotely should not be reflected until coming back online. - goOffline() - val doc1Update = Document("\$inc", Document("foo", 1)) - val result = Tasks.await(remoteColl.updateOne( - doc1Filter, - doc1Update)) - assertEquals(1, result.matchedCount) - streamAndSync() - assertEquals(doc, Tasks.await(coll.findOneById(doc1Id))) - goOnline() - streamAndSync() - val expectedDocument = Document(doc) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, Tasks.await(coll.findOneById(doc1Id))) - - // 2. insertOneAndSync should work offline and then sync the document when online. - goOffline() - val doc3 = Document("so", "syncy") - val insResult = Tasks.await(coll.insertOneAndSync(doc3)) - assertEquals(doc3, withoutSyncVersion(Tasks.await(coll.findOneById(insResult.insertedId))!!)) - streamAndSync() - assertNull(Tasks.await(remoteColl.find(Document("_id", doc3["_id"])).first())) - goOnline() - streamAndSync() - assertEquals(doc3, withoutSyncVersion(Tasks.await(remoteColl.find(Document("_id", doc3["_id"])).first())!!)) - - // 3. updating a document locally that has been updated remotely should invoke the conflict - // resolver. - val sem = watchForEvents(this.namespace) - val result2 = Tasks.await(remoteColl.updateOne( - doc1Filter, - withNewSyncVersionSet(doc1Update))) - sem.acquire() - assertEquals(1, result2.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - val result3 = Tasks.await(coll.updateOneById( - doc1Id, - doc1Update)) - assertEquals(1, result3.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - // first pass will invoke the conflict handler and update locally but not remotely yet - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - expectedDocument["foo"] = 4 - expectedDocument.remove("fooOps") - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - // second pass will update with the ack'd version id - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - } + override fun testSync() { + testProxy.testSync() } @Test - fun testUpdateConflicts() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure({ _: 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 - } - merged - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // Update remote - val remoteUpdate = withNewSyncVersionSet(Document("\$set", Document("remote", "update"))) - val sem = watchForEvents(this.namespace) - var result = Tasks.await(remoteColl.updateOne(doc1Filter, remoteUpdate)) - sem.acquire() - assertEquals(1, result.matchedCount) - val expectedRemoteDocument = Document(doc) - expectedRemoteDocument["remote"] = "update" - assertEquals(expectedRemoteDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - - // Update local - val localUpdate = Document("\$set", Document("local", "updateWow")) - result = Tasks.await(coll.updateOneById(doc1Id, localUpdate)) - assertEquals(1, result.matchedCount) - val expectedLocalDocument = Document(doc) - expectedLocalDocument["local"] = "updateWow" - assertEquals(expectedLocalDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - // first pass will invoke the conflict handler and update locally but not remotely yet - streamAndSync() - assertEquals(expectedRemoteDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - expectedLocalDocument["remote"] = "update" - assertEquals(expectedLocalDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - // second pass will update with the ack'd version id - streamAndSync() - assertEquals(expectedLocalDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - assertEquals(expectedLocalDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - } + override fun testUpdateConflicts() { + testProxy.testUpdateConflicts() } @Test - fun testUpdateRemoteWins() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(DefaultSyncConflictResolvers.remoteWins(), null, null) - coll.syncOne(doc1Id) - streamAndSync() - - val expectedDocument = Document(doc) - val sem = watchForEvents(this.namespace) - var result = Tasks.await(remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2))))) - sem.acquire() - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - result = Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1)))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - streamAndSync() - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - } + override fun testUpdateRemoteWins() { + testProxy.testUpdateRemoteWins() } @Test - fun testUpdateLocalWins() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - coll.syncOne(doc1Id) - streamAndSync() - - val expectedDocument = Document(doc) - val sem = watchForEvents(this.namespace) - var result = Tasks.await(remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2))))) - sem.acquire() - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - result = Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1)))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - streamAndSync() - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - } + override fun testUpdateLocalWins() { + testProxy.testUpdateLocalWins() } @Test - fun testDeleteOneByIdNoConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - goOffline() - val result = Tasks.await(coll.deleteOneById(doc1Id)) - assertEquals(1, result.deletedCount) - - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertNull(Tasks.await(coll.findOneById(doc1Id))) - - goOnline() - streamAndSync() - assertNull(Tasks.await(remoteColl.find(doc1Filter).first())) - assertNull(Tasks.await(coll.findOneById(doc1Id))) - } + override fun testDeleteOneByIdNoConflict() { + testProxy.testDeleteOneByIdNoConflict() } @Test - fun testDeleteOneByIdConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("well", "shoot") - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, Tasks.await(remoteColl.updateOne( - doc1Filter, - withNewSyncVersionSet(doc1Update))).matchedCount) - - goOffline() - val result = Tasks.await(coll.deleteOneById(doc1Id)) - assertEquals(1, result.deletedCount) - - val expectedDocument = Document(doc) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertNull(Tasks.await(coll.findOneById(doc1Id))) - - goOnline() - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - expectedDocument.remove("hello") - expectedDocument.remove("foo") - expectedDocument["well"] = "shoot" - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - } + override fun testDeleteOneByIdConflict() { + testProxy.testDeleteOneByIdConflict() } @Test - fun testInsertThenUpdateThenSync() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - - coll.configure(failingConflictHandler, null, null) - val insertResult = Tasks.await(coll.insertOneAndSync(docToInsert)) - - val doc = Tasks.await(coll.findOneById(insertResult.insertedId))!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, Tasks.await(coll.updateOneById(doc1Id, doc1Update)).matchedCount) - - val expectedDocument = withoutSyncVersion(Document(doc)) - expectedDocument["foo"] = 1 - assertNull(Tasks.await(remoteColl.find(doc1Filter).first())) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - goOnline() - streamAndSync() - - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - } + override fun testInsertThenUpdateThenSync() { + testProxy.testInsertThenUpdateThenSync() } @Test - fun testInsertThenSyncUpdateThenUpdate() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - - coll.configure(failingConflictHandler, null, null) - val insertResult = Tasks.await(coll.insertOneAndSync(docToInsert)) - - val doc = Tasks.await(coll.findOneById(insertResult.insertedId))!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - goOnline() - streamAndSync() - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, Tasks.await(coll.updateOneById(doc1Id, doc1Update)).matchedCount) - - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - } + override fun testInsertThenSyncUpdateThenUpdate() { + testProxy.testInsertThenSyncUpdateThenUpdate() } @Test - fun testInsertThenSyncThenRemoveThenInsertThenUpdate() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val insertResult = Tasks.await(coll.insertOneAndSync(docToInsert)) - streamAndSync() - - val doc = Tasks.await(coll.findOneById(insertResult.insertedId))!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - assertEquals(1, Tasks.await(coll.deleteOneById(doc1Id)).deletedCount) - Tasks.await(coll.insertOneAndSync(doc)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, Tasks.await(coll.updateOneById(doc1Id, doc1Update)).matchedCount) - - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - } + override fun testInsertThenSyncThenRemoveThenInsertThenUpdate() { + testProxy.testInsertThenSyncThenRemoveThenInsertThenUpdate() } @Test - fun testRemoteDeletesLocalNoConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - - assertEquals(coll.syncedIds.size, 1) - - val sem = watchForEvents(this.namespace) - Tasks.await(remoteColl.deleteOne(doc1Filter)) - sem.acquire() - - streamAndSync() - - assertNull(Tasks.await(remoteColl.find(doc1Filter).first())) - assertNull(Tasks.await(coll.findOneById(doc1Id))) - - // This should not re-sync the document - streamAndSync() - Tasks.await(remoteColl.insertOne(doc)) - streamAndSync() - - assertEquals(doc, Tasks.await(remoteColl.find(doc1Filter).first())) - assertNull(Tasks.await(coll.findOneById(doc1Id))) - } + override fun testRemoteDeletesLocalNoConflict() { + testProxy.testRemoteDeletesLocalNoConflict() } @Test - fun testRemoteDeletesLocalConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("hello", "world") - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, Tasks.await(coll.findOneById(doc1Id))) - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - goOffline() - Tasks.await(remoteColl.deleteOne(doc1Filter)) - assertEquals(1, Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1)))).matchedCount) - - goOnline() - streamAndSync() - assertNull(Tasks.await(remoteColl.find(doc1Filter).first())) - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - streamAndSync() - assertNotNull(Tasks.await(remoteColl.find(doc1Filter).first())) - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - } + override fun testRemoteDeletesLocalConflict() { + testProxy.testRemoteDeletesLocalConflict() } @Test - fun testRemoteInsertsLocalUpdates() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("hello", "again") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - - assertEquals(doc, Tasks.await(coll.findOneById(doc1Id))) - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - val wait = watchForEvents(this.namespace, 2) - Tasks.await(remoteColl.deleteOne(doc1Filter)) - Tasks.await(remoteColl.insertOne(withNewSyncVersion(doc))) - wait.acquire() - - assertEquals(1, Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1)))).matchedCount) - - streamAndSync() - - assertEquals(doc, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - val expectedDocument = Document("_id", doc1Id.value) - expectedDocument["hello"] = "again" - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - } + override fun testRemoteInsertsLocalUpdates() { + testProxy.testRemoteInsertsLocalUpdates() } @Test - fun testRemoteInsertsWithVersionLocalUpdates() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(withNewSyncVersion(docToInsert))) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, Tasks.await(coll.findOneById(doc1Id))) - - assertEquals(1, Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1)))).matchedCount) - - streamAndSync() - val expectedDocument = Document(withoutSyncVersion(doc)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - } + override fun testRemoteInsertsWithVersionLocalUpdates() { + testProxy.testRemoteInsertsWithVersionLocalUpdates() } @Test - fun testResolveConflictWithDelete() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - Tasks.await(remoteColl.insertOne(withNewSyncVersion(docToInsert))) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - null - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, Tasks.await(coll.findOneById(doc1Id))) - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - val sem = watchForEvents(this.namespace) - assertEquals(1, Tasks.await(remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 1))))).matchedCount) - sem.acquire() - - assertEquals(1, Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1)))).matchedCount) - - streamAndSync() - val expectedDocument = Document(withoutSyncVersion(doc)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - - goOffline() - assertNull(Tasks.await(coll.findOneById(doc1Id))) - - goOnline() - streamAndSync() - assertNull(Tasks.await(remoteColl.find(doc1Filter).first())) - assertNull(Tasks.await(coll.findOneById(doc1Id))) - } + override fun testResolveConflictWithDelete() { + testProxy.testResolveConflictWithDelete() } @Test - fun testTurnDeviceOffAndOn() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - powerCycleDevice() - - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - coll.syncOne(doc1Id) - - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - streamAndSync() - - val expectedDocument = Document(doc) - var result = Tasks.await(remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2))))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - result = Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1)))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - streamAndSync() // does nothing with no conflict handler - - assertEquals(1, coll.syncedIds.size) - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - streamAndSync() // resolves the conflict - - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - } + override fun testTurnDeviceOffAndOn() { + testProxy.testTurnDeviceOffAndOn() } @Test - fun testDesync() { - testSyncInBothDirections { - val coll = getTestSync() - - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val doc1Id = Tasks.await(coll.insertOneAndSync(docToInsert)).insertedId - - assertEquals(docToInsert, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - coll.desyncOne(doc1Id) - streamAndSync() - assertNull(Tasks.await(coll.findOneById(doc1Id))) - } + override fun testDesync() { + testProxy.testDesync() } @Test - fun testInsertInsertConflict() { - testSyncInBothDirections { - val coll = getTestSync() - val remoteColl = getTestCollRemote() - - val docToInsert = Document("_id", "hello") - - Tasks.await(remoteColl.insertOne(docToInsert)) - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("friend", "welcome") - }, null, null) - val doc1Id = Tasks.await(coll.insertOneAndSync(docToInsert)).insertedId - - val doc1Filter = Document("_id", doc1Id) - - streamAndSync() - val expectedDocument = Document(docToInsert) - expectedDocument["friend"] = "welcome" - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - assertEquals(docToInsert, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(remoteColl.find(doc1Filter).first())!!)) - } + override fun testInsertInsertConflict() { + testProxy.testInsertInsertConflict() } @Test - fun testFrozenDocumentConfig() { - testSyncInBothDirections { - val testSync = getTestSync() - val remoteColl = getTestCollRemote() - var errorEmitted = false - - var conflictCounter = 0 - - testSync.configure( - { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> - if (conflictCounter == 0) { - conflictCounter++ - errorEmitted = true - throw Exception("ouch") - } - remoteEvent.fullDocument - }, - { _: BsonValue, _: ChangeEvent -> - }, { _, _ -> - }) - - // insert an initial doc - val testDoc = Document("hello", "world") - val result = Tasks.await(testSync.insertOneAndSync(testDoc)) - - // do a sync pass, synchronizing the doc - streamAndSync() - - assertNotNull(Tasks.await(remoteColl.find(Document("_id", testDoc.get("_id"))).first())) - - // update the doc - val expectedDoc = Document("hello", "computer") - Tasks.await(testSync.updateOneById(result.insertedId, Document("\$set", expectedDoc))) - - // create a conflict - var sem = watchForEvents(namespace) - Tasks.await(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() - assertTrue(errorEmitted) - - // update the doc remotely - val nextDoc = Document("hello", "friend") - - sem = watchForEvents(namespace) - Tasks.await(remoteColl.updateOne(Document("_id", result.insertedId), nextDoc)) - sem.acquire() - streamAndSync() - - // it should not have updated the local doc, as the local doc should be frozen - assertEquals( - withoutId(expectedDoc), - withoutSyncVersion(withoutId(Tasks.await(testSync.find(Document("_id", result.insertedId)).first())!!))) - - // update the local doc. this should unfreeze the config - Tasks.await(testSync.updateOneById(result.insertedId, Document("\$set", Document("no", "op")))) - - streamAndSync() - - // this should still be the remote doc since remote wins - assertEquals( - withoutId(nextDoc), - withoutSyncVersion(withoutId(Tasks.await(testSync.find(Document("_id", result.insertedId)).first())!!))) - - // update the doc remotely - val lastDoc = Document("good night", "computer") - - sem = watchForEvents(namespace) - Tasks.await(remoteColl.updateOne( - Document("_id", result.insertedId), - withNewSyncVersion(lastDoc) - )) - sem.acquire() - - // now that we're sync'd and unfrozen, it should be reflected locally - // TODO: STITCH-1958 Possible race condition here for update listening - streamAndSync() - - assertEquals( - withoutId(lastDoc), - withoutSyncVersion( - withoutId(Tasks.await(testSync.find(Document("_id", result.insertedId)).first())!!))) - } + override fun testFrozenDocumentConfig() { + testProxy.testFrozenDocumentConfig() } @Test - fun testConfigure() { - val testSync = getTestSync() - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - val insertedId = Tasks.await(testSync.insertOneAndSync(docToInsert)).insertedId - - var hasConflictHandlerBeenInvoked = false - var hasChangeEventListenerBeenInvoked = false - - testSync.configure( - { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> - hasConflictHandlerBeenInvoked = true - assertEquals(remoteEvent.fullDocument["fly"], "away") - remoteEvent.fullDocument - }, - { _: BsonValue, _: ChangeEvent -> - hasChangeEventListenerBeenInvoked = true - }, - { _, _ -> } - ) - - val sem = watchForEvents(namespace) - Tasks.await(remoteColl.insertOne(Document("_id", insertedId).append("fly", "away"))) - sem.acquire() - - streamAndSync() - - assertTrue(hasConflictHandlerBeenInvoked) - assertTrue(hasChangeEventListenerBeenInvoked) + override fun testConfigure() { + testProxy.testConfigure() } @Test - fun testSyncVersioningScheme() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - - coll.configure(failingConflictHandler, null, null) - val insertResult = Tasks.await(coll.insertOneAndSync(docToInsert)) - - val doc = Tasks.await(coll.findOneById(insertResult.insertedId))!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - 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 = Tasks.await(remoteColl.find(doc1Filter).first())!! - assertEquals(expectedDocument, withoutSyncVersion(firstRemoteDoc)) - - assertEquals(0, versionCounterOf(firstRemoteDoc)) - - assertEquals(expectedDocument, Tasks.await(coll.findOneById(doc1Id))!!) - - // 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, Tasks.await(coll.updateOneById(doc1Id, doc1Update)).matchedCount) - - val secondRemoteDocBeforeSyncPass = Tasks.await(remoteColl.find(doc1Filter).first())!! - assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDocBeforeSyncPass)) - assertEquals(versionOf(firstRemoteDoc), versionOf(secondRemoteDocBeforeSyncPass)) - - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, Tasks.await(coll.findOneById(doc1Id))!!) - - // 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 = Tasks.await(remoteColl.find(doc1Filter).first())!! - assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDoc)) - assertEquals(instanceIdOf(firstRemoteDoc), instanceIdOf(secondRemoteDoc)) - assertEquals(1, versionCounterOf(secondRemoteDoc)) - - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id))!!)) - - // 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, Tasks.await(coll.deleteOneById(doc1Id)).deletedCount) - Tasks.await(coll.insertOneAndSync(doc)) - - val thirdRemoteDocBeforeSyncPass = Tasks.await(remoteColl.find(doc1Filter).first())!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDocBeforeSyncPass)) - - expectedDocument.remove("foo") - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id)))) - - // 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 = Tasks.await(remoteColl.find(doc1Filter).first())!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id)))) - - 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, Tasks.await(coll.deleteOneById(doc1Id)).deletedCount) - streamAndSync() - Tasks.await(coll.insertOneAndSync(doc)) - streamAndSync() - - val fourthRemoteDoc = Tasks.await(remoteColl.find(doc1Filter).first())!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) - assertEquals(expectedDocument, withoutSyncVersion(Tasks.await(coll.findOneById(doc1Id)))) - - assertNotEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(fourthRemoteDoc)) - assertEquals(0, versionCounterOf(fourthRemoteDoc)) - } + override fun testSyncVersioningScheme() { + testProxy.testSyncVersioningScheme() } @Test - fun testUnsupportedSpvFails() { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = withNewUnsupportedSyncVersion(Document("hello", "world")) - - val errorEmittedSem = Semaphore(0) - coll.configure( - failingConflictHandler, - null, - ErrorListener { documentId, error -> errorEmittedSem.release() }) - - Tasks.await(remoteColl.insertOne(docToInsert)) - - val doc = Tasks.await(remoteColl.find(docToInsert).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - coll.syncOne(doc1Id) - - assertTrue(coll.syncedIds.contains(doc1Id)) - - // syncing on this document with an unsupported spv should cause the document to desync - goOnline() - streamAndSync() - - assertFalse(coll.syncedIds.contains(doc1Id)) - - // an error should also have been emitted - assertTrue(errorEmittedSem.tryAcquire(10, TimeUnit.SECONDS)) + override fun testUnsupportedSpvFails() { + testProxy.testUnsupportedSpvFails() } @Test - fun testStaleFetchSingle() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val doc1 = Document("hello", "world") - Tasks.await(remoteColl.insertOne(doc1)) - - // get the document - val doc = Tasks.await(remoteColl.find(doc1).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1)))) - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - } + override fun testStaleFetchSingle() { + testProxy.testStaleFetchSingle() } @Test - fun testStaleFetchSingleDeleted() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val doc1 = Document("hello", "world") - Tasks.await(remoteColl.insertOne(doc1)) - - // get the document - val doc = Tasks.await(remoteColl.find(doc1).first())!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1)))) - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - assertEquals(1, Tasks.await(remoteColl.deleteOne(doc1Filter)).deletedCount) - powerCycleDevice() - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - - streamAndSync() - assertNull(Tasks.await(coll.findOneById(doc1Id))) - } + override fun testStaleFetchSingleDeleted() { + testProxy.testStaleFetchSingleDeleted() } @Test - fun testStaleFetchMultiple() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val insertResult = - Tasks.await(remoteColl.insertMany(listOf( - Document("hello", "world"), - Document("hello", "friend")))) - - // get the document - val doc1Id = insertResult.insertedIds[0] - val doc2Id = insertResult.insertedIds[1] - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - Tasks.await(coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1)))) - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - - coll.syncOne(doc2Id) - streamAndSync() - assertNotNull(Tasks.await(coll.findOneById(doc1Id))) - assertNotNull(Tasks.await(coll.findOneById(doc2Id))) - } - } - - private fun streamAndSync() { - val dataSync = (mongoClient as RemoteMongoClientImpl).dataSynchronizer - if (testNetworkMonitor.connectedState) { - while (!dataSync.areAllStreamsOpen()) { - println("waiting for all streams to open before doing sync pass") - Thread.sleep(1000) - } - } - dataSync.doSyncPass() + override fun testStaleFetchMultiple() { + testProxy.testStaleFetchMultiple() } - private fun watchForEvents( - namespace: MongoNamespace, - n: Int = 1 - ): Semaphore { - println("watching for $n change event(s) ns=$namespace") - val waitFor = AtomicInteger(n) - val sem = Semaphore(0) - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.addWatcher(namespace, object : Callback, Any> { - override fun onComplete(result: OperationResult, Any>) { - if (result.isSuccessful && result.geResult() != null) { - println("change event of operation ${result.geResult().operationType} ns=$namespace found!") - } - if (waitFor.decrementAndGet() == 0) { - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.removeWatcher(namespace, this) - sem.release() - } - } - }) - return sem - } - - private fun powerCycleDevice() { - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.reloadConfig() - } - - private fun goOffline() { - println("going offline") - testNetworkMonitor.connectedState = false - } - - private fun goOnline() { - println("going online") - testNetworkMonitor.connectedState = true - } - - private fun withoutId(document: Document): Document { - val newDoc = Document(document) - newDoc.remove("_id") - return newDoc - } - - private fun withoutSyncVersion(document: Document): Document { - val newDoc = Document(document) - newDoc.remove("__stitch_sync_version") - return newDoc - } - - private fun withNewSyncVersionSet(document: Document): Document { - return appendDocumentToKey( - "\$set", - document, - Document("__stitch_sync_version", freshSyncVersionDoc())) - } - - private fun withNewSyncVersion(document: Document): Document { - val newDocument = Document(java.util.HashMap(document)) - newDocument["__stitch_sync_version"] = freshSyncVersionDoc() - - return newDocument - } - - private fun withNewUnsupportedSyncVersion(document: Document): Document { - val newDocument = Document(java.util.HashMap(document)) - val badVersion = freshSyncVersionDoc() - badVersion.remove("spv") - badVersion.append("spv", 2) - - newDocument["__stitch_sync_version"] = badVersion - - return newDocument - } - - private fun freshSyncVersionDoc(): Document { - return Document("spv", 1).append("id", UUID.randomUUID().toString()).append("v", 0L) - } - - private fun versionOf(document: Document): Document { - return document["__stitch_sync_version"] as Document - } - - private fun versionCounterOf(document: Document): Long { - return versionOf(document)["v"] as Long - } - - private fun instanceIdOf(document: Document): String { - return versionOf(document)["id"] as String - } - - private fun appendDocumentToKey(key: String, on: Document, toAppend: Document): Document { - val newDocument = Document(HashMap(on)) - var found = false - newDocument.forEach { - if (it.key != key) { - return@forEach - } - found = true - val valueAtKey = (it.value as Document) - toAppend.forEach { - valueAtKey[it.key] = it.value - } - } - if (!found) { - newDocument[key] = toAppend - } - return newDocument - } - - private val failingConflictHandler: ConflictHandler = ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - fail("did not expect a conflict") - throw IllegalStateException("unreachable") - } - - private fun testSyncInBothDirections(testFun: () -> Unit) { - val dataSync = (mongoClient as RemoteMongoClientImpl).dataSynchronizer - println("running tests with L2R going first") - dataSync.swapSyncDirection(true) - testFun() - - teardown() - setup() - println("running tests with R2L going first") - dataSync.swapSyncDirection(false) - testFun() + /** + * Get the uri for where mongodb is running locally. + */ + private fun getMongoDbUri(): String { + return InstrumentationRegistry.getArguments().getString(mongodbUriProp, "mongodb://localhost:26000") } } diff --git a/android/testutils/src/main/java/com/mongodb/stitch/android/testutils/BaseStitchAndroidIntTest.kt b/android/testutils/src/main/java/com/mongodb/stitch/android/testutils/BaseStitchAndroidIntTest.kt index 08abc933e..5ea7e1ba8 100644 --- a/android/testutils/src/main/java/com/mongodb/stitch/android/testutils/BaseStitchAndroidIntTest.kt +++ b/android/testutils/src/main/java/com/mongodb/stitch/android/testutils/BaseStitchAndroidIntTest.kt @@ -10,39 +10,14 @@ import com.mongodb.stitch.core.admin.Apps import com.mongodb.stitch.core.admin.apps.AppResponse import com.mongodb.stitch.core.admin.userRegistrations.sendConfirmation import com.mongodb.stitch.core.auth.providers.userpassword.UserPasswordCredential -import com.mongodb.stitch.core.internal.net.NetworkMonitor import com.mongodb.stitch.core.testutils.BaseStitchIntTest import org.junit.After import org.junit.Before -import java.util.concurrent.CopyOnWriteArrayList open class BaseStitchAndroidIntTest : BaseStitchIntTest() { private var clients: MutableList = mutableListOf() - class TestNetworkMonitor : NetworkMonitor { - private var _connectedState = false - var connectedState: Boolean - set(value) { - _connectedState = value - listeners.forEach { it.onNetworkStateChanged() } - } - get() = _connectedState - - private var listeners = CopyOnWriteArrayList() - - override fun isConnected(): Boolean { - return connectedState - } - - override fun addNetworkStateListener(listener: NetworkMonitor.StateListener) { - listeners.add(listener) - } - - override fun removeNetworkStateListener(listener: NetworkMonitor.StateListener) { - listeners.remove(listener) - } - } companion object { val testNetworkMonitor = TestNetworkMonitor() } diff --git a/core/sdk/src/main/java/com/mongodb/stitch/core/internal/net/StitchEvent.java b/core/sdk/src/main/java/com/mongodb/stitch/core/internal/net/StitchEvent.java index fd366ccd2..01ce9a871 100644 --- a/core/sdk/src/main/java/com/mongodb/stitch/core/internal/net/StitchEvent.java +++ b/core/sdk/src/main/java/com/mongodb/stitch/core/internal/net/StitchEvent.java @@ -56,6 +56,11 @@ private StitchEvent(final String eventName, final String data, final Decoder decoder) { this.eventName = eventName; + if (data == null) { + this.data = null; + this.error = null; + return; + } final StringBuilder decodedStringBuilder = new StringBuilder(data.length()); for (int chIdx = 0; chIdx < data.length(); chIdx++) { diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java index cb1a30ee9..993ac4219 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java @@ -183,6 +183,8 @@ static BsonDocument toBsonDocument(final ChangeEvent value) { removedFields); asDoc.put(ChangeEventCoder.Fields.UPDATE_DESCRIPTION_FIELD, updateDescDoc); } + asDoc.put(ChangeEventCoder.Fields.WRITE_PENDING_FIELD, + new BsonBoolean(value.hasUncommittedWrites)); return asDoc; } @@ -235,7 +237,7 @@ static ChangeEvent fromBsonDocument(final BsonDocument document) { nsDoc.getString(ChangeEventCoder.Fields.NS_COLL_FIELD).getValue()), document.getDocument(ChangeEventCoder.Fields.DOCUMENT_KEY_FIELD), updateDescription, - nsDoc.getBoolean( + document.getBoolean( ChangeEventCoder.Fields.WRITE_PENDING_FIELD, BsonBoolean.FALSE).getValue()); } @@ -303,7 +305,7 @@ private static final class Fields { static final String UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD = "updatedFields"; static final String UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD = "removedFields"; - static final String WRITE_PENDING_FIELD = "write_pending"; + static final String WRITE_PENDING_FIELD = "writePending"; } } 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 2c8638d37..5ac04ee3d 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 @@ -338,7 +338,7 @@ public void swapSyncDirection(final boolean localToRemoteFirst) { * @return whether or not the synchronization pass was successful. */ public boolean doSyncPass() { - if (!syncLock.tryLock()) { + if (!this.isConfigured || !syncLock.tryLock()) { return false; } try { @@ -402,7 +402,7 @@ private void syncRemoteToLocal() { // 2. Run remote to local (R2L) sync routine for (final NamespaceSynchronizationConfig nsConfig : syncConfig) { final Map> remoteChangeEvents = - instanceChangeStreamListener.getEventsForNamespace(nsConfig.getNamespace()); + getEventsForNamespace(nsConfig.getNamespace()); final Set unseenIds = nsConfig.getStaleDocumentIds(); final Set latestDocumentsFromStale = @@ -1408,6 +1408,10 @@ public void removeWatcher(final MongoNamespace namespace, instanceChangeStreamListener.removeWatcher(namespace, watcher); } + Map> getEventsForNamespace(final MongoNamespace namespace) { + return instanceChangeStreamListener.getEventsForNamespace(namespace); + } + // ----- CRUD operations ----- /** @@ -1744,7 +1748,7 @@ private void deleteOneFromRemote( emitEvent(documentId, changeEventForLocalDelete(namespace, documentId, false)); } - void triggerListeningToNamespace(final MongoNamespace namespace) { + private void triggerListeningToNamespace(final MongoNamespace namespace) { syncLock.lock(); try { final NamespaceSynchronizationConfig nsConfig = this.syncConfig.getNamespaceConfig(namespace); diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEventUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEventUnitTests.kt new file mode 100644 index 000000000..7749fe64b --- /dev/null +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEventUnitTests.kt @@ -0,0 +1,123 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal + +import com.mongodb.MongoNamespace +import org.bson.BsonArray +import org.bson.BsonBoolean +import org.bson.BsonDocument +import org.bson.BsonObjectId +import org.bson.BsonString +import org.junit.Assert.assertEquals +import org.junit.Test + +class ChangeEventUnitTests { + private val namespace = MongoNamespace("foo", "bar") + + @Test + fun testNew() { + val expectedFullDocument = BsonDocument("foo", BsonString("bar")).append("_id", BsonObjectId()) + val expectedId = BsonDocument("_id", expectedFullDocument["_id"]) + val expectedOperationType = ChangeEvent.OperationType.INSERT + val expectedNamespace = namespace + val expectedDocumentKey = BsonDocument("_id", expectedFullDocument["_id"]) + val expectedUpdateDescription = ChangeEvent.UpdateDescription( + BsonDocument("foo", BsonString("bar")), + listOf("baz")) + + val changeEvent = ChangeEvent( + expectedId, + expectedOperationType, + expectedFullDocument, + expectedNamespace, + expectedDocumentKey, + expectedUpdateDescription, + true + ) + + assertEquals(expectedId, changeEvent.id) + assertEquals(expectedOperationType, changeEvent.operationType) + assertEquals(expectedFullDocument, changeEvent.fullDocument) + assertEquals(expectedNamespace, changeEvent.namespace) + assertEquals(expectedDocumentKey, changeEvent.documentKey) + assertEquals(expectedUpdateDescription, changeEvent.updateDescription) + assertEquals(true, changeEvent.hasUncommittedWrites()) + } + + @Test + fun testOperationTypeFromRemote() { + assertEquals( + ChangeEvent.OperationType.INSERT, + ChangeEvent.OperationType.fromRemote("insert")) + + assertEquals( + ChangeEvent.OperationType.UPDATE, + ChangeEvent.OperationType.fromRemote("update")) + + assertEquals( + ChangeEvent.OperationType.REPLACE, + ChangeEvent.OperationType.fromRemote("replace")) + + assertEquals( + ChangeEvent.OperationType.DELETE, + ChangeEvent.OperationType.fromRemote("delete")) + + assertEquals( + ChangeEvent.OperationType.UNKNOWN, + ChangeEvent.OperationType.fromRemote("bad")) + } + + @Test + fun testOperationTypeToRemote() { + assertEquals("insert", ChangeEvent.OperationType.INSERT.toRemote()) + assertEquals("update", ChangeEvent.OperationType.UPDATE.toRemote()) + assertEquals("replace", ChangeEvent.OperationType.REPLACE.toRemote()) + assertEquals("delete", ChangeEvent.OperationType.DELETE.toRemote()) + assertEquals("unknown", ChangeEvent.OperationType.UNKNOWN.toRemote()) + } + + @Test + fun testToBsonDocumentRoundTrip() { + val expectedFullDocument = BsonDocument("foo", BsonString("bar")).append("_id", BsonObjectId()) + val expectedId = BsonDocument("_id", expectedFullDocument["_id"]) + val expectedOperationType = ChangeEvent.OperationType.INSERT + val expectedNamespace = namespace + val expectedDocumentKey = BsonDocument("_id", expectedFullDocument["_id"]) + val expectedUpdateDescription = ChangeEvent.UpdateDescription( + BsonDocument("foo", BsonString("bar")), + listOf("baz")) + + val changeEvent = ChangeEvent( + expectedId, + expectedOperationType, + expectedFullDocument, + expectedNamespace, + expectedDocumentKey, + expectedUpdateDescription, + true) + + val changeEventDocument = ChangeEvent.toBsonDocument(changeEvent) + + assertEquals(expectedFullDocument, changeEventDocument["fullDocument"]) + assertEquals(expectedId, changeEventDocument["_id"]) + assertEquals(BsonString(expectedOperationType.toRemote()), changeEventDocument["operationType"]) + assertEquals( + BsonDocument("db", BsonString(namespace.databaseName)) + .append("coll", BsonString(namespace.collectionName)), + changeEventDocument["ns"]) + assertEquals(expectedDocumentKey, changeEventDocument["documentKey"]) + assertEquals( + BsonDocument("updatedFields", expectedUpdateDescription.updatedFields) + .append("removedFields", BsonArray(expectedUpdateDescription.removedFields.map { BsonString(it) })), + changeEventDocument["updateDescription"]) + assertEquals(BsonBoolean(true), changeEventDocument["writePending"]) + + val changeEventFromDocument = ChangeEvent.fromBsonDocument(changeEventDocument) + + assertEquals(expectedFullDocument, changeEventFromDocument.fullDocument) + assertEquals(expectedId, changeEventFromDocument.id) + assertEquals(expectedOperationType, changeEventFromDocument.operationType) + assertEquals(expectedDocumentKey, changeEventFromDocument.documentKey) + assertEquals(expectedUpdateDescription.updatedFields, changeEventFromDocument.updateDescription.updatedFields) + assertEquals(expectedUpdateDescription.removedFields, changeEventFromDocument.updateDescription.removedFields) + assertEquals(true, changeEventFromDocument.hasUncommittedWrites()) + } +} diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt index dd8604a86..190fd80f8 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt @@ -5,6 +5,7 @@ import com.mongodb.stitch.core.StitchAppClientInfo import com.mongodb.stitch.core.internal.common.AuthMonitor import com.mongodb.stitch.core.internal.common.BsonUtils import com.mongodb.stitch.core.internal.net.NetworkMonitor +import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncUnitTestHarness.Companion.compareEvents import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory import org.bson.BsonDocument import org.bson.BsonObjectId @@ -12,7 +13,7 @@ import org.bson.BsonString import org.bson.codecs.configuration.CodecRegistries import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test @@ -76,36 +77,36 @@ class CoreDocumentSynchronizationConfigUnitTests { } @Test - fun testStaleAndFrozen() { + fun testToBsonDocumentRoundTrip() { var config = CoreDocumentSynchronizationConfig(coll, namespace, id) - coll.insertOne(config) - - assertFalse(config.isStale) - - config.isStale = true + val expectedTestVersion = BsonDocument("dummy", BsonString("version")) + val expectedEvent = ChangeEvent.changeEventForLocalDelete(namespace, id, false) + config.setSomePendingWrites( + 1, + expectedTestVersion, + expectedEvent) config.isFrozen = true + config.isStale = true - var doc = config.toBsonDocument() + val doc = config.toBsonDocument() + + assertEquals(id, doc[CoreDocumentSynchronizationConfig.ConfigCodec.Fields.DOCUMENT_ID_FIELD]) assertTrue(doc.getBoolean(CoreDocumentSynchronizationConfig.ConfigCodec.Fields.IS_STALE).value) assertTrue(doc.getBoolean(CoreDocumentSynchronizationConfig.ConfigCodec.Fields.IS_FROZEN).value) + assertEquals(expectedTestVersion, + doc[CoreDocumentSynchronizationConfig.ConfigCodec.Fields.LAST_KNOWN_REMOTE_VERSION_FIELD]) + assertEquals( + BsonString("${namespace.databaseName}.${namespace.collectionName}"), + doc[CoreDocumentSynchronizationConfig.ConfigCodec.Fields.NAMESPACE_FIELD]) + assertNotNull(doc[CoreDocumentSynchronizationConfig.ConfigCodec.Fields.LAST_UNCOMMITTED_CHANGE_EVENT]) - config = CoreDocumentSynchronizationConfig( - coll, CoreDocumentSynchronizationConfig.fromBsonDocument(doc)) - - assertTrue(config.isStale) - - config.isStale = false - config.setSomePendingWrites( - 1, - ChangeEvent.changeEventForLocalInsert( - coll.namespace, BsonDocument("_id", BsonObjectId()), true)) + config = CoreDocumentSynchronizationConfig.fromBsonDocument(doc) - doc = config.toBsonDocument() - // should be stale from set some pending writes - assertTrue( - doc.getBoolean(CoreDocumentSynchronizationConfig.ConfigCodec.Fields.IS_STALE).value) - assertFalse( - doc.getBoolean(CoreDocumentSynchronizationConfig.ConfigCodec.Fields.IS_FROZEN).value) + assertTrue(config.isFrozen) + assertEquals(namespace, config.namespace) + assertEquals(expectedTestVersion, config.lastKnownRemoteVersion) + compareEvents(expectedEvent, config.lastUncommittedChangeEvent) + assertEquals(id, config.documentId) } } diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt new file mode 100644 index 000000000..8e6f42658 --- /dev/null +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt @@ -0,0 +1,210 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal + +import com.mongodb.MongoWriteException +import com.mongodb.stitch.core.services.mongodb.remote.RemoteFindOptions +import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory +import org.bson.BsonDocument +import org.bson.BsonInt32 +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +class CoreSyncUnitTests { + private val harness = SyncUnitTestHarness() + + @After + fun teardown() { + harness.teardown() + CoreRemoteClientFactory.close() + ServerEmbeddedMongoClientFactory.getInstance().close() + } + + @Test + fun testSyncOne() { + val ctx = harness.freshTestContext() + val (coreSync, _) = harness.createCoreSyncWithContext(ctx) + // assert that calling syncOne on coreSync proxies the appropriate call + // to the data synchronizer. assert that the appropriate document is being synchronized + coreSync.syncOne(ctx.testDocumentId) + verify(ctx.dataSynchronizer, times(1)).syncDocumentFromRemote( + eq(ctx.namespace), + eq(ctx.testDocumentId)) + assertEquals(1, ctx.dataSynchronizer.getSynchronizedDocuments(ctx.namespace).size) + assertEquals( + ctx.testDocumentId, + ctx.dataSynchronizer.getSynchronizedDocuments(ctx.namespace).first().documentId) + } + + @Test + fun testSyncMany() { + val ctx = harness.freshTestContext() + val (coreSync, _) = harness.createCoreSyncWithContext(ctx) + + // assert that calling syncMany on coreSync proxies the appropriate call to the data + // synchronizer for each document being sync'd + coreSync.syncMany(ctx.testDocumentId, ctx.testDocumentId) + verify(ctx.dataSynchronizer, times(2)).syncDocumentFromRemote( + eq(ctx.namespace), + eq(ctx.testDocumentId)) + } + + @Test + fun testFind() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + var findIterable = coreSync.find() + + val filterDoc = BsonDocument("_id", ctx.testDocumentId) + val sortDoc = BsonDocument("count", BsonInt32(-1)) + val projectionDoc = BsonDocument("count", BsonInt32(1)) + + assertNull(findIterable.filter(filterDoc).first()) + assertNull(findIterable.sort(sortDoc).first()) + assertNull(findIterable.projection(projectionDoc).first()) + assertNull(findIterable.limit(10).first()) + + ctx.insertTestDocument() + + findIterable = coreSync.find() + + val expectedRemoteFindOptions = RemoteFindOptions() + val remoteFindCaptor = ArgumentCaptor.forClass(RemoteFindOptions::class.java) + fun compareRemoteFindOptions( + expectedRemoteFindOptions: RemoteFindOptions, + actualRemoteFindOptions: RemoteFindOptions + ) { + assertEquals(expectedRemoteFindOptions.limit, actualRemoteFindOptions.limit) + assertEquals(expectedRemoteFindOptions.sort, actualRemoteFindOptions.sort) + assertEquals(expectedRemoteFindOptions.projection, actualRemoteFindOptions.projection) + } + + assertEquals( + ctx.testDocument, + SyncUnitTestHarness.withoutSyncVersion(findIterable.filter(filterDoc).first())) + verify(syncOperations, times(5)).findFirst(eq(filterDoc), eq(BsonDocument::class.java), remoteFindCaptor.capture()) + compareRemoteFindOptions(expectedRemoteFindOptions, remoteFindCaptor.value) + + expectedRemoteFindOptions.sort(sortDoc) + assertEquals( + ctx.testDocument, + SyncUnitTestHarness.withoutSyncVersion(findIterable.sort(sortDoc).first())) + verify(syncOperations, times(6)).findFirst(eq(filterDoc), eq(BsonDocument::class.java), remoteFindCaptor.capture()) + compareRemoteFindOptions(expectedRemoteFindOptions, remoteFindCaptor.value) + + expectedRemoteFindOptions.projection(projectionDoc) + assertEquals( + ctx.testDocument, + SyncUnitTestHarness.withoutSyncVersion(findIterable.projection(projectionDoc).first())) + verify(syncOperations, times(7)).findFirst(eq(filterDoc), eq(BsonDocument::class.java), remoteFindCaptor.capture()) + compareRemoteFindOptions(expectedRemoteFindOptions, remoteFindCaptor.value) + + expectedRemoteFindOptions.limit(10) + assertEquals( + ctx.testDocument, + SyncUnitTestHarness.withoutSyncVersion(findIterable.limit(10).first())) + verify(syncOperations, times(8)).findFirst(eq(filterDoc), eq(BsonDocument::class.java), remoteFindCaptor.capture()) + compareRemoteFindOptions(expectedRemoteFindOptions, remoteFindCaptor.value) + } + + @Test + fun testFindOneById() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + assertNull(coreSync.findOneById(ctx.testDocumentId)) + + ctx.insertTestDocument() + + assertEquals( + ctx.testDocument, + SyncUnitTestHarness.withoutSyncVersion(coreSync.findOneById(ctx.testDocumentId))) + + verify(syncOperations, times(2)).findOneById( + eq(ctx.testDocumentId), eq(BsonDocument::class.java)) + + verify(ctx.dataSynchronizer, times(2)).findOneById( + eq(ctx.namespace), eq(ctx.testDocumentId), eq(BsonDocument::class.java), any() + ) + } + + @Test + fun testUpdateOneById() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + var result = coreSync.updateOneById(ctx.testDocumentId, ctx.updateDocument) + assertEquals(0, result.matchedCount) + assertEquals(0, result.modifiedCount) + assertNull(result.upsertedId) + + ctx.insertTestDocument() + + result = coreSync.updateOneById(ctx.testDocumentId, ctx.updateDocument) + + assertEquals(1, result.matchedCount) + assertEquals(1, result.modifiedCount) + assertNull(result.upsertedId) + + verify(syncOperations, times(2)).updateOneById( + eq(ctx.testDocumentId), eq(ctx.updateDocument)) + + verify(ctx.dataSynchronizer, times(2)).updateOneById( + eq(ctx.namespace), eq(ctx.testDocumentId), eq(ctx.updateDocument)) + } + + @Test + fun testInsertOneAndSync() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + assertEquals( + ctx.testDocumentId, + coreSync.insertOneAndSync(ctx.testDocument).insertedId) + + try { + coreSync.insertOneAndSync(ctx.testDocument) + fail("should have received duplicate key error index") + } catch (e: MongoWriteException) { + assertTrue(e.message?.contains("E11000") ?: false) + assertNotNull(e) + } + + verify(syncOperations, times(2)).insertOneAndSync( + eq(ctx.testDocument)) + + verify(ctx.dataSynchronizer, times(2)).insertOneAndSync( + eq(ctx.namespace), eq(ctx.testDocument)) + } + + @Test + fun testDeleteOneById() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + var deleteResult = coreSync.deleteOneById(ctx.testDocumentId) + + assertEquals(0, deleteResult.deletedCount) + + ctx.insertTestDocument() + + deleteResult = coreSync.deleteOneById(ctx.testDocumentId) + + assertEquals(1, deleteResult.deletedCount) + + verify(syncOperations, times(2)).deleteOneById( + eq(ctx.testDocumentId)) + + verify(ctx.dataSynchronizer, times(2)).deleteOneById( + eq(ctx.namespace), eq(ctx.testDocumentId)) + } +} diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt new file mode 100644 index 000000000..3bb784229 --- /dev/null +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt @@ -0,0 +1,167 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal + +import com.mongodb.MongoNamespace +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.UpdateResult +import com.mongodb.stitch.core.internal.net.Event +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollectionImpl +import org.bson.BsonDocument +import org.bson.BsonValue +import java.lang.Exception + +/** + * Testing context to test a data synchronizer. + * + * Should be served fresh only by the [SyncUnitTestHarness]. + * + * Multiple instances of the testing context could result in + * race conditions if not opened and closed properly. + */ +interface DataSynchronizerTestContext { + val namespace: MongoNamespace + val testDocument: BsonDocument + val testDocumentId: BsonValue + var updateDocument: BsonDocument + + val collectionMock: CoreRemoteMongoCollectionImpl + var shouldConflictBeResolvedByRemote: Boolean + var exceptionToThrowDuringConflict: Exception? + + /** + * Whether or not we are online. Acts as a switch. + */ + var isOnline: Boolean + + /** + * Whether or not we are logged in. Acts as a switch. + */ + var isLoggedIn: Boolean + + /** + * A stream event to be consumed. Should be written to. + */ + var nextStreamEvent: Event + val dataSynchronizer: DataSynchronizer + + /** + * Reconfigure the dataSynchronizer. + */ + fun reconfigure() + + /** + * Wait for an error to be emitted. + */ + fun waitForError() + + /** + * Wait for an event to be emitted. + */ + fun waitForEvent() + + /** + * Reconfigure dataSynchronizer. Insert the contextual test document. + */ + fun insertTestDocument() + + /** + * Reconfigure dataSynchronizer. Update the contextual test document with + * the contextual update document. + */ + fun updateTestDocument(): UpdateResult + + /** + * Reconfigure dataSynchronizer. Delete the contextual test document. + */ + fun deleteTestDocument(): DeleteResult + + /** + * Reconfigure dataSynchronizer. Do a sync pass. + */ + fun doSyncPass() + + /** + * Attempt to find the contextual test document locally. + */ + fun findTestDocumentFromLocalCollection(): BsonDocument? + + /** + * Verify the changeEventListener was called for the test document. + */ + fun verifyChangeEventListenerCalledForActiveDoc(times: Int, expectedChangeEvent: ChangeEvent? = null) + + /** + * Verify the errorListener was called for the test document. + */ + fun verifyErrorListenerCalledForActiveDoc(times: Int, error: Exception? = null) + + /** + * Verify the conflict handler was called for the test document. + */ + fun verifyConflictHandlerCalledForActiveDoc( + times: Int, + expectedLocalConflictEvent: ChangeEvent? = null, + expectedRemoteConflictEvent: ChangeEvent? = null + ) + + /** + * Verify the stream function was called. + */ + fun verifyWatchFunctionCalled(times: Int, expectedArgs: List) + + /** + * Verify dataSynchronizer.start() has been called. + */ + fun verifyStartCalled(times: Int) + + /** + * Verify dataSynchronizer.stop() has been called. + */ + fun verifyStopCalled(times: Int) + + /** + * Queue a pseudo-remote insert event to be consumed during R2L. + */ + fun queueConsumableRemoteInsertEvent() + + /** + * Queue a pseudo-remote update event to be consumed during R2L. + */ + fun queueConsumableRemoteUpdateEvent() + + /** + * Queue a pseudo-remote delete event to be consumed during R2L. + */ + fun queueConsumableRemoteDeleteEvent() + + /** + * Queue a pseudo-remote unknown event to be consumed during R2L. + */ + fun queueConsumableRemoteUnknownEvent() + + /** + * Mock an exception when inserting into the remote collection. + */ + fun mockInsertException(exception: Exception) + + /** + * Mock a result when updating on remote collection. + */ + fun mockUpdateResult(remoteUpdateResult: RemoteUpdateResult) + + /** + * Mock an exception when updating on the remote collection. + */ + fun mockUpdateException(exception: Exception) + + /** + * Mock a result when deleting on the remote collection. + */ + fun mockDeleteResult(remoteDeleteResult: RemoteDeleteResult) + + /** + * Mock an exception when deleting on the remote collection. + */ + fun mockDeleteException(exception: Exception) +} diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt index f26c33f23..10ec50094 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt @@ -1,190 +1,887 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal -import com.mongodb.MongoNamespace -import com.mongodb.stitch.core.auth.internal.StitchAuthRequestClient -import com.mongodb.stitch.core.internal.common.AuthMonitor -import com.mongodb.stitch.core.internal.common.BsonUtils -import com.mongodb.stitch.core.internal.net.NetworkMonitor -import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient -import com.mongodb.stitch.core.services.internal.CoreStitchServiceClientImpl -import com.mongodb.stitch.core.services.internal.StitchServiceRoutes -import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoClientImpl -import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollectionImpl -import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoDatabaseImpl -import com.mongodb.stitch.core.services.mongodb.remote.internal.TestUtils -import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener -import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler -import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import com.mongodb.stitch.core.StitchServiceErrorCode +import com.mongodb.stitch.core.StitchServiceException +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncUnitTestHarness.Companion.withoutSyncVersion import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory import org.bson.BsonDocument -import org.bson.BsonObjectId - -import org.bson.codecs.BsonDocumentCodec +import org.bson.BsonInt32 import org.junit.After + +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify -import java.util.Random +import java.lang.Exception class DataSynchronizerUnitTests { + companion object { + private fun setupPendingReplace( + ctx: DataSynchronizerTestContext, + expectedDocument: BsonDocument, + shouldConflictBeResolvedByRemote: Boolean = false, + shouldWaitForError: Boolean = false + ) { + ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) + ctx.queueConsumableRemoteInsertEvent() + ctx.dataSynchronizer.syncDocumentFromRemote(ctx.namespace, ctx.testDocumentId) + ctx.doSyncPass() + + // prepare a remote update and a local update. + // do a sync pass, accepting the local doc. this will create + // a pending replace to be sync'd on the next pass + ctx.queueConsumableRemoteUpdateEvent() + // set a different update doc than the remote + ctx.updateDocument = BsonDocument("\$inc", BsonDocument("count", BsonInt32(2))) + ctx.updateTestDocument() + // set it back + ctx.updateDocument = BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))) + ctx.shouldConflictBeResolvedByRemote = shouldConflictBeResolvedByRemote + + ctx.doSyncPass() + + if (shouldWaitForError) { + ctx.waitForError() + } else { + ctx.waitForEvent() + } + + val expectedChangeEvent = if (shouldConflictBeResolvedByRemote) + ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) + else ChangeEvent.changeEventForLocalInsert(ctx.namespace, expectedDocument, true) + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = if (shouldWaitForError) 0 else 1, + expectedChangeEvent = if (shouldWaitForError) null else expectedChangeEvent) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 1) + ctx.verifyErrorListenerCalledForActiveDoc(times = if (shouldWaitForError) 1 else 0, + error = if (shouldWaitForError) ctx.exceptionToThrowDuringConflict else null) + } + } + + private val harness = SyncUnitTestHarness() + @After fun teardown() { + harness.teardown() CoreRemoteClientFactory.close() ServerEmbeddedMongoClientFactory.getInstance().close() } - private val namespace = MongoNamespace("foo", "bar") - private val networkMonitor = object : NetworkMonitor { - override fun removeNetworkStateListener(listener: NetworkMonitor.StateListener) { - } + @Test + fun testNew() { + val ctx = harness.freshTestContext(shouldPreconfigure = false) - override fun isConnected(): Boolean { - return true - } + // a fresh, non-configured dataSynchronizer should not be running. + assertFalse(ctx.dataSynchronizer.isRunning) + } - override fun addNetworkStateListener(listener: NetworkMonitor.StateListener) { - } + @Test + fun testOnNetworkStateChanged() { + val ctx = harness.freshTestContext() + + // verify that, since we are offline, start has not been called + ctx.isOnline = false + assertFalse(ctx.dataSynchronizer.isRunning) + ctx.verifyStartCalled(0) + ctx.verifyStopCalled(2) + + // verify that, since we are online, the dataSync has started + ctx.isOnline = true + ctx.verifyStartCalled(1) + ctx.verifyStopCalled(2) + } + + @Test + fun testStartAndStop() { + val ctx = harness.freshTestContext(shouldPreconfigure = false) + assertFalse(ctx.dataSynchronizer.isRunning) + ctx.reconfigure() + + // with a configuration, we should be running + assertTrue(ctx.dataSynchronizer.isRunning) + + ctx.dataSynchronizer.stop() + assertFalse(ctx.dataSynchronizer.isRunning) + } + + @Test + fun testSuccessfulInsert() { + val ctx = harness.freshTestContext() + + // insert the doc, wait, sync, and assert that the expected change events are emitted + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + ctx.namespace, + ctx.testDocument, + true)) + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + ctx.namespace, + ctx.testDocument, + false)) + + // verify the appropriate doc was inserted + val docCaptor = ArgumentCaptor.forClass(BsonDocument::class.java) + verify(ctx.collectionMock, times(1)).insertOne(docCaptor.capture()) + assertEquals(ctx.testDocument, withoutSyncVersion(docCaptor.value)) + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + // verify the conflict and error handlers not called + ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + } + + @Test + fun testConflictedInsert() { + val duplicateInsertException = StitchServiceException("E11000", StitchServiceErrorCode.MONGODB_ERROR) + var ctx = harness.freshTestContext() + // setup our expectations + ctx.mockInsertException(duplicateInsertException) + + // 1: Insert -> Conflict -> Delete (remote wins) + // insert the expected doc, waiting for the change event + // assert we inserted it properly + ctx.insertTestDocument() + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + + // sync and assert that the conflict handler was called, + // accepting the remote delete, nullifying the document + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) + ctx.verifyConflictHandlerCalledForActiveDoc( + times = 1, + expectedLocalConflictEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), + expectedRemoteConflictEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + assertNull(ctx.findTestDocumentFromLocalCollection()) + + // 2: Insert -> Conflict -> Insert (local wins) + // reset + ctx = harness.freshTestContext() + ctx.mockInsertException(duplicateInsertException) + ctx.insertTestDocument() + + // accept the local event this time, which will insert the local doc. + // assert that the local doc has been inserted + ctx.shouldConflictBeResolvedByRemote = false + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) + ctx.verifyConflictHandlerCalledForActiveDoc( + times = 1, + expectedLocalConflictEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), + expectedRemoteConflictEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + + // 3: Insert -> Conflict -> Exception -> Freeze + // reset + ctx = harness.freshTestContext() + ctx.mockInsertException(duplicateInsertException) + ctx.insertTestDocument() + + // prepare an exceptionToThrow to be thrown, and sync + ctx.exceptionToThrowDuringConflict = Exception("bad") + ctx.doSyncPass() + ctx.waitForError() + + // verify that, though the conflict handler was called, the exceptionToThrow was emitted + // by the dataSynchronizer + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 0) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 1) + ctx.verifyErrorListenerCalledForActiveDoc(times = 1, error = ctx.exceptionToThrowDuringConflict) + + // assert that the local doc is the same. this is frozen now + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + + ctx.exceptionToThrowDuringConflict = null + ctx.shouldConflictBeResolvedByRemote = true + ctx.doSyncPass() + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + + // 4: Unknown -> Delete + ctx = harness.freshTestContext() + ctx.mockInsertException(duplicateInsertException) + ctx.insertTestDocument() + ctx.doSyncPass() + + ctx.queueConsumableRemoteUnknownEvent() + ctx.doSyncPass() + assertNull(ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testFailedInsert() { + val ctx = harness.freshTestContext() + // prepare the exceptionToThrow + val expectedException = StitchServiceException("bad", StitchServiceErrorCode.UNKNOWN) + ctx.mockInsertException(expectedException) + + // insert the document, prepare for an error + ctx.insertTestDocument() + ctx.waitForEvent() + + // sync, verifying that the expected exceptionToThrow was emitted, freezing the document + ctx.doSyncPass() + ctx.waitForError() + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 0) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) + ctx.verifyErrorListenerCalledForActiveDoc(times = 1, error = expectedException) + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + + // prepare a remote delete event, sync, and assert that nothing was affecting + // (since we're frozen) + ctx.queueConsumableRemoteDeleteEvent() + ctx.doSyncPass() + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) } - private val authMonitor = AuthMonitor { true } - private val localClient by lazy { - SyncMongoClientFactory.getClient( - TestUtils.getClientInfo(), - "mongodblocal", - ServerEmbeddedMongoClientFactory.getInstance() + @Test + fun testSuccessfulReplace() { + val ctx = harness.freshTestContext() + val expectedDocument = BsonDocument("_id", ctx.testDocumentId).append("count", BsonInt32(3)) + setupPendingReplace(ctx, expectedDocument) + + ctx.mockUpdateResult(RemoteUpdateResult(1, 1, null)) + + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + ctx.namespace, expectedDocument, false)) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + } + + @Test + fun testConflictedReplace() { + var ctx = harness.freshTestContext() + var expectedDoc = BsonDocument("count", BsonInt32(3)).append("_id", ctx.testDocumentId) + + // 1: Replace -> Conflict -> Replace (local wins) + setupPendingReplace( + ctx, + shouldConflictBeResolvedByRemote = false, + expectedDocument = expectedDoc) + + // do a sync pass, addressing the conflict + ctx.doSyncPass() + ctx.waitForEvent() + // verify that a change event has been emitted. the conflict will have been handled + // in setupPendingReplace + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + ctx.namespace, expectedDoc, false + )) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + + assertEquals(expectedDoc, ctx.findTestDocumentFromLocalCollection()) + + // 2: Replace -> Conflict -> Delete (remote wins) + ctx = harness.freshTestContext() + expectedDoc = BsonDocument("count", BsonInt32(3)).append("_id", ctx.testDocumentId) + setupPendingReplace(ctx, expectedDoc, shouldConflictBeResolvedByRemote = true) + + ctx.verifyConflictHandlerCalledForActiveDoc(times = 1) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + assertNull(ctx.findTestDocumentFromLocalCollection()) + + // 3: Replace -> Conflict -> Exception -> Freeze + ctx = harness.freshTestContext() + expectedDoc = BsonDocument("count", BsonInt32(3)).append("_id", ctx.testDocumentId) + ctx.exceptionToThrowDuringConflict = Exception("bad") + // verify that, though the conflict handler was called, the exceptionToThrow was emitted + // by the dataSynchronizer + setupPendingReplace(ctx, expectedDoc, shouldWaitForError = true) + assertEquals(expectedDoc, ctx.findTestDocumentFromLocalCollection()) + + // clear issues. open a path for a delete. + // do another sync pass. the doc should remain the same as it is frozen + ctx.exceptionToThrowDuringConflict = null + ctx.shouldConflictBeResolvedByRemote = false + ctx.doSyncPass() + assertEquals(expectedDoc, ctx.findTestDocumentFromLocalCollection()) + expectedDoc = BsonDocument("count", BsonInt32(5)).append("_id", ctx.testDocumentId) + + // replace the doc locally (with an update), unfreezing it, and syncing it + setupPendingReplace(ctx, expectedDoc) + ctx.doSyncPass() + assertEquals(expectedDoc, ctx.findTestDocumentFromLocalCollection()) + + // 4: Unknown -> Freeze + ctx = harness.freshTestContext() + expectedDoc = BsonDocument("count", BsonInt32(3)).append("_id", ctx.testDocumentId) + ctx.queueConsumableRemoteUnknownEvent() + setupPendingReplace(ctx, expectedDoc) + + ctx.queueConsumableRemoteUpdateEvent() + ctx.doSyncPass() + assertEquals(expectedDoc, ctx.findTestDocumentFromLocalCollection()) + + // should be frozen since the operation type was unknown + ctx.queueConsumableRemoteUnknownEvent() + ctx.doSyncPass() + assertEquals(expectedDoc, ctx.findTestDocumentFromLocalCollection()) + + ctx.queueConsumableRemoteDeleteEvent() + ctx.doSyncPass() + assertEquals(expectedDoc, ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testSuccessfulUpdate() { + val ctx = harness.freshTestContext() + // setup our expectations + val docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) + + // insert, sync the doc, update, and verify that the change event was emitted + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ctx.updateTestDocument() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + ctx.testDocumentId, + ctx.updateDocument, + docAfterUpdate, + true + )) + + // mock a successful update, sync the update. verify that the update + // was of the correct doc, and that no conflicts or errors occured + ctx.mockUpdateResult(RemoteUpdateResult(1, 1, null)) + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, expectedChangeEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + ctx.testDocumentId, + ctx.updateDocument, + docAfterUpdate, + false + )) + val docCaptor = ArgumentCaptor.forClass(BsonDocument::class.java) + verify(ctx.collectionMock, times(1)).updateOne(any(), docCaptor.capture()) + assertEquals(docAfterUpdate, withoutSyncVersion(docCaptor.value)) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + + // verify the doc update was maintained locally + assertEquals( + docAfterUpdate, + ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testConflictedUpdate() { + var ctx = harness.freshTestContext() + // setup our expectations + var docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) + var expectedLocalEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + ctx.testDocumentId, + ctx.updateDocument, + docAfterUpdate, + true) + + // 1: Update -> Conflict -> Delete (remote wins) + // insert a new document, and sync. + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.doSyncPass() + + // update the document and wait for the local update event + ctx.updateTestDocument() + ctx.waitForEvent() + + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, + expectedChangeEvent = expectedLocalEvent) + + // create conflict here by claiming there is no remote doc to update + ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) + + // do a sync pass, addressing the conflict + ctx.doSyncPass() + ctx.waitForEvent() + // verify that a change event has been emitted, a conflict has been handled, + // and no errors were emitted + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 1) + ctx.verifyErrorListenerCalledForActiveDoc(times = 0) + + // since we've accepted the remote result, this doc will have been deleted + assertNull(ctx.findTestDocumentFromLocalCollection()) + + // 2: Update -> Conflict -> Update (local wins) + // reset (delete, insert, sync) + ctx = harness.freshTestContext() + docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) + expectedLocalEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + ctx.testDocumentId, + ctx.updateDocument, + docAfterUpdate, + true) + var expectedRemoteEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) + + ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, expectedChangeEvent = + ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + + // update the document and wait for the local update event + ctx.updateTestDocument() + ctx.waitForEvent() + + // do a sync pass, addressing the conflict. let local win + ctx.shouldConflictBeResolvedByRemote = false + + ctx.doSyncPass() + ctx.waitForEvent() + + // verify that a change event has been emitted, a conflict has been handled, + // and no errors were emitted + ctx.verifyChangeEventListenerCalledForActiveDoc( + times = 1, + expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, docAfterUpdate, true)) + ctx.verifyConflictHandlerCalledForActiveDoc(1, expectedLocalEvent, expectedRemoteEvent) + ctx.verifyErrorListenerCalledForActiveDoc(0) + + // since we've accepted the local result, this doc will have been updated remotely + // and sync'd locally + assertEquals( + docAfterUpdate, + ctx.findTestDocumentFromLocalCollection()) + + // 3: Update -> Conflict -> Exception -> Freeze + // reset (delete, insert, sync) + ctx = harness.freshTestContext() + docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) + expectedLocalEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + ctx.testDocumentId, + ctx.updateDocument, + docAfterUpdate, + true) + expectedRemoteEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) + + ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) + + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc( + 1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ctx.doSyncPass() + + // update the reset doc + ctx.updateTestDocument() + ctx.waitForEvent() + + // prepare an exceptionToThrow to be thrown, and sync + ctx.exceptionToThrowDuringConflict = Exception("bad") + ctx.doSyncPass() + ctx.waitForError() + + // verify that, though the conflict handler was called, the exceptionToThrow was emitted + // by the dataSynchronizer + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 0) + ctx.verifyConflictHandlerCalledForActiveDoc(1, expectedLocalEvent, expectedRemoteEvent) + ctx.verifyErrorListenerCalledForActiveDoc(1, ctx.exceptionToThrowDuringConflict) + + // assert that this document is still the locally updated doc. this is frozen now + assertEquals(docAfterUpdate, ctx.findTestDocumentFromLocalCollection()) + + // clear issues. open a path for a delete. + // do another sync pass. the doc should remain the same as it is frozen + ctx.exceptionToThrowDuringConflict = null + ctx.shouldConflictBeResolvedByRemote = false + + ctx.doSyncPass() + assertEquals(docAfterUpdate, ctx.findTestDocumentFromLocalCollection()) + + // update the doc locally, unfreezing it, and syncing it + ctx.mockUpdateResult(RemoteUpdateResult(1, 1, null)) + assertEquals(1L, ctx.updateTestDocument().matchedCount) + ctx.doSyncPass() + + // 4: Unknown -> Freeze + ctx = harness.freshTestContext() + ctx.insertTestDocument() + ctx.queueConsumableRemoteUnknownEvent() + ctx.doSyncPass() + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + + // should be frozen since the operation type was unknown + ctx.queueConsumableRemoteUpdateEvent() + ctx.doSyncPass() + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testFailedUpdate() { + val ctx = harness.freshTestContext() + // set up expectations and insert + val docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) + val expectedEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + ctx.testDocument["_id"], + ctx.updateDocument, + docAfterUpdate, + true ) + ctx.insertTestDocument() + ctx.doSyncPass() + ctx.waitForEvent() + + // update the inserted doc, and prepare our exceptionToThrow + ctx.updateTestDocument() + ctx.waitForEvent() + + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, expectedChangeEvent = expectedEvent) + val expectedException = StitchServiceException("bad", StitchServiceErrorCode.UNKNOWN) + ctx.mockUpdateException(expectedException) + + // sync, and verify that we attempted to update with the correct document, + // but the expected exceptionToThrow was called + ctx.doSyncPass() + ctx.waitForError() + val docCaptor = ArgumentCaptor.forClass(BsonDocument::class.java) + verify(ctx.collectionMock, times(1)).updateOne(any(), docCaptor.capture()) + assertEquals(expectedEvent.fullDocument, withoutSyncVersion(docCaptor.value)) + ctx.verifyChangeEventListenerCalledForActiveDoc(times = 0) + ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) + ctx.verifyErrorListenerCalledForActiveDoc(times = 1, error = expectedException) + assertEquals( + docAfterUpdate, + ctx.findTestDocumentFromLocalCollection()) + + // prepare a remote delete event, sync, and assert that nothing was affecting + // (since we're frozen) + ctx.queueConsumableRemoteDeleteEvent() + ctx.doSyncPass() + assertEquals(docAfterUpdate, ctx.findTestDocumentFromLocalCollection()) } - private val instanceKey = "${Random().nextInt()}" - private val service = spy( - CoreStitchServiceClientImpl( - Mockito.mock(StitchAuthRequestClient::class.java), - StitchServiceRoutes("foo"), - BsonUtils.DEFAULT_CODEC_REGISTRY) - ) - - private val remoteClient = spy(CoreRemoteMongoClientImpl( - service, - instanceKey, - localClient, - networkMonitor, - authMonitor - )) - - @Before - fun setup() { - remoteClient.dataSynchronizer.stop() - } - - @Test - @Suppress("UNCHECKED_CAST") - fun testCoreDocumentSynchronizationConfigIsFrozenCheck() { - // create a dataSynchronizer with an injected remote client - val id1 = BsonObjectId() - - val dataSynchronizer = spy(DataSynchronizer( - instanceKey, - mock(CoreStitchServiceClient::class.java), - localClient, - remoteClient, - networkMonitor, - authMonitor + @Test + fun testSuccessfulDelete() { + val ctx = harness.freshTestContext() + + // insert a new document. assert that the correct change events + // have been reflected w/ and w/o pending writes + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) + ctx.doSyncPass() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + + // delete the document and wait + ctx.deleteTestDocument() + ctx.waitForEvent() + + // verify a delete event with pending writes is called + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( + ctx.namespace, + ctx.testDocument["_id"], + true)) + ctx.mockDeleteResult(RemoteDeleteResult(1)) + + // sync. verify the correct doc was deleted and that a change event + // with no pending writes was emitted + ctx.doSyncPass() + ctx.waitForEvent() + val docCaptor = ArgumentCaptor.forClass(BsonDocument::class.java) + verify(ctx.collectionMock, times(1)).deleteOne(docCaptor.capture()) + assertEquals(ctx.testDocument["_id"], docCaptor.value["_id"]) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( + ctx.namespace, + ctx.testDocument["_id"], + false)) + ctx.verifyConflictHandlerCalledForActiveDoc(0) + ctx.verifyErrorListenerCalledForActiveDoc(0) + assertNull(ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testConflictedDelete() { + var ctx = harness.freshTestContext() + + var expectedLocalEvent = ChangeEvent.changeEventForLocalDelete( + ctx.namespace, + ctx.testDocument["_id"], + true + ) + + ctx.insertTestDocument() + ctx.doSyncPass() + ctx.waitForEvent() + + ctx.deleteTestDocument() + ctx.waitForEvent() + + ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedLocalEvent) + + // create conflict here + // 1: Remote wins + `when`(ctx.collectionMock.deleteOne(any())).thenReturn(RemoteDeleteResult(0)) + ctx.queueConsumableRemoteUpdateEvent() + + ctx.doSyncPass() + ctx.waitForEvent() + + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalReplace( + ctx.namespace, + ctx.testDocumentId, + ctx.testDocument, + false )) + ctx.verifyConflictHandlerCalledForActiveDoc(1, expectedLocalEvent, + ChangeEvent.changeEventForLocalUpdate(ctx.namespace, ctx.testDocumentId, ctx.updateDocument, ctx.testDocument, false)) + ctx.verifyErrorListenerCalledForActiveDoc(0) - // insert a new doc. the details of the doc do not matter - val doc1 = BsonDocument("_id", id1) - dataSynchronizer.insertOneAndSync(namespace, doc1) + assertEquals( + ctx.testDocument, + ctx.findTestDocumentFromLocalCollection()) - // set the doc to frozen and reload the configs - dataSynchronizer.getSynchronizedDocuments(namespace).forEach { - it.isFrozen = true - } - dataSynchronizer.reloadConfig() + // 2: Local wins + ctx = harness.freshTestContext() - // spy on the remote client - val remoteMongoDatabase = mock(CoreRemoteMongoDatabaseImpl::class.java) - `when`(remoteClient.getDatabase(namespace.databaseName)).thenReturn(remoteMongoDatabase) + expectedLocalEvent = ChangeEvent.changeEventForLocalDelete( + ctx.namespace, + ctx.testDocument["_id"], + true + ) - val remoteMongoCollection = mock(CoreRemoteMongoCollectionImpl::class.java) - as CoreRemoteMongoCollectionImpl - `when`(remoteMongoDatabase.getCollection(namespace.collectionName, BsonDocument::class.java)) - .thenReturn(remoteMongoCollection) + ctx.insertTestDocument() + ctx.doSyncPass() + ctx.waitForEvent() - // ensure that no remote inserts are made during this sync pass - dataSynchronizer.doSyncPass() + ctx.deleteTestDocument() + ctx.waitForEvent() - verify(remoteMongoCollection, times(0)).insertOne(any()) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedLocalEvent) - // unfreeze the configs and reload - dataSynchronizer.getSynchronizedDocuments(namespace).forEach { - it.isFrozen = false - } - dataSynchronizer.reloadConfig() + // create conflict here + `when`(ctx.collectionMock.deleteOne(any())).thenReturn(RemoteDeleteResult(0)) + ctx.queueConsumableRemoteUpdateEvent() + ctx.shouldConflictBeResolvedByRemote = false + ctx.doSyncPass() + ctx.waitForEvent() - // this time ensure that the remote insert has been called - dataSynchronizer.doSyncPass() + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( + ctx.namespace, + ctx.testDocumentId, + true + )) + ctx.verifyConflictHandlerCalledForActiveDoc(1, + ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, true), + ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, ctx.testDocumentId, ctx.updateDocument, ctx.testDocument, false + )) + ctx.verifyErrorListenerCalledForActiveDoc(0) - verify(remoteMongoCollection, times(1)).insertOne(any()) + assertNull(ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testFailedDelete() { + val ctx = harness.freshTestContext() + + val expectedEvent = ChangeEvent.changeEventForLocalDelete( + ctx.namespace, + ctx.testDocument["_id"], + true + ) + + ctx.insertTestDocument() + ctx.waitForEvent() + + ctx.doSyncPass() + + ctx.deleteTestDocument() + ctx.waitForEvent() + + ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedChangeEvent = expectedEvent) + val expectedException = StitchServiceException("bad", StitchServiceErrorCode.UNKNOWN) + ctx.mockDeleteException(expectedException) + + ctx.doSyncPass() + ctx.waitForError() + // verify we have deleted the correct doc + val docCaptor = ArgumentCaptor.forClass(BsonDocument::class.java) + verify(ctx.collectionMock, times(1)).deleteOne(docCaptor.capture()) + assertEquals( + BsonDocument("_id", ctx.testDocument["_id"]!!.asObjectId()), + withoutSyncVersion(docCaptor.value)) + ctx.verifyChangeEventListenerCalledForActiveDoc(0) + ctx.verifyConflictHandlerCalledForActiveDoc(0) + ctx.verifyErrorListenerCalledForActiveDoc(1, expectedException) + + assertNull(ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testInsertOneAndSync() { + val ctx = harness.freshTestContext() + + ctx.insertTestDocument() + + val expectedEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true) + + ctx.deleteTestDocument() + + ctx.insertTestDocument() + ctx.waitForEvent() + + ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedEvent) + + assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) + } + + @Test + fun testUpdateOneById() { + val ctx = harness.freshTestContext() + val expectedDocumentAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) + // assert this doc does not exist + assertNull(ctx.findTestDocumentFromLocalCollection()) + + // update the non-existent document... + var updateResult = ctx.updateTestDocument() + // ...which should continue to not exist... + assertNull(ctx.findTestDocumentFromLocalCollection()) + // ...and result in an "empty" UpdateResult + assertEquals(0, updateResult.matchedCount) + assertEquals(0, updateResult.modifiedCount) + assertNull(updateResult.upsertedId) + assertTrue(updateResult.wasAcknowledged()) + + // insert the initial document + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(1) + + // do the actual update + updateResult = ctx.updateTestDocument() + ctx.waitForEvent() + + // assert the UpdateResult is non-zero + assertEquals(1, updateResult.matchedCount) + assertEquals(1, updateResult.modifiedCount) + assertNull(updateResult.upsertedId) + assertTrue(updateResult.wasAcknowledged()) + ctx.verifyChangeEventListenerCalledForActiveDoc( + 1, + ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, ctx.testDocumentId, ctx.updateDocument, expectedDocumentAfterUpdate, true)) + // assert that the updated document equals what we've expected + assertEquals(ctx.testDocument["_id"], ctx.findTestDocumentFromLocalCollection()?.get("_id")) + assertEquals(expectedDocumentAfterUpdate, ctx.findTestDocumentFromLocalCollection()!!) + } + + @Test + fun testDeleteOneById() { + val ctx = harness.freshTestContext() + + // 0: Pre-checks + // assert this doc does not exist + assertNull(ctx.findTestDocumentFromLocalCollection()) + + // delete the non-existent document... + var deleteResult = ctx.deleteTestDocument() + // ...which should continue to not exist... + assertNull(ctx.findTestDocumentFromLocalCollection()) + // ...and result in an "empty" DeleteResult + assertEquals(0, deleteResult.deletedCount) + assertTrue(deleteResult.wasAcknowledged()) + + // 1: Insert -> Delete -> Coalescence + // insert the initial document + ctx.insertTestDocument() + ctx.waitForEvent() + ctx.verifyChangeEventListenerCalledForActiveDoc(1) + + // do the actual delete + deleteResult = ctx.deleteTestDocument() + // assert the DeleteResult is non-zero, and that a (new) change event was not + // called (coalescence). verify desync was called + assertEquals(1, deleteResult.deletedCount) + assertTrue(deleteResult.wasAcknowledged()) + verify(ctx.dataSynchronizer).desyncDocumentFromRemote(eq(ctx.namespace), eq(ctx.testDocumentId)) + // assert that the updated document equals what we've expected + assertNull(ctx.findTestDocumentFromLocalCollection()) + + // 2: Insert -> Update -> Delete -> Event Emission + // insert the initial document + ctx.insertTestDocument() + ctx.doSyncPass() + + // do the actual delete + deleteResult = ctx.deleteTestDocument() + ctx.waitForEvent() + + // assert the UpdateResult is non-zero + assertEquals(1, deleteResult.deletedCount) + assertTrue(deleteResult.wasAcknowledged()) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, + ChangeEvent.changeEventForLocalDelete( + ctx.namespace, ctx.testDocumentId, true + )) + // assert that the updated document equals what we've expected + assertNull(ctx.findTestDocumentFromLocalCollection()) } @Test - @Suppress("UNCHECKED_CAST") fun testConfigure() { - // spy a new DataSynchronizer - val dataSynchronizer = spy(DataSynchronizer( - instanceKey, - service, - localClient, - remoteClient, - networkMonitor, - authMonitor - )) + val ctx = harness.freshTestContext(false) + ctx.verifyStartCalled(0) // without a configuration it should not be // configured or running - assertFalse(dataSynchronizer.isRunning) - - // mock the necessary config args - val conflictHandler = mock(ConflictHandler::class.java) as ConflictHandler - val changeEventListener = mock(ChangeEventListener::class.java) as ChangeEventListener - val errorListener = mock(ErrorListener::class.java) - val bsonCodec = BsonDocumentCodec() - - // insert a pseudo doc - dataSynchronizer.insertOneAndSync(namespace, BsonDocument()) - // verify that, though triggerListeningToNamespace was called, - // it was short circuited and never attempted to open the stream - verify(dataSynchronizer, times(1)).triggerListeningToNamespace(any()) - - // configure the dataSynchronizer, - // which should pass down the configuration to the namespace config - // this should also trigger listening to the namespace And attempt to open the stream - dataSynchronizer.configure(namespace, conflictHandler, changeEventListener, errorListener, bsonCodec) - - // verify that the data synchronizer has triggered the namespace, - // has started itself, and has attempted to open the stream for the namespace - verify(dataSynchronizer, times(2)).triggerListeningToNamespace(any()) - verify(dataSynchronizer, times(1)).start() - - // assert that the dataSynchronizer is concretely running - assertTrue(dataSynchronizer.isRunning) - - // configuring again, verifying that the data synchronizer does NOT - // trigger the namespace or start up a second time - dataSynchronizer.configure(namespace, conflictHandler, changeEventListener, errorListener, bsonCodec) - - verify(dataSynchronizer, times(2)).triggerListeningToNamespace(any()) - verify(dataSynchronizer, times(1)).start() - - // assert that nothing has changed about our state - assertTrue(dataSynchronizer.isRunning) + assertFalse(ctx.dataSynchronizer.isRunning) + + // this call will configure the data synchronizer + ctx.insertTestDocument() + + ctx.verifyStartCalled(1) + + ctx.deleteTestDocument() + + ctx.insertTestDocument() + ctx.waitForEvent() + + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert( + ctx.namespace, ctx.testDocument, true)) + assertTrue(ctx.dataSynchronizer.isRunning) } } diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListenerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListenerUnitTests.kt new file mode 100644 index 000000000..e7d1a8143 --- /dev/null +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListenerUnitTests.kt @@ -0,0 +1,100 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal + +import com.mongodb.stitch.core.internal.net.Event +import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory +import org.bson.BsonObjectId +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.`when` +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.util.Collections + +class NamespaceChangeStreamListenerUnitTests { + private val harness = SyncUnitTestHarness() + + @After + fun teardown() { + CoreRemoteClientFactory.close() + ServerEmbeddedMongoClientFactory.getInstance().close() + } + + @Test + fun testOpenStream() { + val ctx = harness.freshTestContext() + val (namespaceChangeStreamListener, nsConfigMock) = harness.createNamespaceChangeStreamListenerWithContext(ctx) + + // assert the stream does not open since we are offline + ctx.isOnline = false + ctx.isLoggedIn = false + assertFalse(namespaceChangeStreamListener.openStream()) + + // assert the stream does not open since we are not logged in + ctx.isOnline = true + assertFalse(namespaceChangeStreamListener.openStream()) + + // assert the stream does not open since we have no document ids + ctx.isLoggedIn = true + assertFalse(namespaceChangeStreamListener.openStream()) + verify(nsConfigMock, times(1)).synchronizedDocumentIds + + // assert and verify that our stream has opened, and that the streamFunction + // method has been called with the appropriate arguments. verify that we have + // set the nsConfig to stale + `when`(nsConfigMock.synchronizedDocumentIds).thenReturn(setOf(BsonObjectId())) + assertTrue(namespaceChangeStreamListener.openStream()) + val expectedArgs = Collections.singletonList(mapOf( + "database" to ctx.namespace.databaseName, + "collection" to ctx.namespace.collectionName, + "ids" to nsConfigMock.synchronizedDocumentIds + )) + ctx.verifyWatchFunctionCalled(times = 1, expectedArgs = expectedArgs) + verify(nsConfigMock).setStale(eq(true)) + } + + @Test + fun testStoreEvent() { + val ctx = harness.freshTestContext() + val (namespaceChangeStreamListener, nsConfigMock) = harness.createNamespaceChangeStreamListenerWithContext(ctx) + // assert nothing happens when we try to store events on a closed stream + assertFalse(namespaceChangeStreamListener.isOpen) + namespaceChangeStreamListener.storeNextEvent() + + // open the stream. assert that, with an injected error Event, the stream closes + `when`(nsConfigMock.synchronizedDocumentIds).thenReturn(setOf(BsonObjectId())) + assertTrue(namespaceChangeStreamListener.openStream()) + ctx.nextStreamEvent = Event.Builder().withEventName("error").withData( + """{"error": "bad", "error_code": "Unknown"}""" + ).build() + namespaceChangeStreamListener.storeNextEvent() + assertFalse(namespaceChangeStreamListener.isOpen) + + // re-open the stream. assert that, with an injected null message Event, + // the stream does not close, but nothing else should occur + assertTrue(namespaceChangeStreamListener.openStream()) + ctx.nextStreamEvent = Event.Builder().withEventName("message").build() + namespaceChangeStreamListener.storeNextEvent() + assertEquals(0, namespaceChangeStreamListener.events.size) + assertTrue(namespaceChangeStreamListener.isOpen) + + // assert that, with an expected ChangeEvent, the event is stored + // and the stream remains open + val expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true) + ctx.nextStreamEvent = Event.Builder().withEventName("message").withData( + ChangeEvent.toBsonDocument(expectedChangeEvent).toJson() + ).build() + namespaceChangeStreamListener.storeNextEvent() + assertTrue(namespaceChangeStreamListener.isOpen) + + // assert that the consumed event equals the expected event. + // assert that the events have been drained from the event map + val actualEvents = namespaceChangeStreamListener.events + assertEquals(1, actualEvents.size) + SyncUnitTestHarness.compareEvents(expectedChangeEvent, actualEvents.values.first()) + assertEquals(0, namespaceChangeStreamListener.events.size) + } +} diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfigUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfigUnitTests.kt new file mode 100644 index 000000000..92c668bcb --- /dev/null +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfigUnitTests.kt @@ -0,0 +1,39 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal + +import com.mongodb.client.FindIterable +import com.mongodb.client.MongoCollection +import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncUnitTestHarness.Companion.newNamespace +import org.bson.BsonDocument +import org.bson.BsonInt32 +import org.bson.BsonString +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock + +class NamespaceSynchronizationConfigUnitTests { + @Test + fun testToBsonDocumentRoundTrip() { + val namespace = newNamespace() + + val docsColl = mock(MongoCollection::class.java) as MongoCollection + val findIterable = mock(FindIterable::class.java) as FindIterable + + `when`(docsColl.find(any(BsonDocument::class.java))).thenReturn(findIterable) + + val nsConfig = NamespaceSynchronizationConfig( + mock(MongoCollection::class.java) as MongoCollection, + docsColl, + namespace) + + val configBsonDocument = nsConfig.toBsonDocument() + + assertEquals(BsonString(namespace.toString()), configBsonDocument["namespace"]) + assertEquals(BsonInt32(1), configBsonDocument["schema_version"]) + + val roundTrippedNsConfig = NamespaceSynchronizationConfig.fromBsonDocument(configBsonDocument) + + assertEquals(nsConfig.namespace, roundTrippedNsConfig.namespace) + } +} diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt new file mode 100644 index 000000000..ce8fb0e03 --- /dev/null +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt @@ -0,0 +1,569 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal + +import com.mongodb.MongoNamespace +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.UpdateResult +import com.mongodb.stitch.core.StitchAppClientInfo +import com.mongodb.stitch.core.internal.common.AuthMonitor +import com.mongodb.stitch.core.internal.net.Event +import com.mongodb.stitch.core.internal.net.EventStream +import com.mongodb.stitch.core.internal.net.NetworkMonitor +import com.mongodb.stitch.core.internal.net.Stream +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClientImpl +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteFindIterable +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoClientImpl +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollectionImpl +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoDatabaseImpl +import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSync +import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory +import org.bson.BsonDocument +import org.bson.BsonInt32 +import org.bson.BsonObjectId +import org.bson.BsonString +import org.bson.BsonValue +import org.bson.codecs.BsonDocumentCodec +import org.bson.codecs.configuration.CodecRegistries +import org.bson.types.ObjectId +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import java.lang.Exception +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.Random + +class SyncUnitTestHarness { + companion object { + /** + * Conflict handler used for testing purposes. + * + * @param shouldConflictBeResolvedByRemote whether or not to resolve using the remote document or the local + * document + * @param exceptionToThrow if set, will throw an exceptionToThrow after comparing the events + */ + open class TestConflictHandler( + var shouldConflictBeResolvedByRemote: Boolean, + var exceptionToThrow: Exception? = null + ) : ConflictHandler { + override fun resolveConflict( + documentId: BsonValue?, + localEvent: ChangeEvent?, + remoteEvent: ChangeEvent? + ): BsonDocument? { + if (exceptionToThrow != null) { + throw exceptionToThrow!! + } + return if (shouldConflictBeResolvedByRemote) remoteEvent?.fullDocument else localEvent?.fullDocument + } + } + + /** + * Network monitor used for testing purposes. + * Can be switched online or offline via the syncHarness. + */ + open class TestNetworkMonitor : NetworkMonitor { + private val networkStateListeners = mutableListOf() + var isOnline: Boolean = true + set(value) { + if (field != value) { + field = value + networkStateListeners.forEach { it.onNetworkStateChanged() } + } + } + + override fun removeNetworkStateListener(listener: NetworkMonitor.StateListener) { + networkStateListeners.remove(listener) + } + + override fun isConnected(): Boolean { + return isOnline + } + + override fun addNetworkStateListener(listener: NetworkMonitor.StateListener) { + networkStateListeners.add(listener) + } + } + + /** + * Auth monitor used for testing purposes. + * Can be logged on or off via the syncHarness. + */ + open class TestAuthMonitor : AuthMonitor { + var isAuthed = true + override fun isLoggedIn(): Boolean { + return isAuthed + } + } + + /** + * Test event stream that can passed on injected events. + */ + private class TestEventStream(private val testContext: DataSynchronizerTestContext) : EventStream { + override fun nextEvent(): Event { + return testContext.nextStreamEvent + } + + override fun isOpen(): Boolean { + return true + } + + override fun close() { + } + + override fun cancel() { + } + } + + private open class TestChangeEventListener( + private val expectedEvent: ChangeEvent?, + private val emitEventSemaphore: Semaphore? + ) : ChangeEventListener { + override fun onEvent(documentId: BsonValue?, actualEvent: ChangeEvent?) { + try { + if (expectedEvent != null) { + compareEvents(expectedEvent, actualEvent!!) + Assert.assertEquals(expectedEvent.id, documentId) + } + } finally { + emitEventSemaphore?.release() + } + } + } + + fun newDoc(key: String = "hello", value: BsonValue = BsonString("world")): BsonDocument { + return BsonDocument("_id", BsonObjectId()).append(key, value) + } + + fun newNamespace(): MongoNamespace { + return MongoNamespace( + BsonObjectId().value.toHexString(), + BsonObjectId().value.toHexString()) + } + + fun withoutSyncVersion(document: BsonDocument?): BsonDocument? { + if (document == null) { + return null + } + val newDoc = BsonDocument.parse(document.toJson()) + newDoc.remove("__stitch_sync_version") + return newDoc + } + + /** + * Compare the properties of given events + * + * @param expectedEvent event we are expecting to see + * @Param actualEvent actual event generated + */ + fun compareEvents(expectedEvent: ChangeEvent, actualEvent: ChangeEvent) { + // assert that our actualEvent is correct + Assert.assertEquals(expectedEvent.operationType, actualEvent.operationType) + Assert.assertEquals(expectedEvent.documentKey, actualEvent.documentKey) + + if (actualEvent.fullDocument == null) { + Assert.assertNull(expectedEvent.fullDocument) + } else if (expectedEvent.fullDocument == null) { + Assert.assertNull(actualEvent.fullDocument) + } else { + Assert.assertEquals(expectedEvent.fullDocument, withoutSyncVersion(actualEvent.fullDocument)) + } + Assert.assertEquals(expectedEvent.id, actualEvent.id) + Assert.assertEquals(expectedEvent.namespace, actualEvent.namespace) + Assert.assertEquals(expectedEvent.updateDescription.removedFields, actualEvent.updateDescription.removedFields) + Assert.assertEquals(expectedEvent.updateDescription.updatedFields, actualEvent.updateDescription.updatedFields) + + Assert.assertEquals(expectedEvent.hasUncommittedWrites(), actualEvent.hasUncommittedWrites()) + } + + private fun newErrorListener( + emitErrorSemaphore: Semaphore? = null, + expectedDocumentId: BsonValue? = null + ): ErrorListener { + open class TestErrorListener : ErrorListener { + override fun onError(actualDocumentId: BsonValue?, error: Exception?) { + if (expectedDocumentId != null) { + Assert.assertEquals(expectedDocumentId, actualDocumentId) + } + + emitErrorSemaphore?.release() + } + } + return Mockito.spy(TestErrorListener()) + } + + private fun newConflictHandler( + shouldConflictBeResolvedByRemote: Boolean = true, + exceptionToThrow: Exception? = null + ): TestConflictHandler { + return Mockito.spy( + TestConflictHandler( + shouldConflictBeResolvedByRemote = shouldConflictBeResolvedByRemote, + exceptionToThrow = exceptionToThrow)) + } + + private fun newChangeEventListener( + emitEventSemaphore: Semaphore? = null, + expectedEvent: ChangeEvent? = null + ): ChangeEventListener { + return Mockito.spy(TestChangeEventListener(expectedEvent, emitEventSemaphore)) + } + } + + @Suppress("UNCHECKED_CAST") + private class DataSynchronizerTestContextImpl(shouldPreconfigure: Boolean = true) : DataSynchronizerTestContext { + override val collectionMock: CoreRemoteMongoCollectionImpl = + Mockito.mock(CoreRemoteMongoCollectionImpl::class.java) as CoreRemoteMongoCollectionImpl + + override var nextStreamEvent: Event = Event.Builder().withEventName("MOCK").build() + private val streamMock = Stream(TestEventStream(this), ChangeEvent.changeEventCoder) + override val testDocument = newDoc("count", BsonInt32(1)) + override val testDocumentId: BsonObjectId by lazy { testDocument["_id"] as BsonObjectId } + override var updateDocument: BsonDocument = BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))) + private val bsonDocumentCodec = BsonDocumentCodec() + + override var isOnline = true + set(value) { + this.networkMonitor.isOnline = value + field = value + } + override var isLoggedIn = true + set(value) { + this.authMonitor.isAuthed = value + field = value + } + override var shouldConflictBeResolvedByRemote: Boolean = true + set(value) { + this.conflictHandler.shouldConflictBeResolvedByRemote = value + field = value + } + override var exceptionToThrowDuringConflict: Exception? = null + set(value) { + this.conflictHandler.exceptionToThrow = value + field = value + } + + var changeEventListener = newChangeEventListener() + private set + var conflictHandler = newConflictHandler() + private set + var errorListener = newErrorListener() + private set + + override val namespace = newNamespace() + val networkMonitor: TestNetworkMonitor = spy(TestNetworkMonitor()) + val authMonitor: TestAuthMonitor = spy(TestAuthMonitor()) + + private val localClient by lazy { + val clientKey = ObjectId().toHexString() + SyncMongoClientFactory.getClient( + StitchAppClientInfo( + clientKey, + String.format("%s/%s", System.getProperty("java.io.tmpdir"), clientKey), + ObjectId().toHexString(), + ObjectId().toHexString(), + CodecRegistries.fromCodecs(bsonDocumentCodec), + networkMonitor, + authMonitor + ), + "local", + ServerEmbeddedMongoClientFactory.getInstance() + ) + } + + val service: CoreStitchServiceClient by lazy { + val service = Mockito.mock(CoreStitchServiceClientImpl::class.java) + `when`(service.codecRegistry).thenReturn(CodecRegistries.fromCodecs(BsonDocumentCodec())) + service + } + private val remoteClient = Mockito.mock(CoreRemoteMongoClientImpl::class.java) + private val instanceKey = "${Random().nextInt()}" + + override val dataSynchronizer: DataSynchronizer = + Mockito.spy(DataSynchronizer( + instanceKey, + service, + localClient, + remoteClient, + networkMonitor, + authMonitor + )) + + private var eventSemaphore: Semaphore? = null + private var errorSemaphore: Semaphore? = null + + init { + if (shouldPreconfigure) { + // this needs to be done since the spied dataSynchronizer does not + // re-add itself to the network monitor + networkMonitor.addNetworkStateListener(dataSynchronizer) + + dataSynchronizer.disableSyncThread() + + dataSynchronizer.stop() + + Mockito.`when`(service.streamFunction( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyList(), + ArgumentMatchers.eq(ChangeEvent.changeEventCoder)) + ).thenReturn(streamMock) + + val databaseSpy = Mockito.mock(CoreRemoteMongoDatabaseImpl::class.java) + Mockito.`when`(remoteClient.getDatabase(ArgumentMatchers.eq(namespace.databaseName))).thenReturn(databaseSpy) + Mockito.`when`( + databaseSpy.getCollection(ArgumentMatchers.eq(namespace.collectionName), + ArgumentMatchers.eq(BsonDocument::class.java))).thenReturn(collectionMock) + + Mockito.`when`(collectionMock.namespace).thenReturn(namespace) + val remoteFindIterable = Mockito.mock(CoreRemoteFindIterable::class.java) as CoreRemoteFindIterable + Mockito.`when`(collectionMock.find(ArgumentMatchers.any())).thenReturn(remoteFindIterable) + Mockito.`when`(remoteFindIterable.into>(ArgumentMatchers.any())).thenReturn(HashSet()) + + Mockito.verifyZeroInteractions(collectionMock) + } + } + + /** + * Reconfigure the internal dataSynchronizer with + * the current conflictHandler, changeEventListener, and + * errorListener. + */ + override fun reconfigure() { + dataSynchronizer.configure( + namespace, + conflictHandler, + changeEventListener, + errorListener, + bsonDocumentCodec) + } + + override fun waitForEvent() { + assertTrue(eventSemaphore?.tryAcquire(10, TimeUnit.SECONDS) ?: true) + } + + override fun waitForError() { + assertTrue(errorSemaphore?.tryAcquire(10, TimeUnit.SECONDS) ?: true) + } + + /** + * Insert the current test document. + */ + override fun insertTestDocument() { + configureNewChangeEventListener() + configureNewErrorListener() + configureNewConflictHandler() + + dataSynchronizer.insertOneAndSync(namespace, testDocument) + } + + override fun updateTestDocument(): UpdateResult { + configureNewChangeEventListener() + configureNewErrorListener() + configureNewConflictHandler() + + return dataSynchronizer.updateOneById(namespace, testDocumentId, updateDocument) + } + + override fun deleteTestDocument(): DeleteResult { + configureNewChangeEventListener() + configureNewErrorListener() + configureNewConflictHandler() + + return dataSynchronizer.deleteOneById(namespace, testDocumentId) + } + + override fun doSyncPass() { + configureNewChangeEventListener() + configureNewErrorListener() + configureNewConflictHandler() + + dataSynchronizer.doSyncPass() + } + + override fun queueConsumableRemoteInsertEvent() { + `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( + mapOf(testDocument to ChangeEvent.changeEventForLocalInsert(namespace, testDocument, true)), + mapOf()) + } + + override fun queueConsumableRemoteUpdateEvent() { + `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( + mapOf(testDocument to ChangeEvent.changeEventForLocalUpdate(namespace, testDocumentId, updateDocument, testDocument, false)), + mapOf()) + } + + override fun queueConsumableRemoteDeleteEvent() { + `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( + mapOf(testDocument to ChangeEvent.changeEventForLocalDelete(namespace, testDocumentId, true)), + mapOf()) + } + + override fun queueConsumableRemoteUnknownEvent() { + `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( + mapOf(testDocument to ChangeEvent( + BsonDocument("_id", testDocumentId), + ChangeEvent.OperationType.UNKNOWN, + testDocument, + namespace, + BsonDocument("_id", testDocumentId), + null, + true)), mapOf()) + } + + override fun findTestDocumentFromLocalCollection(): BsonDocument? { + // TODO: this may be rendered unnecessary with STITCH-1972 + return withoutSyncVersion( + dataSynchronizer.findOneById( + namespace, + testDocumentId, + BsonDocument::class.java, + CodecRegistries.fromCodecs(bsonDocumentCodec))) + } + + override fun verifyChangeEventListenerCalledForActiveDoc(times: Int, expectedChangeEvent: ChangeEvent?) { + val changeEventArgumentCaptor = ArgumentCaptor.forClass(ChangeEvent::class.java) + Mockito.verify(changeEventListener, times(times)).onEvent( + eq(testDocumentId), + changeEventArgumentCaptor.capture() as ChangeEvent?) + + if (expectedChangeEvent != null) { + compareEvents(expectedChangeEvent, changeEventArgumentCaptor.value as ChangeEvent) + } + } + + override fun verifyErrorListenerCalledForActiveDoc(times: Int, error: Exception?) { + Mockito.verify(errorListener, times(times)).onError(eq(testDocumentId), eq(error)) + } + + override fun verifyConflictHandlerCalledForActiveDoc( + times: Int, + expectedLocalConflictEvent: ChangeEvent?, + expectedRemoteConflictEvent: ChangeEvent? + ) { + val localChangeEventArgumentCaptor = ArgumentCaptor.forClass(ChangeEvent::class.java) + val remoteChangeEventArgumentCaptor = ArgumentCaptor.forClass(ChangeEvent::class.java) + + Mockito.verify(conflictHandler, times(times)).resolveConflict( + eq(testDocumentId), + localChangeEventArgumentCaptor.capture() as ChangeEvent?, + remoteChangeEventArgumentCaptor.capture() as ChangeEvent?) + + if (expectedLocalConflictEvent != null) { + compareEvents(expectedLocalConflictEvent, localChangeEventArgumentCaptor.value as ChangeEvent) + } + + if (expectedRemoteConflictEvent != null) { + compareEvents(expectedRemoteConflictEvent, remoteChangeEventArgumentCaptor.value as ChangeEvent) + } + } + + override fun verifyWatchFunctionCalled(times: Int, expectedArgs: List) { + Mockito.verify(service, times(times)).streamFunction(eq("watch"), eq(expectedArgs), eq(ChangeEvent.changeEventCoder)) + } + + override fun verifyStartCalled(times: Int) { + Mockito.verify(dataSynchronizer, times(times)).start() + } + + override fun verifyStopCalled(times: Int) { + Mockito.verify(dataSynchronizer, times(times)).stop() + } + + override fun mockInsertException(exception: Exception) { + `when`(collectionMock.insertOne(any())).thenThrow(exception) + } + + override fun mockUpdateResult(remoteUpdateResult: RemoteUpdateResult) { + `when`(collectionMock.updateOne(any(), any())).thenReturn(remoteUpdateResult) + } + + override fun mockUpdateException(exception: Exception) { + `when`(collectionMock.updateOne(any(), any())).thenAnswer { + throw exception + } + } + + override fun mockDeleteResult(remoteDeleteResult: RemoteDeleteResult) { + `when`(collectionMock.deleteOne(any())).thenReturn(remoteDeleteResult) + } + + override fun mockDeleteException(exception: Exception) { + `when`(collectionMock.deleteOne(any())).thenAnswer { + throw exception + } + } + + private fun configureNewErrorListener() { + val emitErrorSemaphore = Semaphore(0) + this.errorSemaphore?.release() + this.errorListener = newErrorListener(emitErrorSemaphore) + this.reconfigure() + this.errorSemaphore = emitErrorSemaphore + } + + private fun configureNewChangeEventListener(expectedChangeEvent: ChangeEvent? = null) { + val emitEventSemaphore = Semaphore(0) + this.eventSemaphore?.release() + this.changeEventListener = newChangeEventListener(emitEventSemaphore, expectedChangeEvent) + this.reconfigure() + this.eventSemaphore = emitEventSemaphore + } + + private fun configureNewConflictHandler() { + this.conflictHandler = newConflictHandler(shouldConflictBeResolvedByRemote, exceptionToThrowDuringConflict) + this.reconfigure() + } + } + + private var latestCtx: DataSynchronizerTestContext? = null + + internal fun teardown() { + latestCtx?.dataSynchronizer?.close() + } + + internal fun freshTestContext(shouldPreconfigure: Boolean = true): DataSynchronizerTestContext { + latestCtx?.dataSynchronizer?.close() + latestCtx = DataSynchronizerTestContextImpl(shouldPreconfigure) + return latestCtx!! + } + + internal fun createNamespaceChangeStreamListenerWithContext(context: DataSynchronizerTestContext): Pair { + val nsConfigMock = Mockito.mock(NamespaceSynchronizationConfig::class.java) + val namespaceChangeStreamListener = NamespaceChangeStreamListener( + context.namespace, + nsConfigMock, + (context as DataSynchronizerTestContextImpl).service, + context.networkMonitor, + context.authMonitor) + + return namespaceChangeStreamListener to nsConfigMock + } + + internal fun createCoreSyncWithContext(context: DataSynchronizerTestContext): Pair, SyncOperations> { + val syncOperations = Mockito.spy(SyncOperations( + context.namespace, + BsonDocument::class.java, + context.dataSynchronizer, + CodecRegistries.fromCodecs(BsonDocumentCodec()))) + val coreSync = CoreSyncImpl( + context.namespace, + BsonDocument::class.java, + context.dataSynchronizer, + (context as DataSynchronizerTestContextImpl).service, + syncOperations) + + return coreSync to syncOperations + } +} diff --git a/core/testutils/build.gradle b/core/testutils/build.gradle index 3bb3e00f5..26b0ad115 100644 --- a/core/testutils/build.gradle +++ b/core/testutils/build.gradle @@ -9,6 +9,7 @@ buildscript { dependencies { implementation project(':core:stitch-core-sdk') + implementation project(':core:core-services:stitch-core-services-mongodb-remote') compile project(path: ':core:stitch-core-admin-client') api 'org.mongodb:bson:3.7.0' implementation 'junit:junit:4.12' diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/BaseStitchIntTest.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/BaseStitchIntTest.kt index 24469a46a..265f1ce43 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/BaseStitchIntTest.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/BaseStitchIntTest.kt @@ -22,6 +22,7 @@ import com.mongodb.stitch.core.admin.services.rules.RuleResponse import com.mongodb.stitch.core.admin.services.service import com.mongodb.stitch.core.auth.providers.userapikey.UserApiKeyAuthProvider import com.mongodb.stitch.core.auth.providers.userpassword.UserPasswordCredential +import com.mongodb.stitch.core.internal.net.NetworkMonitor import okhttp3.OkHttpClient import okhttp3.Request import org.bson.types.ObjectId @@ -29,8 +30,32 @@ import org.junit.After import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before +import java.util.concurrent.CopyOnWriteArrayList abstract class BaseStitchIntTest { + class TestNetworkMonitor : NetworkMonitor { + private var _connectedState = false + var connectedState: Boolean + set(value) { + _connectedState = value + listeners.forEach { it.onNetworkStateChanged() } + } + get() = _connectedState + + private var listeners = CopyOnWriteArrayList() + + override fun isConnected(): Boolean { + return connectedState + } + + override fun addNetworkStateListener(listener: NetworkMonitor.StateListener) { + listeners.add(listener) + } + + override fun removeNetworkStateListener(listener: NetworkMonitor.StateListener) { + listeners.remove(listener) + } + } private val adminClient: StitchAdminClient by lazy { StitchAdminClient.create(getStitchBaseURL()) diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxyRemoteMethods.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxyRemoteMethods.kt new file mode 100644 index 000000000..7fc759b74 --- /dev/null +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxyRemoteMethods.kt @@ -0,0 +1,58 @@ +package com.mongodb.stitch.core.testutils.sync + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import org.bson.Document +import org.bson.conversions.Bson + +/** + * A set of platform independent methods related to + * [com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollection]. + */ +interface ProxyRemoteMethods { + /** + * Inserts the provided document. If the document is missing an identifier, the client should + * generate one. + * + * @param document the document to insert + * @return the result of the insert one operation + */ + fun insertOne(document: Document): RemoteInsertOneResult + + /** + * Inserts one or more documents. + * + * @param documents the documents to insert + * @return the result of the insert many operation + */ + fun insertMany(documents: List): RemoteInsertManyResult + + /** + * Finds all documents in the collection. + * + * @param filter the query filter + * @return an iterable interface containing the documents + */ + fun find(filter: Document): Iterable + /** + * Update a single document in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param updateDocument a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update one operation + */ + fun updateOne(filter: Document, updateDocument: Document): RemoteUpdateResult + + /** + * Removes at most one document from the collection that matches the given filter. If no + * documents match, the collection is not + * modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove one operation + */ + fun deleteOne(filter: Bson): RemoteDeleteResult +} diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt new file mode 100644 index 000000000..46ea1190d --- /dev/null +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt @@ -0,0 +1,95 @@ +package com.mongodb.stitch.core.testutils.sync + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler +import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import org.bson.BsonValue +import org.bson.Document +import org.bson.conversions.Bson + +/** + * A set of platform independent methods related to + * [com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSync]. + */ +interface ProxySyncMethods { + /** + * Set the conflict handler and and change event listener on this collection. + * @param conflictResolver the conflict resolver to invoke when a conflict happens between local + * and remote events. + * @param changeEventListener the event listener to invoke when a change event happens for the + * document. + * @param errorListener the error listener to invoke when an irrecoverable error occurs + */ + fun configure( + conflictResolver: ConflictHandler, + changeEventListener: ChangeEventListener?, + errorListener: ErrorListener? + ) + + /** + * Requests that the given document _id be synchronized. + * @param id the document _id to synchronize. + */ + fun syncOne(id: BsonValue) + + /** + * Stops synchronizing the given document _id. Any uncommitted writes will be lost. + * + * @param id the _id of the document to desynchronize. + */ + fun desyncOne(id: BsonValue) + + /** + * Returns the set of synchronized document ids in a namespace. + * + * @return the set of synchronized document ids in a namespace. + */ + fun getSyncedIds(): Set + + /** + * Finds all documents in the collection. + * + * @param filter the query filter + * @return the find iterable interface + */ + fun find(filter: Bson): Iterable + + /** + * Finds a single document by the given id. It is first searched for in the local synchronized + * cache and if not found and there is internet connectivity, it is searched for remotely. + * + * @param id the _id of the document to search for. + * @return the document if found locally or remotely. + */ + fun findOneById(id: BsonValue): Document? + + /** + * Updates a document by the given id. It is first searched for in the local synchronized cache + * and if not found and there is internet connectivity, it is searched for remotely. + * + * @param documentId the _id of the document to search for. + * @param update the update specifier. + * @return the result of the local or remote update. + */ + fun updateOneById(documentId: BsonValue, update: Bson): RemoteUpdateResult + + /** + * Inserts a single document and begins to synchronize it. + * + * @param document the document to insert and synchronize. + * @return the result of the insertion. + */ + fun insertOneAndSync(document: Document): RemoteInsertOneResult + + /** + * Deletes a single document by the given id. It is first searched for in the local synchronized + * cache and if not found and there is internet connectivity, it is searched for remotely. + * + * @param documentId the _id of the document to search for. + * @return the result of the local or remote update. + */ + fun deleteOneById(documentId: BsonValue): RemoteDeleteResult +} 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 new file mode 100644 index 000000000..08ba4d7dd --- /dev/null +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt @@ -0,0 +1,1359 @@ +package com.mongodb.stitch.core.testutils.sync + +import com.mongodb.MongoNamespace +import com.mongodb.stitch.core.internal.common.Callback +import com.mongodb.stitch.core.internal.common.OperationResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler +import com.mongodb.stitch.core.services.mongodb.remote.sync.DefaultSyncConflictResolvers +import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent +import org.bson.BsonDocument +import org.bson.BsonObjectId +import org.bson.BsonValue +import org.bson.Document +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Ignore +import org.junit.Test +import java.lang.Exception +import java.util.UUID +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +/** + * Test harness for Sync integration tests. + * + * Each @Test method in this interface reflects a test + * that must be implemented to properly test Sync. + * + * The tests should be proxied from a [SyncIntTestRunner] implementor. + * [SyncIntTestProxy] and [SyncIntTestRunner] should be in sync + * on the these test methods. + * + * @param syncTestRunner a runner that contains the necessary properties + * to run the tests + */ +@Ignore +class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { + @Test + fun testSync() { + testSyncInBothDirections { + val remoteMethods = syncTestRunner.remoteMethods() + val remoteOperations = 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 + remoteOperations.configure(ConflictHandler { id: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> + 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 + remoteOperations.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, remoteOperations.findOneById(doc1Id)) + // 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, remoteOperations.findOneById(doc1Id)) + + // 2. insertOneAndSync should work offline and then sync the document when online. + goOffline() + val doc3 = Document("so", "syncy") + val insResult = remoteOperations.insertOneAndSync(doc3) + Assert.assertEquals(doc3, withoutSyncVersion(remoteOperations.findOneById(insResult.insertedId)!!)) + 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 = remoteOperations.updateOneById( + doc1Id, + doc1Update) + Assert.assertEquals(1, result3.matchedCount) + expectedDocument["foo"] = 2 + Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteOperations.findOneById(doc1Id)!!)) + // 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, withoutSyncVersion(remoteOperations.findOneById(doc1Id)!!)) + // second pass will update with the ack'd version id + streamAndSync() + Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteOperations.findOneById(doc1Id)!!)) + Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) + } + } + + @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 + } + 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.updateOneById(doc1Id, localUpdate) + assertEquals(1, result.matchedCount) + val expectedLocalDocument = Document(doc) + expectedLocalDocument["local"] = "updateWow" + assertEquals(expectedLocalDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // second pass will update with the ack'd version id + streamAndSync() + assertEquals(expectedLocalDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedLocalDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + } + } + + @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.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // sync the collection. the remote document should be accepted + // and this resolution should be reflected locally and remotely + streamAndSync() + expectedDocument["foo"] = 3 + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + } + } + + @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.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // sync the collection. the local document should be accepted + // and this resolution should be reflected locally and remotely + streamAndSync() + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + } + } + + @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 + val 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() + + // go offline to avoid processing events. + // delete the document locally + goOffline() + val result = coll.deleteOneById(doc1Id) + 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.findOneById(doc1Id)) + + // 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.findOneById(doc1Id)) + } + } + + @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.deleteOneById(doc1Id) + 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.findOneById(doc1Id)) + + // 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + } + } + + @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.insertOneAndSync(docToInsert) + + // find the local document we just inserted + val doc = coll.findOneById(insertResult.insertedId)!! + 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.updateOneById(doc1Id, doc1Update).matchedCount) + + // assert that nothing has been inserting remotely + val expectedDocument = withoutSyncVersion(Document(doc)) + expectedDocument["foo"] = 1 + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + } + } + + @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.insertOneAndSync(docToInsert) + + // find the document we just inserted + val doc = coll.findOneById(insertResult.insertedId)!! + 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 = withoutSyncVersion(Document(doc)) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // update the document locally + val doc1Update = Document("\$inc", Document("foo", 1)) + assertEquals(1, coll.updateOneById(doc1Id, 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // sync. assert that our update is reflected locally and remotely + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + } + } + + @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.insertOneAndSync(docToInsert) + streamAndSync() + + // assert the sync'd document is found locally and remotely + val doc = coll.findOneById(insertResult.insertedId)!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + val expectedDocument = withoutSyncVersion(Document(doc)) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // delete the doc locally, then re-insert it. + // assert the document is still the same locally and remotely + assertEquals(1, coll.deleteOneById(doc1Id).deletedCount) + coll.insertOneAndSync(doc) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // update the document locally + val doc1Update = Document("\$inc", Document("foo", 1)) + assertEquals(1, coll.updateOneById(doc1Id, 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // sync. assert that the update has been reflected remotely and locally + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + } + } + + @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.findOneById(doc1Id)) + + // 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.findOneById(doc1Id)) + } + } + + @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.findOneById(doc1Id)) + Assert.assertNotNull(coll.findOneById(doc1Id)) + + // go offline. + // delete the document remotely. + // update the document locally. + goOffline() + remoteColl.deleteOne(doc1Filter) + assertEquals(1, coll.updateOneById(doc1Id, 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.findOneById(doc1Id)) + + // sync again. assert that the resolution is reflected locally and remotely + streamAndSync() + Assert.assertNotNull(remoteColl.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.findOneById(doc1Id)) + } + } + + @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.findOneById(doc1Id)) + Assert.assertNotNull(coll.findOneById(doc1Id)) + + // 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.updateOneById(doc1Id, 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // do another sync pass. assert that the local and remote docs are in sync + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + } + } + + @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(doc, coll.findOneById(doc1Id)) + + // update the document locally. sync. + assertEquals(1, coll.updateOneById(doc1Id, 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + } + } + + @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(doc, coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.findOneById(doc1Id)) + + // 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.updateOneById(doc1Id, 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.findOneById(doc1Id)) + + // go online and sync. the deletion should be reflected remotely and locally now + goOnline() + streamAndSync() + Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) + Assert.assertNull(coll.findOneById(doc1Id)) + } + } + + @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) + 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)))) + 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.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) + assertEquals(1, result.matchedCount) + expectedDocument["foo"] = 2 + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + powerCycleDevice() + coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + + // sync. assert that the update was reflected remotely + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + } + } + + @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.insertOneAndSync(docToInsert).insertedId + + // assert the document exists locally. desync it. + assertEquals(docToInsert, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + coll.desyncOne(doc1Id) + + // sync. assert that the desync'd document no longer exists locally + streamAndSync() + Assert.assertNull(coll.findOneById(doc1Id)) + } + } + + @Test + fun testInsertInsertConflict() { + testSyncInBothDirections { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a new document remotely + val docToInsert = Document("_id", "hello") + 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.insertOneAndSync(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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(docToInsert, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + + // sync again. assert that the resolution is reflected + // locally and remotely. + streamAndSync() + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) + } + } + + @Test + fun testFrozenDocumentConfig() { + 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 { _, _ -> + }) + + // insert an initial doc + val testDoc = Document("hello", "world") + val result = testSync.insertOneAndSync(testDoc) + + // do a sync pass, synchronizing the doc + streamAndSync() + + Assert.assertNotNull(remoteColl.find(Document("_id", testDoc.get("_id"))).first()) + + // update the doc + val expectedDoc = Document("hello", "computer") + testSync.updateOneById(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() + + // do a sync pass, and throw an error during the conflict resolver + // freezing the document + streamAndSync() + Assert.assertTrue(errorEmitted) + + // update the doc remotely + val nextDoc = Document("hello", "friend") + + 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 frozen + assertEquals( + withoutId(expectedDoc), + withoutSyncVersion(withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) + + // update the local doc. this should unfreeze the config + testSync.updateOneById(result.insertedId, Document("\$set", Document("no", "op"))) + + streamAndSync() + + // this should still be the remote doc since remote wins + assertEquals( + withoutId(nextDoc), + withoutSyncVersion(withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) + + // update the doc remotely + val lastDoc = Document("good night", "computer") + + sem = watchForEvents(syncTestRunner.namespace) + remoteColl.updateOne( + Document("_id", result.insertedId), + withNewSyncVersion(lastDoc) + ) + sem.acquire() + + // now that we're sync'd and unfrozen, it should be reflected locally + // TODO: STITCH-1958 Possible race condition here for update listening + streamAndSync() + + assertEquals( + withoutId(lastDoc), + withoutSyncVersion( + withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) + } + } + + @Test + fun testConfigure() { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // insert a documnet locally + val docToInsert = Document("hello", "world") + val insertedId = coll.insertOneAndSync(docToInsert).insertedId + + var hasConflictHandlerBeenInvoked = false + var hasChangeEventListenerBeenInvoked = false + + // configure Sync, each entry with flags checking + // that the listeners/handlers have been called + val changeEventListenerSemaphore = Semaphore(0) + coll.configure( + ConflictHandler { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> + hasConflictHandlerBeenInvoked = true + assertEquals(remoteEvent.fullDocument["fly"], "away") + remoteEvent.fullDocument + }, + ChangeEventListener { _: BsonValue, _: ChangeEvent -> + hasChangeEventListenerBeenInvoked = true + changeEventListenerSemaphore.release() + }, + ErrorListener { _, _ -> } + ) + + // insert a document remotely + remoteColl.insertOne(Document("_id", insertedId).append("fly", "away")) + + // sync. assert that the conflict handler and + // change event listener have been called + streamAndSync() + + assertTrue(changeEventListenerSemaphore.tryAcquire(10, TimeUnit.SECONDS)) + Assert.assertTrue(hasConflictHandlerBeenInvoked) + Assert.assertTrue(hasChangeEventListenerBeenInvoked) + } + + @Test + fun testSyncVersioningScheme() { + testSyncInBothDirections { + val coll = syncTestRunner.syncMethods() + + val remoteColl = syncTestRunner.remoteMethods() + + val docToInsert = Document("hello", "world") + + coll.configure(failingConflictHandler, null, null) + val insertResult = coll.insertOneAndSync(docToInsert) + + val doc = coll.findOneById(insertResult.insertedId) + val doc1Id = BsonObjectId(doc?.getObjectId("_id")) + val doc1Filter = Document("_id", doc1Id) + + 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)) + + assertEquals(0, versionCounterOf(firstRemoteDoc)) + + assertEquals(expectedDocument, coll.findOneById(doc1Id)) + + // 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.updateOneById(doc1Id, doc1Update).matchedCount) + + val secondRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDocBeforeSyncPass)) + assertEquals(versionOf(firstRemoteDoc), versionOf(secondRemoteDocBeforeSyncPass)) + + expectedDocument["foo"] = 1 + assertEquals(expectedDocument, coll.findOneById(doc1Id)) + + // 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.findOneById(doc1Id)) + + // 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.deleteOneById(doc1Id).deletedCount) + coll.insertOneAndSync(doc!!) + + val thirdRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDocBeforeSyncPass)) + + expectedDocument.remove("foo") + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + // 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + 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.deleteOneById(doc1Id).deletedCount) + streamAndSync() + coll.insertOneAndSync(doc) + streamAndSync() + + val fourthRemoteDoc = remoteColl.find(doc1Filter).first()!! + assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) + assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + + Assert.assertNotEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(fourthRemoteDoc)) + assertEquals(0, versionCounterOf(fourthRemoteDoc)) + } + } + + @Test + fun testUnsupportedSpvFails() { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + val docToInsert = withNewUnsupportedSyncVersion(Document("hello", "world")) + + val errorEmittedSem = Semaphore(0) + coll.configure( + failingConflictHandler, + null, + ErrorListener { _, _ -> errorEmittedSem.release() }) + + remoteColl.insertOne(docToInsert) + + val doc = remoteColl.find(docToInsert).first()!! + val doc1Id = BsonObjectId(doc.getObjectId("_id")) + coll.syncOne(doc1Id) + + assertTrue(coll.getSyncedIds().contains(doc1Id)) + + // syncing on this document with an unsupported spv should cause the document to desync + goOnline() + streamAndSync() + + assertFalse(coll.getSyncedIds().contains(doc1Id)) + + // an error should also have been emitted + assertTrue(errorEmittedSem.tryAcquire(10, TimeUnit.SECONDS)) + } + + @Test + fun testStaleFetchSingle() { + testSyncInBothDirections { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + // 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")) + + // 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.findOneById(doc1Id)) + + // update the document locally. + coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) + + // sync. assert the document still exists + streamAndSync() + Assert.assertNotNull(coll.findOneById(doc1Id)) + + // sync. assert the document still exists + streamAndSync() + Assert.assertNotNull(coll.findOneById(doc1Id)) + } + } + + @Test + fun testStaleFetchSingleDeleted() { + testSyncInBothDirections { + val coll = syncTestRunner.syncMethods() + + val remoteColl = syncTestRunner.remoteMethods() + + 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) + + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + throw IllegalStateException("failure") + }, null, null) + coll.syncOne(doc1Id) + + streamAndSync() + Assert.assertNotNull(coll.findOneById(doc1Id)) + + coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) + streamAndSync() + Assert.assertNotNull(coll.findOneById(doc1Id)) + + assertEquals(1, remoteColl.deleteOne(doc1Filter).deletedCount) + powerCycleDevice() + coll.configure(ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + throw IllegalStateException("failure") + }, null, null) + + streamAndSync() + Assert.assertNull(coll.findOneById(doc1Id)) + } + } + + @Test + fun testStaleFetchMultiple() { + testSyncInBothDirections { + val coll = syncTestRunner.syncMethods() + + val remoteColl = syncTestRunner.remoteMethods() + + val insertResult = + remoteColl.insertMany(listOf( + Document("hello", "world"), + Document("hello", "friend"))) + + // 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!!) + + streamAndSync() + Assert.assertNotNull(coll.findOneById(doc1Id)) + + coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) + streamAndSync() + Assert.assertNotNull(coll.findOneById(doc1Id)) + + coll.syncOne(doc2Id!!) + streamAndSync() + Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.findOneById(doc2Id)) + } + } + + private fun watchForEvents(namespace: MongoNamespace, n: Int = 1): Semaphore { + println("watching for $n change event(s) ns=$namespace") + val waitFor = AtomicInteger(n) + val sem = Semaphore(0) + syncTestRunner.dataSynchronizer.addWatcher(namespace, object : Callback, Any> { + override fun onComplete(result: OperationResult, Any>) { + if (result.isSuccessful && result.geResult() != null) { + println("change event of operation ${result.geResult().operationType} ns=$namespace found!") + } + if (waitFor.decrementAndGet() == 0) { + syncTestRunner.dataSynchronizer.removeWatcher(namespace, this) + sem.release() + } + } + }) + return sem + } + + private fun streamAndSync() { + if (syncTestRunner.testNetworkMonitor.connectedState) { + while (!syncTestRunner.dataSynchronizer.areAllStreamsOpen()) { + println("waiting for all streams to open before doing sync pass") + Thread.sleep(1000) + } + } + syncTestRunner.dataSynchronizer.doSyncPass() + } + + private fun powerCycleDevice() { + syncTestRunner.dataSynchronizer.reloadConfig() + } + + private fun goOffline() { + println("going offline") + syncTestRunner.testNetworkMonitor.connectedState = false + } + + private fun goOnline() { + println("going online") + syncTestRunner.testNetworkMonitor.connectedState = true + } + + private fun withoutId(document: Document): Document { + val newDoc = Document(document) + newDoc.remove("_id") + return newDoc + } + + private fun withoutSyncVersion(document: Document): Document { + val newDoc = Document(document) + newDoc.remove("__stitch_sync_version") + return newDoc + } + + private fun withNewSyncVersionSet(document: Document): Document { + return appendDocumentToKey( + "\$set", + document, + Document("__stitch_sync_version", freshSyncVersionDoc())) + } + + private fun withNewSyncVersion(document: Document): Document { + val newDocument = Document(java.util.HashMap(document)) + newDocument["__stitch_sync_version"] = freshSyncVersionDoc() + + return newDocument + } + + private fun withNewUnsupportedSyncVersion(document: Document): Document { + val newDocument = Document(java.util.HashMap(document)) + val badVersion = freshSyncVersionDoc() + badVersion.remove("spv") + badVersion.append("spv", 2) + + newDocument["__stitch_sync_version"] = badVersion + + return newDocument + } + + private fun freshSyncVersionDoc(): Document { + return Document("spv", 1).append("id", UUID.randomUUID().toString()).append("v", 0L) + } + + private fun versionOf(document: Document): Document { + return document["__stitch_sync_version"] as Document + } + + private fun versionCounterOf(document: Document): Long { + return versionOf(document)["v"] as Long + } + + private fun instanceIdOf(document: Document): String { + return versionOf(document)["id"] as String + } + + private fun appendDocumentToKey(key: String, on: Document, toAppend: Document): Document { + val newDocument = Document(HashMap(on)) + var found = false + newDocument.forEach { + if (it.key != key) { + return@forEach + } + found = true + val valueAtKey = (it.value as Document) + toAppend.forEach { doc -> + valueAtKey[doc.key] = doc.value + } + } + if (!found) { + newDocument[key] = toAppend + } + return newDocument + } + + private val failingConflictHandler = ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> + Assert.fail("did not expect a conflict") + throw IllegalStateException("unreachable") + } + + private fun testSyncInBothDirections(testFun: () -> Unit) { + println("running tests with L2R going first") + syncTestRunner.dataSynchronizer.swapSyncDirection(true) + testFun() + + syncTestRunner.teardown() + syncTestRunner.setup() + println("running tests with R2L going first") + syncTestRunner.dataSynchronizer.swapSyncDirection(false) + testFun() + } +} diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt new file mode 100644 index 000000000..026fc4f27 --- /dev/null +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt @@ -0,0 +1,131 @@ +package com.mongodb.stitch.core.testutils.sync + +import com.mongodb.MongoNamespace +import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer +import com.mongodb.stitch.core.testutils.BaseStitchIntTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +/** + * Test running interface for Sync integration tests. + * + * Each @Test method in this interface reflects a test + * that must be implemented to properly test Sync. + * + * The tests should be proxied to a [SyncIntTestProxy] proxy. + * [SyncIntTestProxy] and [SyncIntTestRunner] should be in sync + * on the these test methods. + */ +interface SyncIntTestRunner { + /** + * An integrated DataSynchronizer. + */ + val dataSynchronizer: DataSynchronizer + + /** + * A network monitor that allows us to control the network state + * of the dataSynchronizer. + */ + val testNetworkMonitor: BaseStitchIntTest.TestNetworkMonitor + + /** + * A namespace to be used with these tests. + */ + val namespace: MongoNamespace + + /** + * A series of remote methods, independent of platform, + * that have been normalized for testing. + * + * @return [ProxyRemoteMethods] + */ + fun remoteMethods(): ProxyRemoteMethods + + /** + * A series of sync methods, independent of platform, + * that have been normalized for testing. + * + * @return [ProxySyncMethods] + */ + fun syncMethods(): ProxySyncMethods + + /* TEST METHODS */ + @Before + fun teardown() + + @After + fun setup() + + @Test + fun testSync() + + @Test + fun testUpdateConflicts() + + @Test + fun testUpdateRemoteWins() + + @Test + fun testUpdateLocalWins() + + @Test + fun testDeleteOneByIdNoConflict() + + @Test + fun testDeleteOneByIdConflict() + + @Test + fun testInsertThenUpdateThenSync() + + @Test + fun testInsertThenSyncUpdateThenUpdate() + + @Test + fun testInsertThenSyncThenRemoveThenInsertThenUpdate() + + @Test + fun testRemoteDeletesLocalNoConflict() + + @Test + fun testRemoteDeletesLocalConflict() + + @Test + fun testRemoteInsertsLocalUpdates() + + @Test + fun testRemoteInsertsWithVersionLocalUpdates() + + @Test + fun testResolveConflictWithDelete() + + @Test + fun testTurnDeviceOffAndOn() + + @Test + fun testDesync() + + @Test + fun testInsertInsertConflict() + + @Test + fun testFrozenDocumentConfig() + + @Test + fun testConfigure() + + @Test + fun testSyncVersioningScheme() + + @Test + fun testUnsupportedSpvFails() + + @Test + fun testStaleFetchSingle() + + @Test + fun testStaleFetchSingleDeleted() + + @Test + fun testStaleFetchMultiple() +} diff --git a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt index bb7a0561c..4de07e144 100644 --- a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt +++ b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt @@ -6,54 +6,108 @@ import com.mongodb.stitch.core.admin.authProviders.ProviderConfigs import com.mongodb.stitch.core.admin.services.ServiceConfigs import com.mongodb.stitch.core.admin.services.rules.RuleCreator import com.mongodb.stitch.core.auth.providers.anonymous.AnonymousCredential -import com.mongodb.stitch.core.internal.common.Callback -import com.mongodb.stitch.core.internal.common.OperationResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler -import com.mongodb.stitch.core.services.mongodb.remote.sync.DefaultSyncConflictResolvers import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener -import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent +import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer +import com.mongodb.stitch.core.testutils.sync.ProxyRemoteMethods +import com.mongodb.stitch.core.testutils.sync.ProxySyncMethods +import com.mongodb.stitch.core.testutils.sync.SyncIntTestProxy +import com.mongodb.stitch.core.testutils.sync.SyncIntTestRunner import com.mongodb.stitch.server.services.mongodb.remote.RemoteMongoClient import com.mongodb.stitch.server.services.mongodb.remote.RemoteMongoCollection import com.mongodb.stitch.server.services.mongodb.remote.Sync -import org.bson.BsonDocument -import org.bson.BsonObjectId import org.bson.BsonValue import org.bson.Document +import org.bson.conversions.Bson import org.bson.types.ObjectId import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Assert.fail +import org.junit.Assert import org.junit.Assume import org.junit.Before import org.junit.Test -import java.lang.Exception -import java.util.UUID -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -class SyncMongoClientIntTests : BaseStitchServerIntTest() { +class SyncMongoClientIntTests : BaseStitchServerIntTest(), SyncIntTestRunner { + private class RemoteMethods(private val remoteMongoCollection: RemoteMongoCollection) : ProxyRemoteMethods { + override fun insertOne(document: Document): RemoteInsertOneResult { + return remoteMongoCollection.insertOne(document) + } - private val mongodbUriProp = "test.stitch.mongodbURI" + override fun insertMany(documents: List): RemoteInsertManyResult { + return remoteMongoCollection.insertMany(documents) + } + + override fun find(filter: Document): Iterable { + return remoteMongoCollection.find(filter) + } + + override fun updateOne(filter: Document, updateDocument: Document): RemoteUpdateResult { + return remoteMongoCollection.updateOne(filter, updateDocument) + } + + override fun deleteOne(filter: Bson): RemoteDeleteResult { + return remoteMongoCollection.deleteOne(filter) + } + } + + private class SyncMethods(private val sync: Sync) : ProxySyncMethods { + override fun configure( + conflictResolver: ConflictHandler, + changeEventListener: ChangeEventListener?, + errorListener: ErrorListener? + ) { + sync.configure(conflictResolver, changeEventListener, errorListener) + } + + override fun syncOne(id: BsonValue) { + sync.syncOne(id) + } + + override fun insertOneAndSync(document: Document): RemoteInsertOneResult { + return sync.insertOneAndSync(document) + } + + override fun findOneById(id: BsonValue): Document? { + return sync.findOneById(id) + } + + override fun updateOneById(documentId: BsonValue, update: Bson): RemoteUpdateResult { + return sync.updateOneById(documentId, update) + } - private var remoteMongoClientOpt: RemoteMongoClient? = null - private val remoteMongoClient: RemoteMongoClient - get() = remoteMongoClientOpt!! - private var mongoClientOpt: RemoteMongoClient? = null - private val mongoClient: RemoteMongoClient - get() = mongoClientOpt!! + override fun deleteOneById(documentId: BsonValue): RemoteDeleteResult { + return sync.deleteOneById(documentId) + } + + override fun getSyncedIds(): Set { + return sync.syncedIds + } + + override fun desyncOne(id: BsonValue) { + sync.desyncOne(id) + } + + override fun find(filter: Bson): Iterable { + return sync.find(filter) + } + } + + private val mongodbUriProp = "test.stitch.mongodbURI" + private lateinit var remoteMongoClient: RemoteMongoClient + private lateinit var mongoClient: RemoteMongoClient private var dbName = ObjectId().toHexString() private var collName = ObjectId().toHexString() - private var namespace = MongoNamespace(dbName, collName) + override var namespace = MongoNamespace(dbName, collName) + override val dataSynchronizer: DataSynchronizer + get() = (mongoClient as RemoteMongoClientImpl).dataSynchronizer + override val testNetworkMonitor: TestNetworkMonitor + get() = BaseStitchServerIntTest.testNetworkMonitor - private fun getMongoDbUri(): String { - return System.getProperty(mongodbUriProp, "mongodb://localhost:26000") - } + private val testProxy = SyncIntTestProxy(this) @Before override fun setup() { @@ -90,1213 +144,161 @@ class SyncMongoClientIntTests : BaseStitchServerIntTest() { val client = getAppClient(app.first) client.auth.loginWithCredential(AnonymousCredential()) - mongoClientOpt = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") + mongoClient = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") (mongoClient as RemoteMongoClientImpl).dataSynchronizer.stop() (mongoClient as RemoteMongoClientImpl).dataSynchronizer.disableSyncThread() - remoteMongoClientOpt = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") - goOnline() + remoteMongoClient = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") + BaseStitchServerIntTest.testNetworkMonitor.connectedState = true } @After override fun teardown() { - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.close() + if (::mongoClient.isInitialized) { + (mongoClient as RemoteMongoClientImpl).dataSynchronizer.close() + } super.teardown() } - private fun getTestSync(): Sync { - val db = mongoClient.getDatabase(dbName) - assertEquals(dbName, db.name) + override fun remoteMethods(): ProxyRemoteMethods { + val db = remoteMongoClient.getDatabase(dbName) + Assert.assertEquals(dbName, db.name) val coll = db.getCollection(collName) - assertEquals(MongoNamespace(dbName, collName), coll.namespace) - return coll.sync() + Assert.assertEquals(MongoNamespace(dbName, collName), coll.namespace) + return RemoteMethods(coll) } - private fun getTestCollRemote(): RemoteMongoCollection { - val db = remoteMongoClient.getDatabase(dbName) - assertEquals(dbName, db.name) + override fun syncMethods(): ProxySyncMethods { + val db = mongoClient.getDatabase(dbName) + Assert.assertEquals(dbName, db.name) val coll = db.getCollection(collName) - assertEquals(MongoNamespace(dbName, collName), coll.namespace) - return coll + Assert.assertEquals(MongoNamespace(dbName, collName), coll.namespace) + return SyncMethods(coll.sync()) } @Test - fun testSync() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val doc1 = Document("hello", "world") - val doc2 = Document("hello", "friend") - doc2["proj"] = "field" - remoteColl.insertMany(listOf(doc1, doc2)) - - // get the document - val doc = remoteColl.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 - coll.configure({ id: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> - if (id.equals(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) - coll.syncOne(doc1Id) - streamAndSync() - - // 1. updating a document remotely should not be reflected until coming back online. - goOffline() - val doc1Update = Document("\$inc", Document("foo", 1)) - val result = remoteColl.updateOne( - doc1Filter, - doc1Update) - assertEquals(1, result.matchedCount) - streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) - goOnline() - streamAndSync() - val expectedDocument = Document(doc) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, coll.findOneById(doc1Id)) - - // 2. insertOneAndSync should work offline and then sync the document when online. - goOffline() - val doc3 = Document("so", "syncy") - val insResult = coll.insertOneAndSync(doc3) - assertEquals(doc3, withoutSyncVersion(coll.findOneById(insResult.insertedId)!!)) - streamAndSync() - assertNull(remoteColl.find(Document("_id", doc3["_id"])).first()) - goOnline() - streamAndSync() - assertEquals(doc3, withoutSyncVersion(remoteColl.find(Document("_id", doc3["_id"])).first()!!)) - - // 3. updating a document locally that has been updated remotely should invoke the conflict - // resolver. - val sem = watchForEvents(this.namespace) - val result2 = remoteColl.updateOne( - doc1Filter, - withNewSyncVersionSet(doc1Update)) - sem.acquire() - assertEquals(1, result2.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - val result3 = coll.updateOneById( - doc1Id, - doc1Update) - assertEquals(1, result3.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - // first pass will invoke the conflict handler and update locally but not remotely yet - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedDocument["foo"] = 4 - expectedDocument.remove("fooOps") - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - // second pass will update with the ack'd version id - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + override fun testSync() { + testProxy.testSync() } @Test - fun testUpdateConflicts() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - 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({ _: 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 - } - merged - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - // Update remote - val remoteUpdate = withNewSyncVersionSet(Document("\$set", Document("remote", "update"))) - val sem = watchForEvents(this.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.updateOneById(doc1Id, localUpdate) - assertEquals(1, result.matchedCount) - val expectedLocalDocument = Document(doc) - expectedLocalDocument["local"] = "updateWow" - assertEquals(expectedLocalDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - // 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, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - // second pass will update with the ack'd version id - streamAndSync() - assertEquals(expectedLocalDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - assertEquals(expectedLocalDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + override fun testUpdateConflicts() { + testProxy.testUpdateConflicts() } @Test - fun testUpdateRemoteWins() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - remoteColl.insertOne(docToInsert) - - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(DefaultSyncConflictResolvers.remoteWins(), null, null) - coll.syncOne(doc1Id) - streamAndSync() - - val expectedDocument = Document(doc) - val sem = watchForEvents(this.namespace) - var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - sem.acquire() - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - result = coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - streamAndSync() - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + override fun testUpdateRemoteWins() { + testProxy.testUpdateRemoteWins() } @Test - fun testUpdateLocalWins() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - remoteColl.insertOne(docToInsert) - - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - coll.syncOne(doc1Id) - streamAndSync() - - val expectedDocument = Document(doc) - val sem = watchForEvents(this.namespace) - var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - sem.acquire() - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - result = coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - streamAndSync() - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + override fun testUpdateLocalWins() { + testProxy.testUpdateLocalWins() } @Test - fun testDeleteOneByIdNoConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - 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(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - goOffline() - val result = coll.deleteOneById(doc1Id) - assertEquals(1, result.deletedCount) - - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertNull(coll.findOneById(doc1Id)) - - goOnline() - streamAndSync() - assertNull(remoteColl.find(doc1Filter).first()) - assertNull(coll.findOneById(doc1Id)) - } + override fun testDeleteOneByIdNoConflict() { + testProxy.testDeleteOneByIdNoConflict() } @Test - fun testDeleteOneByIdConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - 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({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("well", "shoot") - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, remoteColl.updateOne( - doc1Filter, - withNewSyncVersionSet(doc1Update)).matchedCount) - - goOffline() - val result = coll.deleteOneById(doc1Id) - assertEquals(1, result.deletedCount) - - val expectedDocument = Document(doc) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertNull(coll.findOneById(doc1Id)) - - goOnline() - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedDocument.remove("hello") - expectedDocument.remove("foo") - expectedDocument["well"] = "shoot" - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - } + override fun testDeleteOneByIdConflict() { + testProxy.testDeleteOneByIdConflict() } @Test - fun testInsertThenUpdateThenSync() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOneAndSync(docToInsert) - - val doc = coll.findOneById(insertResult.insertedId)!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOneById(doc1Id, doc1Update).matchedCount) - - val expectedDocument = withoutSyncVersion(Document(doc)) - expectedDocument["foo"] = 1 - assertNull(remoteColl.find(doc1Filter).first()) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - goOnline() - streamAndSync() - - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - } + override fun testInsertThenUpdateThenSync() { + testProxy.testInsertThenUpdateThenSync() } @Test - fun testInsertThenSyncUpdateThenUpdate() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOneAndSync(docToInsert) - - val doc = coll.findOneById(insertResult.insertedId)!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - goOnline() - streamAndSync() - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOneById(doc1Id, doc1Update).matchedCount) - - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - } + override fun testInsertThenSyncUpdateThenUpdate() { + testProxy.testInsertThenSyncUpdateThenUpdate() } @Test - fun testInsertThenSyncThenRemoveThenInsertThenUpdate() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOneAndSync(docToInsert) - streamAndSync() - - val doc = coll.findOneById(insertResult.insertedId)!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - val expectedDocument = withoutSyncVersion(Document(doc)) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - assertEquals(1, coll.deleteOneById(doc1Id).deletedCount) - coll.insertOneAndSync(doc) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOneById(doc1Id, doc1Update).matchedCount) - - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - } + override fun testInsertThenSyncThenRemoveThenInsertThenUpdate() { + testProxy.testInsertThenSyncThenRemoveThenInsertThenUpdate() } @Test - fun testRemoteDeletesLocalNoConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - 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(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - - assertEquals(coll.syncedIds.size, 1) - - val sem = watchForEvents(this.namespace) - remoteColl.deleteOne(doc1Filter) - sem.acquire() - - streamAndSync() - - assertNull(remoteColl.find(doc1Filter).first()) - assertNull(coll.findOneById(doc1Id)) - - // This should not re-sync the document - streamAndSync() - remoteColl.insertOne(doc) - streamAndSync() - - assertEquals(doc, remoteColl.find(doc1Filter).first()) - assertNull(coll.findOneById(doc1Id)) - } + override fun testRemoteDeletesLocalNoConflict() { + testProxy.testRemoteDeletesLocalNoConflict() } @Test - fun testRemoteDeletesLocalConflict() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - 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({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("hello", "world") - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) - assertNotNull(coll.findOneById(doc1Id)) - - goOffline() - remoteColl.deleteOne(doc1Filter) - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) - - goOnline() - streamAndSync() - assertNull(remoteColl.find(doc1Filter).first()) - assertNotNull(coll.findOneById(doc1Id)) - - streamAndSync() - assertNotNull(remoteColl.find(doc1Filter).first()) - assertNotNull(coll.findOneById(doc1Id)) - } + override fun testRemoteDeletesLocalConflict() { + testProxy.testRemoteDeletesLocalConflict() } @Test - fun testRemoteInsertsLocalUpdates() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - 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({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("hello", "again") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - - assertEquals(doc, coll.findOneById(doc1Id)) - assertNotNull(coll.findOneById(doc1Id)) - - val wait = watchForEvents(this.namespace, 2) - remoteColl.deleteOne(doc1Filter) - remoteColl.insertOne(withNewSyncVersion(doc)) - wait.acquire() - - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) - - streamAndSync() - - assertEquals(doc, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - val expectedDocument = Document("_id", doc1Id.value) - expectedDocument["hello"] = "again" - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - } + override fun testRemoteInsertsLocalUpdates() { + testProxy.testRemoteInsertsLocalUpdates() } @Test - fun testRemoteInsertsWithVersionLocalUpdates() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - remoteColl.insertOne(withNewSyncVersion(docToInsert)) - - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure(failingConflictHandler, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) - - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) - - streamAndSync() - val expectedDocument = Document(withoutSyncVersion(doc)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - } + override fun testRemoteInsertsWithVersionLocalUpdates() { + testProxy.testRemoteInsertsWithVersionLocalUpdates() } @Test - fun testResolveConflictWithDelete() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - remoteColl.insertOne(withNewSyncVersion(docToInsert)) - - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - null - }, null, null) - coll.syncOne(doc1Id) - streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) - assertNotNull(coll.findOneById(doc1Id)) - - val sem = watchForEvents(this.namespace) - assertEquals(1, remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 1)))).matchedCount) - sem.acquire() - - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) - - streamAndSync() - val expectedDocument = Document(withoutSyncVersion(doc)) - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - - goOffline() - assertNull(coll.findOneById(doc1Id)) - - goOnline() - streamAndSync() - assertNull(remoteColl.find(doc1Filter).first()) - assertNull(coll.findOneById(doc1Id)) - } + override fun testResolveConflictWithDelete() { + testProxy.testResolveConflictWithDelete() } @Test - fun testTurnDeviceOffAndOn() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - docToInsert["foo"] = 1 - remoteColl.insertOne(docToInsert) - - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - powerCycleDevice() - - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - coll.syncOne(doc1Id) - - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - streamAndSync() - - val expectedDocument = Document(doc) - var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - result = coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) - assertEquals(1, result.matchedCount) - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - streamAndSync() // does nothing with no conflict handler - - assertEquals(1, coll.syncedIds.size) - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - streamAndSync() // resolves the conflict - - expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - powerCycleDevice() - coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + override fun testTurnDeviceOffAndOn() { + testProxy.testTurnDeviceOffAndOn() } @Test - fun testDesync() { - testSyncInBothDirections { - val coll = getTestSync() - - val docToInsert = Document("hello", "world") - coll.configure(failingConflictHandler, null, null) - val doc1Id = coll.insertOneAndSync(docToInsert).insertedId - - assertEquals(docToInsert, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - coll.desyncOne(doc1Id) - streamAndSync() - assertNull(coll.findOneById(doc1Id)) - } + override fun testDesync() { + testProxy.testDesync() } @Test - fun testInsertInsertConflict() { - testSyncInBothDirections { - val coll = getTestSync() - val remoteColl = getTestCollRemote() - - val docToInsert = Document("_id", "hello") - - remoteColl.insertOne(docToInsert) - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - Document("friend", "welcome") - }, null, null) - val doc1Id = coll.insertOneAndSync(docToInsert).insertedId - - val doc1Filter = Document("_id", doc1Id) - - streamAndSync() - val expectedDocument = Document(docToInsert) - expectedDocument["friend"] = "welcome" - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - assertEquals(docToInsert, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - - streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) - assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - } + override fun testInsertInsertConflict() { + testProxy.testInsertInsertConflict() } @Test - fun testFrozenDocumentConfig() { - testSyncInBothDirections { - val testSync = getTestSync() - val remoteColl = getTestCollRemote() - var errorEmitted = false - - var conflictCounter = 0 - - testSync.configure( - { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> - if (conflictCounter == 0) { - conflictCounter++ - errorEmitted = true - throw Exception("ouch") - } - remoteEvent.fullDocument - }, - { _: BsonValue, _: ChangeEvent -> - }, { _, _ -> - }) - - // insert an initial doc - val testDoc = Document("hello", "world") - val result = testSync.insertOneAndSync(testDoc) - - // do a sync pass, synchronizing the doc - streamAndSync() - - assertNotNull(remoteColl.find(Document("_id", testDoc.get("_id"))).first()) - - // update the doc - val expectedDoc = Document("hello", "computer") - testSync.updateOneById(result.insertedId, Document("\$set", expectedDoc)) - - // create a conflict - var sem = watchForEvents(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() - assertTrue(errorEmitted) - - // update the doc remotely - val nextDoc = Document("hello", "friend") - - sem = watchForEvents(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 frozen - assertEquals( - withoutId(expectedDoc), - withoutSyncVersion(withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) - - // update the local doc. this should unfreeze the config - testSync.updateOneById(result.insertedId, Document("\$set", Document("no", "op"))) - - streamAndSync() - - // this should still be the remote doc since remote wins - assertEquals( - withoutId(nextDoc), - withoutSyncVersion(withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) - - // update the doc remotely - val lastDoc = Document("good night", "computer") - - sem = watchForEvents(namespace) - remoteColl.updateOne( - Document("_id", result.insertedId), - withNewSyncVersion(lastDoc) - ) - sem.acquire() - - // now that we're sync'd and unfrozen, it should be reflected locally - // TODO: STITCH-1958 Possible race condition here for update listening - streamAndSync() - - assertEquals( - withoutId(lastDoc), - withoutSyncVersion( - withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) - } + override fun testFrozenDocumentConfig() { + testProxy.testFrozenDocumentConfig() } @Test - fun testConfigure() { - val testSync = getTestSync() - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - val insertedId = testSync.insertOneAndSync(docToInsert).insertedId - - var hasConflictHandlerBeenInvoked = false - var hasChangeEventListenerBeenInvoked = false - - testSync.configure( - { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> - hasConflictHandlerBeenInvoked = true - assertEquals(remoteEvent.fullDocument["fly"], "away") - remoteEvent.fullDocument - }, - { _: BsonValue, _: ChangeEvent -> - hasChangeEventListenerBeenInvoked = true - }, - { _, _ -> } - ) - - val sem = watchForEvents(namespace) - remoteColl.insertOne(Document("_id", insertedId).append("fly", "away")) - sem.acquire() - - streamAndSync() - - assertTrue(hasConflictHandlerBeenInvoked) - assertTrue(hasChangeEventListenerBeenInvoked) - } - - private fun streamAndSync() { - val dataSync = (mongoClient as RemoteMongoClientImpl).dataSynchronizer - if (testNetworkMonitor.connectedState) { - while (!dataSync.areAllStreamsOpen()) { - println("waiting for all streams to open before doing sync pass") - Thread.sleep(1000) - } - } - dataSync.doSyncPass() + override fun testConfigure() { + testProxy.testConfigure() } @Test - fun testSyncVersioningScheme() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = Document("hello", "world") - - coll.configure(failingConflictHandler, null, null) - val insertResult = coll.insertOneAndSync(docToInsert) - - val doc = coll.findOneById(insertResult.insertedId) - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - val doc1Filter = Document("_id", doc1Id) - - 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)) - - assertEquals(0, versionCounterOf(firstRemoteDoc)) - - assertEquals(expectedDocument, coll.findOneById(doc1Id)) - - // 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.updateOneById(doc1Id, doc1Update).matchedCount) - - val secondRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDocBeforeSyncPass)) - assertEquals(versionOf(firstRemoteDoc), versionOf(secondRemoteDocBeforeSyncPass)) - - expectedDocument["foo"] = 1 - assertEquals(expectedDocument, coll.findOneById(doc1Id)) - - // 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.findOneById(doc1Id)) - - // 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.deleteOneById(doc1Id).deletedCount) - coll.insertOneAndSync(doc) - - val thirdRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDocBeforeSyncPass)) - - expectedDocument.remove("foo") - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id))) - - // 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, withoutSyncVersion(coll.findOneById(doc1Id))) - - 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.deleteOneById(doc1Id).deletedCount) - streamAndSync() - coll.insertOneAndSync(doc) - streamAndSync() - - val fourthRemoteDoc = remoteColl.find(doc1Filter).first()!! - assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id))) - - assertNotEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(fourthRemoteDoc)) - assertEquals(0, versionCounterOf(fourthRemoteDoc)) - } + override fun testSyncVersioningScheme() { + testProxy.testSyncVersioningScheme() } @Test - fun testUnsupportedSpvFails() { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val docToInsert = withNewUnsupportedSyncVersion(Document("hello", "world")) - - val errorEmittedSem = Semaphore(0) - coll.configure( - failingConflictHandler, - null, - ErrorListener { _, _ -> errorEmittedSem.release() }) - - remoteColl.insertOne(docToInsert) - - val doc = remoteColl.find(docToInsert).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - coll.syncOne(doc1Id) - - assertTrue(coll.syncedIds.contains(doc1Id)) - - // syncing on this document with an unsupported spv should cause the document to desync - goOnline() - streamAndSync() - - assertFalse(coll.syncedIds.contains(doc1Id)) - - // an error should also have been emitted - assertTrue(errorEmittedSem.tryAcquire(10, TimeUnit.SECONDS)) + override fun testUnsupportedSpvFails() { + testProxy.testUnsupportedSpvFails() } @Test - fun testStaleFetchSingle() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val doc1 = Document("hello", "world") - remoteColl.insertOne(doc1) - - // get the document - val doc = remoteColl.find(doc1).first()!! - val doc1Id = BsonObjectId(doc.getObjectId("_id")) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - - coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - } + override fun testStaleFetchSingle() { + testProxy.testStaleFetchSingle() } @Test - fun testStaleFetchSingleDeleted() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - 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) - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - - coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - - assertEquals(1, remoteColl.deleteOne(doc1Filter).deletedCount) - powerCycleDevice() - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - - streamAndSync() - assertNull(coll.findOneById(doc1Id)) - } + override fun testStaleFetchSingleDeleted() { + testProxy.testStaleFetchSingleDeleted() } @Test - fun testStaleFetchMultiple() { - testSyncInBothDirections { - val coll = getTestSync() - - val remoteColl = getTestCollRemote() - - val insertResult = - remoteColl.insertMany(listOf( - Document("hello", "world"), - Document("hello", "friend"))) - - // get the document - val doc1Id = insertResult.insertedIds[0] - val doc2Id = insertResult.insertedIds[1] - - coll.configure({ _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - throw IllegalStateException("failure") - }, null, null) - coll.syncOne(doc1Id) - - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - - coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - - coll.syncOne(doc2Id) - streamAndSync() - assertNotNull(coll.findOneById(doc1Id)) - assertNotNull(coll.findOneById(doc2Id)) - } + override fun testStaleFetchMultiple() { + testProxy.testStaleFetchMultiple() } - private fun watchForEvents( - namespace: MongoNamespace, - n: Int = 1 - ): Semaphore { - println("watching for $n change event(s) ns=$namespace") - val waitFor = AtomicInteger(n) - val sem = Semaphore(0) - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.addWatcher(namespace, object : Callback, Any> { - override fun onComplete(result: OperationResult, Any>) { - if (result.isSuccessful && result.geResult() != null) { - println("change event of operation ${result.geResult().operationType} ns=$namespace found!") - } - if (waitFor.decrementAndGet() == 0) { - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.removeWatcher(namespace, this) - sem.release() - } - } - }) - return sem - } - - private fun powerCycleDevice() { - (mongoClient as RemoteMongoClientImpl).dataSynchronizer.reloadConfig() - } - - private fun goOffline() { - println("going offline") - testNetworkMonitor.connectedState = false - } - - private fun goOnline() { - println("going online") - testNetworkMonitor.connectedState = true - } - - private fun withoutId(document: Document): Document { - val newDoc = Document(document) - newDoc.remove("_id") - return newDoc - } - - private fun withoutSyncVersion(document: Document): Document { - val newDoc = Document(document) - newDoc.remove("__stitch_sync_version") - return newDoc - } - - private fun withNewSyncVersion(document: Document): Document { - val newDocument = Document(java.util.HashMap(document)) - newDocument["__stitch_sync_version"] = freshSyncVersionDoc() - - return newDocument - } - - private fun withNewSyncVersionSet(document: Document): Document { - return appendDocumentToKey( - "\$set", - document, - Document("__stitch_sync_version", freshSyncVersionDoc())) - } - - private fun withNewUnsupportedSyncVersion(document: Document): Document { - val newDocument = Document(java.util.HashMap(document)) - val badVersion = freshSyncVersionDoc() - badVersion.remove("spv") - badVersion.append("spv", 2) - - newDocument["__stitch_sync_version"] = badVersion - - return newDocument - } - - private fun freshSyncVersionDoc(): Document { - return Document("spv", 1).append("id", UUID.randomUUID().toString()).append("v", 0L) - } - - private fun versionOf(document: Document): Document { - return document["__stitch_sync_version"] as Document - } - - private fun versionCounterOf(document: Document): Long { - return versionOf(document)["v"] as Long - } - - private fun instanceIdOf(document: Document): String { - return versionOf(document)["id"] as String - } - - private fun appendDocumentToKey(key: String, on: Document, toAppend: Document): Document { - val newDocument = Document(HashMap(on)) - var found = false - newDocument.forEach { - if (it.key != key) { - return@forEach - } - found = true - val valueAtKey = (it.value as Document) - toAppend.forEach { - valueAtKey[it.key] = it.value - } - } - if (!found) { - newDocument[key] = toAppend - } - return newDocument - } - - private val failingConflictHandler: ConflictHandler = ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> - fail("did not expect a conflict") - throw IllegalStateException("unreachable") - } - - private fun testSyncInBothDirections(testFun: () -> Unit) { - val dataSync = (mongoClient as RemoteMongoClientImpl).dataSynchronizer - println("running tests with L2R going first") - dataSync.swapSyncDirection(true) - testFun() - - teardown() - setup() - println("running tests with R2L going first") - dataSync.swapSyncDirection(false) - testFun() + /** + * Get the uri for where mongodb is running locally. + */ + private fun getMongoDbUri(): String { + return System.getProperty(mongodbUriProp, "mongodb://localhost:26000") } } diff --git a/server/testutils/src/main/java/com/mongodb/stitch/server/testutils/BaseStitchServerIntTest.kt b/server/testutils/src/main/java/com/mongodb/stitch/server/testutils/BaseStitchServerIntTest.kt index b9a4fed36..25fbd3bca 100644 --- a/server/testutils/src/main/java/com/mongodb/stitch/server/testutils/BaseStitchServerIntTest.kt +++ b/server/testutils/src/main/java/com/mongodb/stitch/server/testutils/BaseStitchServerIntTest.kt @@ -5,44 +5,18 @@ import com.mongodb.stitch.core.admin.Apps import com.mongodb.stitch.core.admin.apps.AppResponse import com.mongodb.stitch.core.admin.userRegistrations.sendConfirmation import com.mongodb.stitch.core.auth.providers.userpassword.UserPasswordCredential -import com.mongodb.stitch.core.internal.net.NetworkMonitor import com.mongodb.stitch.core.testutils.BaseStitchIntTest import com.mongodb.stitch.server.core.Stitch import com.mongodb.stitch.server.core.StitchAppClient import com.mongodb.stitch.server.core.auth.providers.userpassword.UserPasswordAuthProviderClient import org.junit.After import org.junit.Before -import java.util.concurrent.CopyOnWriteArrayList open class BaseStitchServerIntTest : BaseStitchIntTest() { private var clients: MutableList = mutableListOf() private val dataDir = System.getProperty("java.io.tmpdir") - class TestNetworkMonitor : NetworkMonitor { - private var _connectedState = false - var connectedState: Boolean - set(value) { - _connectedState = value - listeners.forEach { it.onNetworkStateChanged() } - } - get() = _connectedState - - private var listeners = CopyOnWriteArrayList() - - override fun isConnected(): Boolean { - return connectedState - } - - override fun addNetworkStateListener(listener: NetworkMonitor.StateListener) { - listeners.add(listener) - } - - override fun removeNetworkStateListener(listener: NetworkMonitor.StateListener) { - listeners.remove(listener) - } - } - companion object { val testNetworkMonitor = TestNetworkMonitor() }