diff --git a/realm/realm-library/build.gradle b/realm/realm-library/build.gradle index bff4d2fb2f..2d96ec0802 100644 --- a/realm/realm-library/build.gradle +++ b/realm/realm-library/build.gradle @@ -104,7 +104,10 @@ android { java.srcDirs += ['src/androidTest/kotlin', 'src/testUtils/java', 'src/testUtils/kotlin'] } androidTestObjectServer { - java.srcDirs += [/* FIXME 'src/syncIntegrationTest/java', */ 'src/androidTestObjectServer/kotlin', 'src/syncTestUtils/java', 'src/syncTestUtils/kotlin'] + java.srcDirs += [/* FIXME 'src/syncIntegrationTest/java', */ + 'src/syncIntegrationTest/kotlin', + 'src/androidTestObjectServer/kotlin', 'src/syncTestUtils/java' + , 'src/syncTestUtils/kotlin'] assets.srcDirs += ['src/syncIntegrationTest/assets/'] } } diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/DefaultSyncSchema.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/DefaultSyncSchema.kt index 246e85a22a..b448797167 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/DefaultSyncSchema.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/DefaultSyncSchema.kt @@ -22,6 +22,6 @@ const val defaultPartitionValue = "default" /** * The set of classes initially supported by MongoDB Realm. */ -@RealmModule(classes = [SyncDog::class, SyncPerson::class]) +@RealmModule(classes = [SyncDog::class, SyncPerson::class, SyncSupportedTypes::class]) class DefaultSyncSchema { -} \ No newline at end of file +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncAllTypes.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncAllTypes.kt new file mode 100644 index 0000000000..58713f55e1 --- /dev/null +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncAllTypes.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.entities + +import io.realm.MutableRealmInteger +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.TestHelper +import io.realm.annotations.PrimaryKey +import io.realm.annotations.RealmField +import io.realm.annotations.Required +import org.bson.types.Decimal128 +import org.bson.types.ObjectId +import java.math.BigDecimal +import java.util.* + +open class SyncAllTypes : RealmObject() { + + companion object { + const val CLASS_NAME = "AllTypes" + const val FIELD_STRING = "columnString" + const val FIELD_LONG = "columnLong" + const val FIELD_FLOAT = "columnFloat" + const val FIELD_DOUBLE = "columnDouble" + const val FIELD_BOOLEAN = "columnBoolean" + const val FIELD_DATE = "columnDate" + const val FIELD_BINARY = "columnBinary" + const val FIELD_MUTABLEREALMINTEGER = "columnMutableRealmInteger" + const val FIELD_DECIMAL128 = "columnDecimal128" + const val FIELD_OBJECT_ID = "columnObjectId" + const val FIELD_REALMOBJECT = "columnRealmObject" + const val FIELD_REALMLIST = "columnRealmList" + const val FIELD_STRING_LIST = "columnStringList" + const val FIELD_BINARY_LIST = "columnBinaryList" + const val FIELD_BOOLEAN_LIST = "columnBooleanList" + const val FIELD_LONG_LIST = "columnLongList" + const val FIELD_DOUBLE_LIST = "columnDoubleList" + const val FIELD_FLOAT_LIST = "columnFloatList" + const val FIELD_DATE_LIST = "columnDateList" + val INVALID_TYPES_FIELDS_FOR_DISTINCT = arrayOf(FIELD_REALMOBJECT, FIELD_REALMLIST, FIELD_DOUBLE, FIELD_FLOAT, + FIELD_STRING_LIST, FIELD_BINARY_LIST, FIELD_BOOLEAN_LIST, FIELD_LONG_LIST, + FIELD_DOUBLE_LIST, FIELD_FLOAT_LIST, FIELD_DATE_LIST) + } + + @PrimaryKey + @RealmField(name = "_id") + var id = ObjectId() + + @Required + var columnString = "" + var columnLong: Long = 0 + var columnFloat = 0f + var columnDouble = 0.0 + var isColumnBoolean = false + + @Required + var columnDate = Date(0) + + @Required + var columnBinary = ByteArray(0) + + @Required + var columnDecimal128 = Decimal128(BigDecimal.ZERO) + + @Required + var columnObjectId = ObjectId(TestHelper.randomObjectIdHexString()) + val columnRealmInteger = MutableRealmInteger.ofNull() + var columnRealmObject: SyncDog? = null + var columnRealmList: RealmList? = null + var columnStringList: RealmList? = null + var columnBinaryList: RealmList? = null + var columnBooleanList: RealmList? = null + var columnLongList: RealmList? = null + var columnDoubleList: RealmList? = null + var columnFloatList: RealmList? = null + var columnDateList: RealmList? = null + var columnDecimal128List: RealmList? = null + + var columnObjectIdList: RealmList? = null + + fun setColumnMutableRealmInteger(value: Int) { + columnRealmInteger.set(value.toLong()) + } + +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncAllTypesSchema.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncAllTypesSchema.kt new file mode 100644 index 0000000000..4480d0985e --- /dev/null +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncAllTypesSchema.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.entities + +import io.realm.annotations.RealmModule + +/** + * The set of classes initially supported by MongoDB Realm. + */ +@RealmModule(classes = [SyncDog::class, SyncPerson::class, SyncAllTypes::class]) +class SyncAllTypesSchema { +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncStringOnly.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncStringOnly.kt new file mode 100644 index 0000000000..c36c0084bc --- /dev/null +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncStringOnly.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.entities + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.RealmField +import org.bson.types.ObjectId + +open class SyncStringOnly : RealmObject() { + + companion object { + const val CLASS_NAME = "StringOnly" + const val FIELD_CHARS = "chars" + } + + @PrimaryKey + @RealmField(name = "_id") + var id = ObjectId() + + var chars: String? = null + +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncStringOnlyModule.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncStringOnlyModule.kt new file mode 100644 index 0000000000..0dced1ff20 --- /dev/null +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncStringOnlyModule.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.entities + +import io.realm.annotations.RealmModule + +@RealmModule(classes = [SyncStringOnly::class]) +class SyncStringOnlyModule diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncSupportedTypes.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncSupportedTypes.kt new file mode 100644 index 0000000000..73bbe548e0 --- /dev/null +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncSupportedTypes.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.entities + +import io.realm.MutableRealmInteger +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.TestHelper +import io.realm.annotations.PrimaryKey +import io.realm.annotations.RealmField +import io.realm.annotations.Required +import org.bson.types.Decimal128 +import org.bson.types.ObjectId +import java.math.BigDecimal +import java.util.* + +open class SyncSupportedTypes : RealmObject() { + + companion object { + const val CLASS_NAME = "AllTypes" + const val FIELD_STRING = "columnString" + const val FIELD_LONG = "columnLong" + const val FIELD_FLOAT = "columnFloat" + const val FIELD_DOUBLE = "columnDouble" + const val FIELD_BOOLEAN = "columnBoolean" + const val FIELD_DATE = "columnDate" + const val FIELD_BINARY = "columnBinary" + const val FIELD_MUTABLEREALMINTEGER = "columnMutableRealmInteger" + const val FIELD_DECIMAL128 = "columnDecimal128" + const val FIELD_OBJECT_ID = "columnObjectId" + const val FIELD_REALMOBJECT = "columnRealmObject" + const val FIELD_REALMLIST = "columnRealmList" + const val FIELD_STRING_LIST = "columnStringList" + const val FIELD_BINARY_LIST = "columnBinaryList" + const val FIELD_BOOLEAN_LIST = "columnBooleanList" + const val FIELD_LONG_LIST = "columnLongList" + const val FIELD_DOUBLE_LIST = "columnDoubleList" + const val FIELD_FLOAT_LIST = "columnFloatList" + const val FIELD_DATE_LIST = "columnDateList" + val INVALID_TYPES_FIELDS_FOR_DISTINCT = arrayOf(FIELD_REALMOBJECT, FIELD_REALMLIST, FIELD_DOUBLE, FIELD_FLOAT, + FIELD_STRING_LIST, FIELD_BINARY_LIST, FIELD_BOOLEAN_LIST, FIELD_LONG_LIST, + FIELD_DOUBLE_LIST, FIELD_FLOAT_LIST, FIELD_DATE_LIST) + } + + @PrimaryKey + @RealmField(name = "_id") + var id = ObjectId() + + @Required + var columnString = "" + var columnLong: Long = 0 + var columnFloat = 0f + var isColumnBoolean = false + + @Required + var columnDate = Date(0) + + @Required + var columnBinary = ByteArray(0) + + @Required + var columnDecimal128 = Decimal128(BigDecimal.ZERO) + + @Required + var columnObjectId = ObjectId(TestHelper.randomObjectIdHexString()) + val columnRealmInteger = MutableRealmInteger.ofNull() + var columnRealmObject: SyncDog? = null + var columnRealmList: RealmList? = null + + // FIXME These are the fields needed to be removed from SyncAllTypes for sync to work when + // updating the partitionValue +// var columnDouble = 0.0 +// var columnStringList: RealmList? = null +// var columnBinaryList: RealmList? = null +// var columnBooleanList: RealmList? = null +// var columnLongList: RealmList? = null +// var columnDoubleList: RealmList? = null +// var columnFloatList: RealmList = RealmList() +// var columnDateList: RealmList? = null +// var columnDecimal128List: RealmList? = null + +// var columnObjectIdList: RealmList? = null + + fun setColumnMutableRealmInteger(value: Int) { + columnRealmInteger.set(value.toLong()) + } + +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncConfigurationTests.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncConfigurationTests.kt index 2122bff3ad..81b299e267 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncConfigurationTests.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncConfigurationTests.kt @@ -97,6 +97,7 @@ class SyncConfigurationTests { } @Test + // FIXME Tests are not exhaustive fun equals_not() { val user1: User = createTestUser(app) val user2: User = createTestUser(app) diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncExt.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncExt.kt index 41e46490c8..238dc0b16a 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncExt.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncExt.kt @@ -20,3 +20,7 @@ package io.realm.mongodb.sync fun Sync.testReset() { this.reset() } + +fun Sync.simulateClientReset(session: SyncSession) { + this.simulateClientReset(session) +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncSessionExt.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncSessionExt.kt new file mode 100644 index 0000000000..872d230255 --- /dev/null +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncSessionExt.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.mongodb.sync + +// Helper to expose package protected methods for testing purpose +fun SyncSession.testClose() { + this.close() +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/util/KotlinTestUtils.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/util/KotlinTestUtils.kt index f34cd55a1a..2305660868 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/util/KotlinTestUtils.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/util/KotlinTestUtils.kt @@ -61,6 +61,7 @@ class ResourceContainer : Closeable { @Synchronized override fun close() { resources.map { it.close() } + resources.clear() } @Synchronized diff --git a/realm/realm-library/src/main/cpp/io_realm_internal_OsRealmConfig.cpp b/realm/realm-library/src/main/cpp/io_realm_internal_OsRealmConfig.cpp index 38d3b9046a..863ff480f3 100644 --- a/realm/realm-library/src/main/cpp/io_realm_internal_OsRealmConfig.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_internal_OsRealmConfig.cpp @@ -26,6 +26,7 @@ #endif #include +#include #include "java_accessor.hpp" #include "util.hpp" @@ -332,8 +333,13 @@ JNIEXPORT jstring JNICALL Java_io_realm_internal_OsRealmConfig_nativeCreateAndSe SyncSessionStopPolicy session_stop_policy = static_cast(j_session_stop_policy); JStringAccessor realm_url(env, j_sync_realm_url); - JStringAccessor partion_key_value(env, j_partion_key_value); - config.sync_config = std::make_shared(SyncConfig{user, partion_key_value}); + // TODO Simplify. Java serialization only allows writing full documents, so the partition + // key is embedded in a document with key 'value'. To get is as string were we parse it + // and reformat with C++ bson serialization as it supports serializing single values. + Bson bson(JniBsonProtocol::jstring_to_bson(env, j_partion_key_value)); + std::stringstream buffer; + buffer << bson; + config.sync_config = std::make_shared(SyncConfig{user, buffer.str()}); config.sync_config->stop_policy = session_stop_policy; config.sync_config->error_handler = std::move(error_handler); switch (j_client_reset_mode) { diff --git a/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_SyncSession.cpp b/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_SyncSession.cpp index e16c432689..d417cd272c 100644 --- a/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_SyncSession.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_SyncSession.cpp @@ -238,7 +238,7 @@ static jlong get_connection_value(SyncSession::ConnectionState state) { return static_cast(-1); } -JNIEXPORT jlong JNICALL Java_io_realm_mongodb_sync_SyncSession_nativeAddConnectionListener(JNIEnv* env, jclass, jstring j_local_realm_path) +JNIEXPORT jlong JNICALL Java_io_realm_mongodb_sync_SyncSession_nativeAddConnectionListener(JNIEnv* env, jobject j_session_object, jstring j_local_realm_path) { try { // JNIEnv is thread confined, so we need a deep copy in order to capture the string in the lambda @@ -252,17 +252,17 @@ JNIEXPORT jlong JNICALL Java_io_realm_mongodb_sync_SyncSession_nativeAddConnecti return 0; } - static JavaClass java_syncmanager_class(env, "io/realm/mongodb/sync/Sync"); - static JavaMethod java_notify_connection_listener(env, java_syncmanager_class, "notifyConnectionListeners", "(Ljava/lang/String;JJ)V", true); + static JavaClass java_syncmanager_class(env, "io/realm/mongodb/sync/SyncSession"); + static JavaMethod java_notify_connection_listener(env, java_syncmanager_class, "notifyConnectionListeners", "(JJ)V"); - std::function callback = [local_realm_path](SyncSession::ConnectionState old_state, SyncSession::ConnectionState new_state) { + auto session_ref = env->NewGlobalRef(j_session_object); // FIXME Leaking reference to session + std::function callback = [session_ref](SyncSession::ConnectionState old_state, SyncSession::ConnectionState new_state) { JNIEnv* local_env = jni_util::JniUtils::get_env(true); jlong old_connection_value = get_connection_value(old_state); jlong new_connection_value = get_connection_value(new_state); - JavaLocalRef path(local_env, to_jstring(local_env, local_realm_path)); - local_env->CallStaticVoidMethod(java_syncmanager_class, java_notify_connection_listener, path.get(), + local_env->CallVoidMethod(session_ref, java_notify_connection_listener, old_connection_value, new_connection_value); // All exceptions will be caught on the Java side of handlers, but Errors will still end diff --git a/realm/realm-library/src/main/java/io/realm/internal/OsRealmConfig.java b/realm/realm-library/src/main/java/io/realm/internal/OsRealmConfig.java index b0750fd4e7..98a2f39902 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/OsRealmConfig.java +++ b/realm/realm-library/src/main/java/io/realm/internal/OsRealmConfig.java @@ -20,8 +20,8 @@ import java.net.ProxySelector; import java.net.URI; import java.net.URISyntaxException; -import java.util.Map; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; @@ -220,7 +220,7 @@ private OsRealmConfig(final RealmConfiguration config, //noinspection unchecked Map customHeadersMap = (Map) (syncConfigurationOptions[j++]); Byte clientResyncMode = (Byte) syncConfigurationOptions[j++]; - String partitionValue = (String) syncConfigurationOptions[j++]; + String encodedPartitionValue = (String) syncConfigurationOptions[j++]; Object syncService = syncConfigurationOptions[j++]; // Convert the headers into a String array to make it easier to send through JNI @@ -291,7 +291,7 @@ private OsRealmConfig(final RealmConfiguration config, customAuthorizationHeaderName, customHeaders, clientResyncMode, - partitionValue, + encodedPartitionValue, syncService); try { resolvedSyncRealmUrl = syncRealmAuthUrl + urlPrefix.substring(1); // FIXME diff --git a/realm/realm-library/src/objectServer/java/io/realm/internal/SyncObjectServerFacade.java b/realm/realm-library/src/objectServer/java/io/realm/internal/SyncObjectServerFacade.java index e38b35675c..3b18b3d939 100644 --- a/realm/realm-library/src/objectServer/java/io/realm/internal/SyncObjectServerFacade.java +++ b/realm/realm-library/src/objectServer/java/io/realm/internal/SyncObjectServerFacade.java @@ -30,12 +30,14 @@ import io.realm.mongodb.App; import io.realm.RealmConfiguration; +import io.realm.mongodb.AppConfiguration; import io.realm.mongodb.sync.Sync; import io.realm.mongodb.User; import io.realm.mongodb.sync.SyncConfiguration; import io.realm.exceptions.DownloadingRealmInterruptedException; import io.realm.exceptions.RealmException; import io.realm.internal.android.AndroidCapabilities; +import io.realm.internal.jni.JniBsonProtocol; import io.realm.internal.network.NetworkStateReceiver; import io.realm.internal.objectstore.OsAsyncOpenTask; @@ -87,20 +89,21 @@ public Object[] getSyncConfigurationOptions(RealmConfiguration config) { String customAuthorizationHeaderName = app.getConfiguration().getAuthorizationHeaderName(); Map customHeaders = app.getConfiguration().getCustomRequestHeaders(); - // Temporary work-around for serializing supported bson values - BsonValue val = syncConfig.getPartitionValue(); - String partitionValue = null; - if (val.isString()) { - partitionValue = "\"" + val.asString().getValue() + "\""; - } else if (val.isInt32()) { - partitionValue = "{ \"$bsonInt\" : " + val.asInt32().intValue() + " }"; - } else if (val.isInt64()) { - partitionValue = "{ \"$bsonLong\" : " + val.asInt64().longValue() + " }"; - } else if (val.isObjectId()) { - partitionValue = "{ \"$oid\" : " + val.asObjectId().toString() + " }"; - } else { - throw new IllegalArgumentException("Unsupported type: " + val); + // TODO Simplify. org.bson serialization only allows writing full documents, so the partition + // key is embedded in a document with key 'value' and unwrapped in JNI. + BsonValue partitionValue = syncConfig.getPartitionValue(); + String encodedPartitionValue; + switch (partitionValue.getBsonType()) { + case STRING: + case OBJECT_ID: + case INT32: + case INT64: + encodedPartitionValue = JniBsonProtocol.encode(partitionValue, AppConfiguration.DEFAULT_BSON_CODEC_REGISTRY); + break; + default: + throw new IllegalArgumentException("Unsupported type: " + partitionValue); } + int i = 0; Object[] configObj = new Object[SYNC_CONFIG_OPTIONS]; configObj[i++] = rosUserIdentity; @@ -114,7 +117,7 @@ public Object[] getSyncConfigurationOptions(RealmConfiguration config) { configObj[i++] = customAuthorizationHeaderName; configObj[i++] = customHeaders; configObj[i++] = OsRealmConfig.CLIENT_RESYNC_MODE_MANUAL; - configObj[i++] = partitionValue; + configObj[i++] = encodedPartitionValue; configObj[i++] = app.getSync(); return configObj; } else { diff --git a/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/Sync.java b/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/Sync.java index af346c66f9..6b28b7890e 100644 --- a/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/Sync.java +++ b/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/Sync.java @@ -259,22 +259,6 @@ private synchronized void notifyProgressListener(String localRealmPath, long lis } } - /** - * Called from native code. This method is not allowed to throw as it would be swallowed - * by the native Sync Client thread. Instead log all exceptions to logcat. - */ - @SuppressWarnings("unused") - private synchronized void notifyConnectionListeners(String localRealmPath, long oldState, long newState) { - SyncSession session = sessions.get(localRealmPath); - if (session != null) { - try { - session.notifyConnectionListeners(ConnectionState.fromNativeValue(oldState), ConnectionState.fromNativeValue(newState)); - } catch (Exception exception) { - RealmLog.error(exception); - } - } - } - /** * Realm will automatically detect when a device gets connectivity after being offline and * resume syncing. diff --git a/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncConfiguration.java b/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncConfiguration.java index a99725f9fb..df3ffde8c4 100644 --- a/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncConfiguration.java +++ b/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncConfiguration.java @@ -292,7 +292,8 @@ public boolean equals(@Nullable Object o) { if (sessionStopPolicy != that.sessionStopPolicy) return false; if (syncUrlPrefix != null ? !syncUrlPrefix.equals(that.syncUrlPrefix) : that.syncUrlPrefix != null) return false; - return clientResyncMode == that.clientResyncMode; + if (clientResyncMode != that.clientResyncMode) return false; + return partitionValue.equals(that.partitionValue); } @Override @@ -307,6 +308,7 @@ public int hashCode() { result = 31 * result + sessionStopPolicy.hashCode(); result = 31 * result + (syncUrlPrefix != null ? syncUrlPrefix.hashCode() : 0); result = 31 * result + clientResyncMode.hashCode(); + result = 31 * result + partitionValue.hashCode(); return result; } @@ -331,6 +333,8 @@ public String toString() { sb.append("syncUrlPrefix: ").append(syncUrlPrefix); sb.append("\n"); sb.append("clientResyncMode: ").append(clientResyncMode); + sb.append("\n"); + sb.append("partitionValue: ").append(partitionValue); return sb.toString(); } @@ -524,7 +528,7 @@ public Builder(User user, long partitionValue) { * synchronized to the Realm. * @see Link to docs about partions */ - private Builder(User user, BsonValue partitionValue) { + Builder(User user, BsonValue partitionValue) { Context context = Realm.getApplicationContext(); if (context == null) { throw new IllegalStateException("Call `Realm.init(Context)` before creating a SyncConfiguration"); diff --git a/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncSession.java b/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncSession.java index a16c65776d..50328a105b 100644 --- a/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncSession.java +++ b/realm/realm-library/src/objectServer/java/io/realm/mongodb/sync/SyncSession.java @@ -272,9 +272,18 @@ synchronized void notifyProgressListener(long listenerId, long transferredBytes, } } - void notifyConnectionListeners(ConnectionState oldState, ConnectionState newState) { + /** + * Called from native code. This method is not allowed to throw as it would be swallowed + * by the native Sync Client thread. Instead log all exceptions to logcat. + */ + @SuppressWarnings("unused") + void notifyConnectionListeners(long oldState, long newState) { for (ConnectionListener listener : connectionListeners) { - listener.onChange(oldState, newState); + try { + listener.onChange(ConnectionState.fromNativeValue(oldState), ConnectionState.fromNativeValue(newState)); + } catch (Exception exception) { + RealmLog.error(exception); + } } } @@ -721,7 +730,7 @@ public void throwExceptionIfNeeded() { } } - private static native long nativeAddConnectionListener(String localRealmPath); + private native long nativeAddConnectionListener(String localRealmPath); private static native void nativeRemoveConnectionListener(long listenerId, String localRealmPath); private native long nativeAddProgressListener(String localRealmPath, long listenerId, int direction, boolean isStreaming); private static native void nativeRemoveProgressListener(String localRealmPath, long listenerToken); diff --git a/realm/realm-library/src/syncIntegrationTest/java/io/realm/SyncSessionTests.java b/realm/realm-library/src/syncIntegrationTest/java/io/realm/SyncSessionTests.java deleted file mode 100644 index a03b2e30eb..0000000000 --- a/realm/realm-library/src/syncIntegrationTest/java/io/realm/SyncSessionTests.java +++ /dev/null @@ -1,661 +0,0 @@ -package io.realm; - -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.SystemClock; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import io.realm.entities.AllTypes; -import io.realm.entities.StringOnly; -import io.realm.exceptions.DownloadingRealmInterruptedException; -import io.realm.internal.OsRealmConfig; -import io.realm.objectserver.utils.Constants; -import io.realm.objectserver.utils.StringOnlyModule; -import io.realm.objectserver.utils.UserFactory; -import io.realm.rule.RunTestInLooperThread; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -@RunWith(AndroidJUnit4.class) -public class SyncSessionTests extends StandardIntegrationTest { - - @Rule - public TestSyncConfigurationFactory configFactory = new TestSyncConfigurationFactory(); - - private interface SessionCallback { - void onReady(SyncSession session); - } - - private void getSession(SessionCallback callback) { - // Work-around for a race condition happening when shutting down a Looper test and - // Resetting the SyncManager - // The problem is the `@After` block which runs as soon as the test method has completed. - // For integration tests this will attempt to reset the SyncManager which will fail - // if Realms are still open as they hold a reference to a session object. - // By moving this into a Looper callback we ensure that a looper test can shutdown as - // intended. - // Generally it seems that using calling `RunInLooperThread.testComplete()` in a synchronous - looperThread.postRunnable((Runnable) () -> { - SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - looperThread.closeAfterTest(Realm.getInstance(syncConfiguration)); - callback.onReady(SyncManager.getSession(syncConfiguration)); - }); - } - - private void getActiveSession(SessionCallback callback) { - getSession(session -> { - if (session.isConnected()) { - callback.onReady(session); - } else { - session.addConnectionChangeListener(new ConnectionListener() { - @Override - public void onChange(ConnectionState oldState, ConnectionState newState) { - if (newState == ConnectionState.CONNECTED) { - session.removeConnectionChangeListener(this); - callback.onReady(session); - } - } - }); - } - }); - } - - @Test(timeout=3000) - public void getState_active() { - SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - Realm realm = Realm.getInstance(syncConfiguration); - - SyncSession session = SyncManager.getSession(syncConfiguration); - - // make sure the `access_token` is acquired. otherwise we can still be - // in WAITING_FOR_ACCESS_TOKEN state - while(session.getState() != SyncSession.State.ACTIVE) { - SystemClock.sleep(200); - } - - realm.close(); - } - - @Test - public void getState_throwOnClosedSession() { - SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - Realm realm = Realm.getInstance(syncConfiguration); - - SyncSession session = SyncManager.getSession(syncConfiguration); - realm.close(); - user.logOut(); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Could not find session, Realm was probably closed"); - session.getState(); - } - - @Test - public void getState_loggedOut() { - SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - Realm realm = Realm.getInstance(syncConfiguration); - - SyncSession session = SyncManager.getSession(syncConfiguration); - - user.logOut(); - - SyncSession.State state = session.getState(); - assertEquals(SyncSession.State.INACTIVE, state); - - realm.close(); - } - - @Test - public void uploadDownloadAllChanges() throws InterruptedException { - SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncUser adminUser = UserFactory.createAdminUser(Constants.AUTH_URL); - SyncConfiguration userConfig = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - SyncConfiguration adminConfig = configFactory - .createSyncConfigurationBuilder(adminUser, userConfig.getServerUrl().toString()) - .build(); - - Realm userRealm = Realm.getInstance(userConfig); - userRealm.beginTransaction(); - userRealm.createObject(AllTypes.class); - userRealm.commitTransaction(); - SyncManager.getSession(userConfig).uploadAllLocalChanges(); - userRealm.close(); - - Realm adminRealm = Realm.getInstance(adminConfig); - SyncManager.getSession(adminConfig).downloadAllServerChanges(); - adminRealm.refresh(); - assertEquals(1, adminRealm.where(AllTypes.class).count()); - adminRealm.close(); - } - - @Test - public void interruptWaits() throws InterruptedException { - final SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncUser adminUser = UserFactory.createAdminUser(Constants.AUTH_URL); - final SyncConfiguration userConfig = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - final SyncConfiguration adminConfig = configFactory - .createSyncConfigurationBuilder(adminUser, userConfig.getServerUrl().toString()) - .build(); - - Thread t = new Thread(new Runnable() { - @Override - public void run() { - Realm userRealm = Realm.getInstance(userConfig); - userRealm.beginTransaction(); - userRealm.createObject(AllTypes.class); - userRealm.commitTransaction(); - SyncSession userSession = SyncManager.getSession(userConfig); - try { - // 1. Start download (which will be interrupted) - Thread.currentThread().interrupt(); - userSession.downloadAllServerChanges(); - } catch (InterruptedException ignored) { - assertFalse(Thread.currentThread().isInterrupted()); - } - try { - // 2. Upload all changes - userSession.uploadAllLocalChanges(); - } catch (InterruptedException e) { - fail("Upload interrupted"); - } - userRealm.close(); - - Realm adminRealm = Realm.getInstance(adminConfig); - SyncSession adminSession = SyncManager.getSession(adminConfig); - try { - // 3. Start upload (which will be interrupted) - Thread.currentThread().interrupt(); - adminSession.uploadAllLocalChanges(); - } catch (InterruptedException ignored) { - assertFalse(Thread.currentThread().isInterrupted()); // clear interrupted flag - } - try { - // 4. Download all changes - adminSession.downloadAllServerChanges(); - } catch (InterruptedException e) { - fail("Download interrupted"); - } - adminRealm.refresh(); - assertEquals(1, adminRealm.where(AllTypes.class).count()); - adminRealm.close(); - } - }); - t.start(); - t.join(); - } - - // check that logging out a SyncUser used by different Realm will - // affect all associated sessions. - @Test(timeout=5000) - public void logout_sameSyncUserMultipleSessions() { - String uniqueName = UUID.randomUUID().toString(); - SyncCredentials credentials = SyncCredentials.usernamePassword(uniqueName, "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - - SyncConfiguration syncConfiguration1 = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - Realm realm1 = Realm.getInstance(syncConfiguration1); - - SyncConfiguration syncConfiguration2 = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL_2) - .build(); - Realm realm2 = Realm.getInstance(syncConfiguration2); - - SyncSession session1 = SyncManager.getSession(syncConfiguration1); - SyncSession session2 = SyncManager.getSession(syncConfiguration2); - - // make sure the `access_token` is acquired. otherwise we can still be - // in WAITING_FOR_ACCESS_TOKEN state - while(session1.getState() != SyncSession.State.ACTIVE || session2.getState() != SyncSession.State.ACTIVE) { - SystemClock.sleep(200); - } - assertEquals(SyncSession.State.ACTIVE, session1.getState()); - assertEquals(SyncSession.State.ACTIVE, session2.getState()); - assertNotEquals(session1, session2); - - assertEquals(session1.getUser(), session2.getUser()); - - user.logOut(); - - assertEquals(SyncSession.State.INACTIVE, session1.getState()); - assertEquals(SyncSession.State.INACTIVE, session2.getState()); - - credentials = SyncCredentials.usernamePassword(uniqueName, "password", false); - SyncUser.logIn(credentials, Constants.AUTH_URL); - - // reviving the sessions. The state could be changed concurrently. - assertTrue(session1.getState() == SyncSession.State.WAITING_FOR_ACCESS_TOKEN || - session1.getState() == SyncSession.State.ACTIVE); - assertTrue(session2.getState() == SyncSession.State.WAITING_FOR_ACCESS_TOKEN || - session2.getState() == SyncSession.State.ACTIVE); - - realm1.close(); - realm2.close(); - } - - // A Realm that was opened before a user logged out should be able to resume uploading if the user logs back in. - @Test - public void logBackResumeUpload() throws InterruptedException { - final String uniqueName = UUID.randomUUID().toString(); - SyncCredentials credentials = SyncCredentials.usernamePassword(uniqueName, "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - - final SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .modules(new StringOnlyModule()) - .waitForInitialRemoteData() - .build(); - final Realm realm = Realm.getInstance(syncConfiguration); - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - realm.createObject(StringOnly.class).setChars("1"); - } - }); - - final SyncSession session = SyncManager.getSession(syncConfiguration); - session.uploadAllLocalChanges(); - - user.logOut(); - - // add a commit while we're still offline - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - realm.createObject(StringOnly.class).setChars("2"); - } - }); - - final CountDownLatch testCompleted = new CountDownLatch(1); - - final HandlerThread handlerThread = new HandlerThread("HandlerThread"); - handlerThread.start(); - Looper looper = handlerThread.getLooper(); - Handler handler = new Handler(looper); - AtomicReference> allResults = new AtomicReference<>();// notifier could be GC'ed before it get a chance to trigger the second commit, so declaring it outside the Runnable - handler.post(new Runnable() { - @Override - public void run() { - // access the Realm from an different path on the device (using admin user), then monitor - // when the offline commits get synchronized - SyncUser admin = UserFactory.createAdminUser(Constants.AUTH_URL); - SyncCredentials credentialsAdmin = SyncCredentials.accessToken(SyncTestUtils.getRefreshToken(admin).value(), "custom-admin-user"); - SyncUser adminUser = SyncUser.logIn(credentialsAdmin, Constants.AUTH_URL); - - SyncConfiguration adminConfig = configurationFactory.createSyncConfigurationBuilder(adminUser, syncConfiguration.getServerUrl().toString()) - .modules(new StringOnlyModule()) - .waitForInitialRemoteData() - .build(); - final Realm adminRealm = Realm.getInstance(adminConfig); - allResults.set(adminRealm.where(StringOnly.class).sort(StringOnly.FIELD_CHARS).findAll()); - RealmChangeListener> realmChangeListener = new RealmChangeListener>() { - @Override - public void onChange(RealmResults stringOnlies) { - if (stringOnlies.size() == 2) { - Assert.assertEquals("1", stringOnlies.get(0).getChars()); - Assert.assertEquals("2", stringOnlies.get(1).getChars()); - handler.post(() -> { - // Closing a Realm from inside a listener doesn't seem to remove the - // active session reference in Object Store - adminRealm.close(); - testCompleted.countDown(); - handlerThread.quitSafely(); - }); - } - } - }; - allResults.get().addChangeListener(realmChangeListener); - - // login again to re-activate the user - SyncCredentials credentials = SyncCredentials.usernamePassword(uniqueName, "password", false); - // this login will re-activate the logged out user, and resume all it's pending sessions - // the OS will trigger bindSessionWithConfig with the new refresh_token, in order to obtain - // a new access_token. - SyncUser.logIn(credentials, Constants.AUTH_URL); - } - }); - - TestHelper.awaitOrFail(testCompleted); - realm.close(); - } - - // A Realm that was opened before a user logged out should be able to resume uploading if the user logs back in. - // this test validate the behaviour of SyncSessionStopPolicy::AfterChangesUploaded - @Test - public void uploadChangesWhenRealmOutOfScope() throws InterruptedException { - final List strongRefs = new ArrayList<>(); - final String uniqueName = UUID.randomUUID().toString(); - SyncCredentials credentials = SyncCredentials.usernamePassword(uniqueName, "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - - final char[] chars = new char[1_000_000];// 2MB - Arrays.fill(chars, '.'); - final String twoMBString = new String(chars); - - final SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .sessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.AFTER_CHANGES_UPLOADED) - .modules(new StringOnlyModule()) - .build(); - Realm realm = Realm.getInstance(syncConfiguration); - - realm.beginTransaction(); - // upload 10MB - for (int i = 0; i < 5; i++) { - realm.createObject(StringOnly.class).setChars(twoMBString); - } - realm.commitTransaction(); - realm.close(); - - final CountDownLatch testCompleted = new CountDownLatch(1); - - final HandlerThread handlerThread = new HandlerThread("HandlerThread"); - handlerThread.start(); - Looper looper = handlerThread.getLooper(); - Handler handler = new Handler(looper); - handler.post(new Runnable() { - @Override - public void run() { - // using an admin user to open the Realm on different path on the device to monitor when all the uploads are done - SyncUser admin = UserFactory.createAdminUser(Constants.AUTH_URL); - - SyncConfiguration adminConfig = configurationFactory.createSyncConfigurationBuilder(admin, syncConfiguration.getServerUrl().toString()) - .modules(new StringOnlyModule()) - .build(); - final Realm adminRealm = Realm.getInstance(adminConfig); - RealmResults all = adminRealm.where(StringOnly.class).findAll(); - - if (all.size() == 5) { - adminRealm.close(); - testCompleted.countDown(); - handlerThread.quit(); - } else { - strongRefs.add(all); - OrderedRealmCollectionChangeListener> realmChangeListener = (results, changeSet) -> { - if (results.size() == 5) { - adminRealm.close(); - testCompleted.countDown(); - handlerThread.quit(); - } - }; - all.addChangeListener(realmChangeListener); - } - } - }); - - TestHelper.awaitOrFail(testCompleted, TestHelper.STANDARD_WAIT_SECS); - handlerThread.join(); - - user.logOut(); - } - - // A Realm that was opened before a user logged out should be able to resume downloading if the user logs back in. - @Test - public void downloadChangesWhenRealmOutOfScope() throws InterruptedException { - final String uniqueName = UUID.randomUUID().toString(); - SyncCredentials credentials = SyncCredentials.usernamePassword(uniqueName, "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - - final SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .modules(new StringOnlyModule()) - .build(); - Realm realm = Realm.getInstance(syncConfiguration); - - realm.beginTransaction(); - realm.createObject(StringOnly.class).setChars("1"); - realm.commitTransaction(); - - SyncSession session = SyncManager.getSession(syncConfiguration); - session.uploadAllLocalChanges(); - - // Log out the user. - user.logOut(); - - // Log the user back in. - credentials = SyncCredentials.usernamePassword(uniqueName, "password", false); - SyncUser.logIn(credentials, Constants.AUTH_URL); - - // now let the admin upload some commits - final CountDownLatch backgroundUpload = new CountDownLatch(1); - - final HandlerThread handlerThread = new HandlerThread("HandlerThread"); - handlerThread.start(); - Looper looper = handlerThread.getLooper(); - Handler handler = new Handler(looper); - handler.post(new Runnable() { - @Override - public void run() { - // using an admin user to open the Realm on different path on the device then some commits - SyncUser admin = UserFactory.createAdminUser(Constants.AUTH_URL); - SyncCredentials credentialsAdmin = SyncCredentials.accessToken(SyncTestUtils.getRefreshToken(admin).value(), "custom-admin-user"); - SyncUser adminUser = SyncUser.logIn(credentialsAdmin, Constants.AUTH_URL); - - SyncConfiguration adminConfig = configurationFactory.createSyncConfigurationBuilder(adminUser, syncConfiguration.getServerUrl().toString()) - .modules(new StringOnlyModule()) - .waitForInitialRemoteData() - .build(); - - final Realm adminRealm = Realm.getInstance(adminConfig); - adminRealm.beginTransaction(); - adminRealm.createObject(StringOnly.class).setChars("2"); - adminRealm.createObject(StringOnly.class).setChars("3"); - adminRealm.commitTransaction(); - - try { - SyncManager.getSession(adminConfig).uploadAllLocalChanges(); - } catch (InterruptedException e) { - e.printStackTrace(); - fail(e.getMessage()); - } - adminRealm.close(); - - backgroundUpload.countDown(); - handlerThread.quit(); - } - }); - - TestHelper.awaitOrFail(backgroundUpload, 60); - // Resume downloading - session.downloadAllServerChanges(); - realm.refresh();//FIXME not calling refresh will still point to the previous version of the Realm count == 1 - assertEquals(3, realm.where(StringOnly.class).count()); - realm.close(); - } - - // Check that if we manually trigger a Client Reset, then it should be possible to start - // downloading the Realm immediately after. - @Test - @RunTestInLooperThread - public void clientReset_manualTriggerAllowSessionToRestart() { - final String uniqueName = UUID.randomUUID().toString(); - SyncCredentials credentials = SyncCredentials.usernamePassword(uniqueName, "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - - final AtomicReference configRef = new AtomicReference<>(null); - final SyncConfiguration config = configFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .clientResyncMode(ClientResyncMode.MANUAL) - .directory(looperThread.getRoot()) - .errorHandler(new SyncSession.ErrorHandler() { - @Override - public void onError(SyncSession session, ObjectServerError error) { - final ClientResetRequiredError handler = (ClientResetRequiredError) error; - // Execute Client Reset - looperThread.closeTestRealms(); - handler.executeClientReset(); - - // Try to re-open Realm and download it again - looperThread.postRunnable(new Runnable() { - @Override - public void run() { - // Validate that files have been moved - assertFalse(handler.getOriginalFile().exists()); - assertTrue(handler.getBackupFile().exists()); - - SyncConfiguration config = configRef.get(); - Realm instance = Realm.getInstance(config); - looperThread.addTestRealm(instance); - try { - SyncManager.getSession(config).downloadAllServerChanges(); - looperThread.testComplete(); - } catch (InterruptedException e) { - fail(e.toString()); - } - } - }); - } - }) - .build(); - configRef.set(config); - - Realm realm = Realm.getInstance(config); - looperThread.addTestRealm(realm); - // Trigger error - SyncManager.simulateClientReset(SyncManager.getSession(config)); - } - - @Test - @RunTestInLooperThread - public void registerConnectionListener() { - getSession(session -> { - session.addConnectionChangeListener((oldState, newState) -> { - if (newState == ConnectionState.DISCONNECTED) { - // Closing a Realm inside a connection listener doesn't work: https://github.com/realm/realm-java/issues/6249 - looperThread.postRunnable(() -> looperThread.testComplete()); - } - }); - session.stop(); - }); - } - - @Test - @RunTestInLooperThread - public void removeConnectionListener() { - SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .build(); - Realm realm = Realm.getInstance(syncConfiguration); - SyncSession session = SyncManager.getSession(syncConfiguration); - ConnectionListener listener1 = (oldState, newState) -> { - if (newState == ConnectionState.DISCONNECTED) { - fail("Listener should have been removed"); - } - }; - ConnectionListener listener2 = (oldState, newState) -> { - if (newState == ConnectionState.DISCONNECTED) { - looperThread.testComplete(); - } - }; - - session.addConnectionChangeListener(listener1); - session.addConnectionChangeListener(listener2); - session.removeConnectionChangeListener(listener1); - realm.close(); - } - - @Test - @RunTestInLooperThread - public void isConnected() { - getActiveSession(session -> { - assertEquals(session.getConnectionState(), ConnectionState.CONNECTED); - assertTrue(session.isConnected()); - looperThread.testComplete(); - }); - } - - @Test - @RunTestInLooperThread - public void stopStartSession() { - getActiveSession(session -> { - assertEquals(SyncSession.State.ACTIVE, session.getState()); - session.stop(); - assertEquals(SyncSession.State.INACTIVE, session.getState()); - session.start(); - assertNotEquals(SyncSession.State.INACTIVE, session.getState()); - looperThread.testComplete(); - }); - } - - @Test - @RunTestInLooperThread - public void start_multipleTimes() { - getActiveSession(session -> { - session.start(); - assertEquals(SyncSession.State.ACTIVE, session.getState()); - session.start(); - assertEquals(SyncSession.State.ACTIVE, session.getState()); - looperThread.testComplete(); - }); - } - - - @Test - @RunTestInLooperThread - public void stop_multipleTimes() { - getSession(session -> { - session.stop(); - assertEquals(SyncSession.State.INACTIVE, session.getState()); - session.stop(); - assertEquals(SyncSession.State.INACTIVE, session.getState()); - looperThread.testComplete(); - }); - } - - @Test - @RunTestInLooperThread - public void waitForInitialRemoteData_throwsOnTimeout() { - SyncUser user = UserFactory.createUniqueUser(Constants.AUTH_URL); - SyncConfiguration syncConfiguration = configFactory - .createSyncConfigurationBuilder(user, Constants.SYNC_SERVER_URL) - .initialData(bgRealm -> { - for (int i = 0; i < 100; i++) { - bgRealm.createObject(AllTypes.class); - } - }) - .waitForInitialRemoteData(1, TimeUnit.MILLISECONDS) - .build(); - - try { - Realm.getInstance(syncConfiguration); - fail("This should have timed out"); - } catch (DownloadingRealmInterruptedException ignore) { - } - looperThread.testComplete(); - } -} diff --git a/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncSessionTests.kt b/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncSessionTests.kt index e69de29bb2..bcac480d64 100644 --- a/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncSessionTests.kt +++ b/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncSessionTests.kt @@ -0,0 +1,755 @@ +package io.realm + +import android.os.Handler +import android.os.HandlerThread +import android.os.SystemClock +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.entities.* +import io.realm.exceptions.DownloadingRealmInterruptedException +import io.realm.internal.OsRealmConfig +import io.realm.kotlin.syncSession +import io.realm.log.LogLevel +import io.realm.log.RealmLog +import io.realm.mongodb.* +import io.realm.mongodb.sync.* +import io.realm.rule.BlockingLooperThread +import io.realm.util.ResourceContainer +import io.realm.util.assertFailsWithMessage +import org.bson.BsonInt32 +import org.bson.BsonInt64 +import org.bson.BsonObjectId +import org.bson.BsonString +import org.bson.types.ObjectId +import org.hamcrest.CoreMatchers +import org.junit.* +import org.junit.Assert.* +import org.junit.runner.RunWith +import java.io.Closeable +import java.lang.Thread +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +typealias SessionCallback = (SyncSession) -> Unit + +private val SECRET_PASSWORD = "123456" + +@RunWith(AndroidJUnit4::class) +class SyncSessionTests { + + @get:Rule + private val looperThread = BlockingLooperThread() + + private lateinit var app: App + private lateinit var user: User + private lateinit var syncConfiguration: SyncConfiguration + + private val configFactory: TestSyncConfigurationFactory = TestSyncConfigurationFactory() + + private fun getSession(callback: SessionCallback) { + // Work-around for a race condition happening when shutting down a Looper test and + // Resetting the SyncManager + // The problem is the `@After` block which runs as soon as the test method has completed. + // For integration tests this will attempt to reset the SyncManager which will fail + // if Realms are still open as they hold a reference to a session object. + // By moving this into a Looper callback we ensure that a looper test can shutdown as + // intended. + // Generally it seems that using calling `RunInLooperThread.testComplete()` in a synchronous + looperThread.postRunnable(Runnable { + val user = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val syncConfiguration = configFactory + .createSyncConfigurationBuilder(user) + .build() + val realm = Realm.getInstance(syncConfiguration) + looperThread.closeAfterTest(realm) + callback(realm.syncSession) + }) + } + + private fun getActiveSession(callback: SessionCallback) { + getSession { session -> + if (session.isConnected) { + callback(session) + } else { + session.addConnectionChangeListener(object : ConnectionListener { + override fun onChange(oldState: ConnectionState, newState: ConnectionState) { + if (newState == ConnectionState.CONNECTED) { + session.removeConnectionChangeListener(this) + callback(session) + } + } + }) + } + } + } + + @Before + fun setup() { + Realm.init(InstrumentationRegistry.getInstrumentation().targetContext) + RealmLog.setLevel(LogLevel.ALL) + app = TestApp() + user = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + syncConfiguration = configFactory + // TODO We generate new partition value for each test to avoid overlaps in data. We + // could make test booting with a cleaner state by somehow flushing data between + // tests. + .createSyncConfigurationBuilder(user, BsonObjectId(ObjectId())) + .modules(DefaultSyncSchema()) + .build() + } + + @After + fun teardown() { + if (this::app.isInitialized) { + app.close() + } + RealmLog.setLevel(LogLevel.WARN) + } + + @Test + fun partitionValue_string() { + val partitionValue = "123464652" + val syncConfiguration = configFactory + .createSyncConfigurationBuilder(user, BsonString(partitionValue)) + .modules(DefaultSyncSchema()) + .build() + Realm.getInstance(syncConfiguration).use { realm -> + realm.executeTransaction { + realm.createObject(SyncDog::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + } + + @Test + fun partitionValue_int32() { + val int = 123536462 + val syncConfiguration = configFactory + .createSyncConfigurationBuilder(user, BsonInt32(int)) + .modules(DefaultSyncSchema()) + .build() + Realm.getInstance(syncConfiguration).use { realm -> + realm.executeTransaction { + realm.createObject(SyncDog::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + } + + @Test + fun partitionValue_int64() { + val long = 1243513244L + val syncConfiguration = configFactory + .createSyncConfigurationBuilder(user, BsonInt64(long)) + .modules(DefaultSyncSchema()) + .build() + Realm.getInstance(syncConfiguration).use { realm -> + realm.executeTransaction { + realm.createObject(SyncDog::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + } + + @Test + fun partitionValue_objectId() { + val objectId = ObjectId("5ecf72df02aa3c32ab6b4ce0") + val syncConfiguration = configFactory + .createSyncConfigurationBuilder(user, BsonObjectId(objectId)) + .modules(DefaultSyncSchema()) + .build() + Realm.getInstance(syncConfiguration).use { realm -> + realm.executeTransaction { + realm.createObject(SyncDog::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + } + + @Test + // FIXME Differentiate path for Realms with different partition values + @Ignore("Partition value does not generate different paths") + fun differentPathsForDifferentPartitionValues() { + val syncConfiguration1 = configFactory + .createSyncConfigurationBuilder(user, BsonString("partitionvalue1")) + .modules(DefaultSyncSchema()) + + .build() + val syncConfiguration2 = configFactory + .createSyncConfigurationBuilder(user, BsonString("partitionvalue2")) + .modules(DefaultSyncSchema()) + + .build() + Realm.getInstance(syncConfiguration1).use { realm1 -> + Realm.getInstance(syncConfiguration2).use { realm2 -> + assertNotEquals(realm1, realm2) + assertNotEquals(realm1.path, realm2.path) + } + } + } + + @Test(timeout = 3000) + fun getState_active() { + Realm.getInstance(syncConfiguration).use { realm -> + val session: SyncSession = realm.syncSession + + // make sure the `access_token` is acquired. otherwise we can still be + // in WAITING_FOR_ACCESS_TOKEN state + while (session.state != SyncSession.State.ACTIVE) { + SystemClock.sleep(200) + } + } + } + + @Test + fun getState_throwOnClosedSession() { + var session: SyncSession? = null + Realm.getInstance(syncConfiguration).use { realm -> + session = realm.syncSession + } + user.logOut() + + assertFailsWithMessage(CoreMatchers.equalTo("Could not find session, Realm was probably closed")) { + session!!.state + } + } + + @Test + fun getState_loggedOut() { + Realm.getInstance(syncConfiguration).use { realm -> + val session = realm.syncSession + user.logOut(); + assertEquals(SyncSession.State.INACTIVE, session.state); + } + } + + @Test + fun uploadDownloadAllChanges() { + Realm.getInstance(syncConfiguration).use { realm -> + realm.executeTransaction { + realm.createObject(SyncSupportedTypes::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + + // New user but same Realm as configuration has the same partition value + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2 = configFactory + .createSyncConfigurationBuilder(user2, syncConfiguration.partitionValue) + .modules(DefaultSyncSchema()) + .build() + + Realm.getInstance(config2).use { realm -> + realm.syncSession.downloadAllServerChanges() + realm.refresh() + assertEquals(1, realm.where(SyncSupportedTypes::class.java).count()) + } + } + + @Test + fun differentPartitionValue_supportedTypes() { + Realm.getInstance(syncConfiguration).use { realm -> + realm.executeTransaction { + realm.createObject(SyncSupportedTypes::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + + // New user and different partition value + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2 = configFactory + .createSyncConfigurationBuilder(user2, BsonObjectId(ObjectId())) + .modules(DefaultSyncSchema()) + .build() + + Realm.getInstance(config2).use { realm -> + realm.executeTransaction { + realm.createObject(SyncSupportedTypes::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + } + + @Test + fun differentPartitionValue_noCrosstalk() { + Realm.getInstance(syncConfiguration).use { realm -> + realm.executeTransaction { + realm.createObject(SyncSupportedTypes::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + + // New user and different partition value + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2 = configFactory + .createSyncConfigurationBuilder(user2, BsonObjectId(ObjectId())) + .modules(DefaultSyncSchema()) + .build() + + Realm.getInstance(config2).use { realm -> + realm.syncSession.downloadAllServerChanges() + // We should not have any data here + assertEquals(0, realm.where(SyncSupportedTypes::class.java).count()) + } + } + + @Test + // FIXME Investigate further + @Ignore("Bad changeset for session with different partitionValue") + fun differentPartitionValue_allTypes() { + val config = configFactory + .createSyncConfigurationBuilder(user) + .modules(SyncAllTypesSchema()) + .build() + Realm.getInstance(config).use { realm -> + realm.executeTransaction { + realm.createObject(SyncAllTypes::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + + // New user and different partition value + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2 = configFactory + .createSyncConfigurationBuilder(user2, BsonObjectId(ObjectId())) + .modules(SyncAllTypesSchema()) + .build() + + Realm.getInstance(config2).use { realm -> + realm.executeTransaction { + realm.createObject(SyncAllTypes::class.java, ObjectId()) + } + realm.syncSession.uploadAllLocalChanges() + } + } + + @Test + fun interruptWaits() { + // FIXME Convert to BackgroundLooperThread? Is it doable with all the interruptions + val t = Thread(Runnable { + Realm.getInstance(syncConfiguration).use { userRealm -> + userRealm.executeTransaction { + userRealm.createObject(SyncSupportedTypes::class.java, ObjectId()) + } + val userSession = userRealm.syncSession + try { + // 1. Start download (which will be interrupted) + Thread.currentThread().interrupt() + userSession.downloadAllServerChanges() + fail() + } catch (ignored: InterruptedException) { + assertFalse(Thread.currentThread().isInterrupted) + } + try { + // 2. Upload all changes + userSession.uploadAllLocalChanges() + } catch (e: InterruptedException) { + fail("Upload interrupted") + } + } + + // New user but same Realm as configuration has the same partition value + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2 = configFactory + .createSyncConfigurationBuilder(user2, syncConfiguration.partitionValue) + .modules(DefaultSyncSchema()) + .build() + + Realm.getInstance(config2).use { adminRealm -> + val adminSession: SyncSession = adminRealm.syncSession + try { + // 3. Start upload (which will be interrupted) + Thread.currentThread().interrupt() + adminSession.uploadAllLocalChanges() + fail() + } catch (ignored: InterruptedException) { + assertFalse(Thread.currentThread().isInterrupted) // clear interrupted flag + } + try { + // 4. Download all changes + adminSession.downloadAllServerChanges() + } catch (e: InterruptedException) { + fail("Download interrupted") + } + adminRealm.refresh() + + assertEquals(1, adminRealm.where(SyncSupportedTypes::class.java).count()) + } + }) + t.start() + t.join() + } + + // check that logging out a SyncUser used by different Realm will + // affect all associated sessions. + @Test(timeout = 5000) + // FIXME Differentiate path for Realms with different partition values, see differentPathsForDifferentPartitionValues + @Ignore("Partition value does not generate different paths") + fun logout_sameSyncUserMultipleSessions() { + Realm.getInstance(syncConfiguration).use { realm1 -> + // New partitionValue to differentiate sync session + val syncConfiguration2 = configFactory + .createSyncConfigurationBuilder(user, BsonObjectId(ObjectId())) + .modules(DefaultSyncSchema()) + .build() + + Realm.getInstance(syncConfiguration2).use { realm2 -> + val session1: SyncSession = realm1.syncSession + val session2: SyncSession = realm2.syncSession + + // make sure the `access_token` is acquired. otherwise we can still be + // in WAITING_FOR_ACCESS_TOKEN state + // FIXME Reavaluate with new sync states + while (session1.state != SyncSession.State.ACTIVE || session2.state != SyncSession.State.ACTIVE) { + SystemClock.sleep(200) + } + + assertEquals(SyncSession.State.ACTIVE, session1.state) + assertEquals(SyncSession.State.ACTIVE, session2.state) + assertNotEquals(realm1, realm2) + assertNotEquals(session1, session2) + assertEquals(session1.user, session2.user) + user.logOut() + assertEquals(SyncSession.State.INACTIVE, session1.state) + assertEquals(SyncSession.State.INACTIVE, session2.state) + + // Login again + app.login(Credentials.emailPassword(user.email!!, SECRET_PASSWORD)) + + // reviving the sessions. The state could be changed concurrently. + // FIXME Reavaluate with new sync states + assertTrue( + //session1.state == SyncSession.State.WAITING_FOR_ACCESS_TOKEN || + session1.state == SyncSession.State.ACTIVE) + assertTrue( + //session2.state == SyncSession.State.WAITING_FOR_ACCESS_TOKEN || + session2.state == SyncSession.State.ACTIVE) + } + } + } + + // A Realm that was opened before a user logged out should be able to resume uploading if the user logs back in. + @Test + // FIXME Investigate further + // FIXME Rewrite to use BlockingLooperThread + @Ignore("Re-logging in does not authorize") + fun logBackResumeUpload() { + val config1 = configFactory + .createSyncConfigurationBuilder(user) + .modules(SyncStringOnlyModule()) + .waitForInitialRemoteData() + .build() + Realm.getInstance(config1).use { realm1 -> + realm1.executeTransaction { realm -> realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "1" } + val session1: SyncSession = realm1.syncSession + session1.uploadAllLocalChanges() + user.logOut() + + // add a commit while we're still offline + realm1.executeTransaction { realm -> realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "2" } + val testCompleted = CountDownLatch(1) + val handlerThread = HandlerThread("HandlerThread") + handlerThread.start() + val looper = handlerThread.looper + val handler = Handler(looper) + val allResults = AtomicReference>() // notifier could be GC'ed before it get a chance to trigger the second commit, so declaring it outside the Runnable + handler.post { // access the Realm from an different path on the device (using admin user), then monitor + // when the offline commits get synchronized + // FIXME Do we somehow need to extract the refreshtoken...and could it be the reason for app.login not working later on + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2: SyncConfiguration = configFactory.createSyncConfigurationBuilder(user2, config1.partitionValue) + .modules(SyncStringOnlyModule()) + .waitForInitialRemoteData() + .build() + val realm2 = Realm.getInstance(config2) + + allResults.set(realm2.where(SyncStringOnly::class.java).sort(SyncStringOnly.FIELD_CHARS).findAll()) + val realmChangeListener: RealmChangeListener> = object : RealmChangeListener> { + override fun onChange(stringOnlies: RealmResults) { + if (stringOnlies.size == 2) { + assertEquals("1", stringOnlies[0]!!.chars) + assertEquals("2", stringOnlies[1]!!.chars) + handler.post { + + // Closing a Realm from inside a listener doesn't seem to remove the + // active session reference in Object Store + realm2.close() + testCompleted.countDown() + handlerThread.quitSafely() + } + } + } + } + allResults.get().addChangeListener(realmChangeListener) + + // login again to re-activate the user + val credentials = Credentials.emailPassword(user.email!!, SECRET_PASSWORD) + // this login will re-activate the logged out user, and resume all it's pending sessions + // the OS will trigger bindSessionWithConfig with the new refresh_token, in order to obtain + // a new access_token. + app.login(credentials) + } + TestHelper.awaitOrFail(testCompleted) + } + } + + // A Realm that was opened before a user logged out should be able to resume uploading if the user logs back in. + // this test validate the behaviour of SyncSessionStopPolicy::AfterChangesUploaded + @Test + // FIXME Investigate why it does not terminate...probably rewrite to BlockingLooperThread + @Ignore("Does not terminate") + fun uploadChangesWhenRealmOutOfScope() { + val strongRefs: MutableList = ArrayList() + val chars = CharArray(1000000) // 2MB + Arrays.fill(chars, '.') + val twoMBString = String(chars) + val config1 = configFactory + .createSyncConfigurationBuilder(user) + .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.AFTER_CHANGES_UPLOADED) + .modules(SyncStringOnlyModule()) + .build() + Realm.getInstance(config1).use { realm -> + realm.executeTransaction { + // upload 10MB + for (i in 0..4) { + realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = twoMBString + } + } + } + + val testCompleted = CountDownLatch(1) + val handlerThread = HandlerThread("HandlerThread") + handlerThread.start() + val looper = handlerThread.looper + val handler = Handler(looper) + handler.post { // using an other user to open the Realm on different path on the device to monitor when all the uploads are done + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2: SyncConfiguration = configFactory.createSyncConfigurationBuilder(user2, config1.partitionValue) + .modules(SyncStringOnlyModule()) + .build() + Realm.getInstance(config2).use { realm2 -> + val all = realm2.where(SyncStringOnly::class.java).findAll() + if (all.size == 5) { + realm2.close() + testCompleted.countDown() + handlerThread.quit() + } else { + strongRefs.add(all) + val realmChangeListener = OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet? -> + if (results.size == 5) { + realm2.close() + testCompleted.countDown() + handlerThread.quit() + } + } + all.addChangeListener(realmChangeListener) + } + } + handlerThread.quit() + } + TestHelper.awaitOrFail(testCompleted, TestHelper.STANDARD_WAIT_SECS) + handlerThread.join() + user.logOut() + } + + // A Realm that was opened before a user logged out should be able to resume downloading if the user logs back in. + @Test + // FIXME Investigate why it does not terminate...probably rewrite to BlockingLooperThread + @Ignore("Does not terminate") + fun downloadChangesWhenRealmOutOfScope() { + val uniqueName = UUID.randomUUID().toString() + var credentials = app.emailPasswordAuth.registerUser(uniqueName, "password") + val config1 = configFactory + .createSyncConfigurationBuilder(user) + .modules(SyncStringOnlyModule()) + .build() + Realm.getInstance(config1).use { realm -> + realm.executeTransaction { + realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "1" + } + val session: SyncSession = realm.syncSession + session.uploadAllLocalChanges() + + // Log out the user. + user.logOut() + + // Log the user back in. + val credentials = Credentials.emailPassword(user.email!!, SECRET_PASSWORD) + app.login(credentials) + + // now let the admin upload some commits + val backgroundUpload = CountDownLatch(1) + val handlerThread = HandlerThread("HandlerThread") + handlerThread.start() + val looper = handlerThread.looper + val handler = Handler(looper) + handler.post { // using an admin user to open the Realm on different path on the device then some commits + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config2: SyncConfiguration = configFactory.createSyncConfigurationBuilder(user2, config1.partitionValue) + .modules(SyncStringOnlyModule()) + .waitForInitialRemoteData() + .build() + Realm.getInstance(config2).use { realm2 -> + realm2.executeTransaction { + realm2.createObject(SyncStringOnly::class.java, ObjectId()).chars = "2" + realm2.createObject(SyncStringOnly::class.java, ObjectId()).chars = "3" + } + realm2.syncSession.uploadAllLocalChanges() + } + backgroundUpload.countDown() + handlerThread.quit() + } + TestHelper.awaitOrFail(backgroundUpload, 60) + // Resume downloading + session.downloadAllServerChanges() + realm.refresh() //FIXME not calling refresh will still point to the previous version of the Realm count == 1 + assertEquals(3, realm.where(SyncStringOnly::class.java).count()) + } + } + + // Check that if we manually trigger a Client Reset, then it should be possible to start + // downloading the Realm immediately after. + @Test + // TODO Seems to align with tests in SessionTests, should we move them to same location + fun clientReset_manualTriggerAllowSessionToRestart() = looperThread.runBlocking { + val resources = ResourceContainer() + + val configRef = AtomicReference(null) + val config: SyncConfiguration = configFactory.createSyncConfigurationBuilder(user) + // ClientResyncMode is currently hidden, but MANUAL is the default + // .clientResyncMode(ClientResyncMode.MANUAL) + // FIXME Is this critical for the test + //.directory(looperThread.getRoot()) + .errorHandler { session, error -> + val handler = error as ClientResetRequiredError + // Execute Client Reset + resources.close() + handler.executeClientReset() + + // Try to re-open Realm and download it again + looperThread.postRunnable(Runnable { // Validate that files have been moved + assertFalse(handler.originalFile.exists()) + assertTrue(handler.backupFile.exists()) + val config = configRef.get() + Realm.getInstance(config!!).use { realm -> + realm.syncSession.downloadAllServerChanges() + looperThread.testComplete() + } + }) + } + .build() + configRef.set(config) + val realm = Realm.getInstance(config) + resources.add(realm) + // Trigger error + user.app.sync.simulateClientReset(realm.syncSession) + } + + @Test + fun registerConnectionListener() = looperThread.runBlocking { + getSession { session: SyncSession -> + session.addConnectionChangeListener { oldState: ConnectionState?, newState: ConnectionState -> + if (newState == ConnectionState.DISCONNECTED) { + // Closing a Realm inside a connection listener doesn't work: https://github.com/realm/realm-java/issues/6249 + looperThread.postRunnable(Runnable { looperThread.testComplete() }) + } + } + session.stop() + } + } + + @Test + fun removeConnectionListener() = looperThread.runBlocking { + Realm.getInstance(syncConfiguration).use { realm -> + val session: SyncSession = realm.syncSession + val listener1 = ConnectionListener { oldState: ConnectionState?, newState: ConnectionState -> + if (newState == ConnectionState.DISCONNECTED) { + fail("Listener should have been removed") + } + } + var listener2 = object : ConnectionListener { + override fun onChange(oldState: ConnectionState, newState: ConnectionState) { + if (newState == ConnectionState.DISCONNECTED) { + looperThread.testComplete() + } + } + } + session.addConnectionChangeListener(listener1) + session.addConnectionChangeListener(listener2) + session.removeConnectionChangeListener(listener1) + } + } + + @Test + fun getIsConnected() = looperThread.runBlocking { + getActiveSession { session: SyncSession -> + assertEquals(session.connectionState, ConnectionState.CONNECTED) + assertTrue(session.isConnected) + looperThread.testComplete() + } + } + + @Test + fun stopStartSession() = looperThread.runBlocking { + getActiveSession { session: SyncSession -> + assertEquals(SyncSession.State.ACTIVE, session.state) + session.stop() + assertEquals(SyncSession.State.INACTIVE, session.state) + session.start() + assertNotEquals(SyncSession.State.INACTIVE, session.state) + looperThread.testComplete() + } + } + + @Test + fun start_multipleTimes() = looperThread.runBlocking { + getActiveSession { session -> + session.start() + assertEquals(SyncSession.State.ACTIVE, session.state) + session.start() + assertEquals(SyncSession.State.ACTIVE, session.state) + looperThread.testComplete() + } + } + + @Test + fun stop_multipleTimes() = looperThread.runBlocking { + getActiveSession { session -> + session.stop() + assertEquals(SyncSession.State.INACTIVE, session.state) + session.stop() + assertEquals(SyncSession.State.INACTIVE, session.state) + looperThread.testComplete() + } + } + + @Test + // FIXME Investigate + @Ignore("Asserts with no_session when tearing down, meaning that all session are not " + + "closed, but realm seems to be closed, so further investigation is needed") + fun waitForInitialRemoteData_throwsOnTimeout() = looperThread.runBlocking { + val syncConfiguration = configFactory + .createSyncConfigurationBuilder(user) + .modules(DefaultSyncSchema()) + .initialData { bgRealm: Realm -> + for (i in 0..99) { + bgRealm.createObject(SyncAllTypes::class.java, ObjectId()) + } + } + .waitForInitialRemoteData(1, TimeUnit.MILLISECONDS) + .build() + assertFailsWith { + val instance = Realm.getInstance(syncConfiguration) + looperThread.closeAfterTest(Closeable { + instance.syncSession.testClose() + instance.close() + }) + } + looperThread.testComplete() + } + +} diff --git a/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestSyncConfigurationFactory.kt b/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestSyncConfigurationFactory.kt index b51883fe4b..1b03689832 100644 --- a/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestSyncConfigurationFactory.kt +++ b/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestSyncConfigurationFactory.kt @@ -17,7 +17,11 @@ package io.realm import io.realm.internal.OsRealmConfig import io.realm.mongodb.User +import io.realm.mongodb.sync.Builder import io.realm.mongodb.sync.SyncConfiguration +import io.realm.mongodb.sync.SyncConfigurationExt +import org.bson.BsonValue; + import io.realm.mongodb.sync.testSessionStopPolicy import io.realm.rule.TestRealmConfigurationFactory @@ -30,4 +34,9 @@ class TestSyncConfigurationFactory : TestRealmConfigurationFactory() { return SyncConfiguration.Builder(user, "default") .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) } + + fun createSyncConfigurationBuilder(user: User, partitionValue: BsonValue): SyncConfiguration.Builder { + return SyncConfigurationExt.Builder(user, partitionValue) + .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY); + } } diff --git a/realm/realm-library/src/syncTestUtils/kotlin/io/realm/mongodb/sync/SyncConfigurationExt.kt b/realm/realm-library/src/syncTestUtils/kotlin/io/realm/mongodb/sync/SyncConfigurationExt.kt index 1a4898c74b..ceeeb7b785 100644 --- a/realm/realm-library/src/syncTestUtils/kotlin/io/realm/mongodb/sync/SyncConfigurationExt.kt +++ b/realm/realm-library/src/syncTestUtils/kotlin/io/realm/mongodb/sync/SyncConfigurationExt.kt @@ -18,11 +18,24 @@ package io.realm.mongodb.sync import io.realm.RealmModel import io.realm.internal.OsRealmConfig +import io.realm.mongodb.User +import org.bson.BsonValue +class SyncConfigurationExt { + companion object +} + +// Added to expose Builder(User, BsonValue) outside io.realm.mongodb.sync package for test +fun SyncConfigurationExt.Companion.Builder(user: User, partitionValue: BsonValue): SyncConfiguration.Builder { + return SyncConfiguration.Builder(user, partitionValue) +} + +// Added to expose schema outside io.realm.mongodb.sync package for test fun SyncConfiguration.Builder.testSchema(firstClass: Class, vararg x: Class ) : SyncConfiguration.Builder { return this.schema(firstClass, *x) } +// Added to expose sesssionStopPolicy outside io.realm.mongodb.sync package for test fun SyncConfiguration.Builder.testSessionStopPolicy(policy: OsRealmConfig.SyncSessionStopPolicy): SyncConfiguration.Builder { return this.sessionStopPolicy(policy) }