diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppConfigurationTests.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppConfigurationTests.kt index 578f4d518c..035076f33c 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppConfigurationTests.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppConfigurationTests.kt @@ -18,9 +18,14 @@ package io.realm import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import io.realm.internal.network.LoggingInterceptor.LOGIN_FEATURE -import io.realm.mongodb.AppConfiguration +import io.realm.log.LogLevel +import io.realm.log.RealmLog +import io.realm.log.RealmLogger +import io.realm.mongodb.* import io.realm.mongodb.log.obfuscator.HttpLogObfuscator import io.realm.mongodb.sync.SyncSession +import io.realm.rule.BlockingLooperThread +import io.realm.util.assertFailsWithErrorCode import org.bson.codecs.StringCodec import org.bson.codecs.configuration.CodecRegistries import org.junit.Assert.* @@ -31,13 +36,22 @@ import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import java.io.File import java.net.URL +import java.util.* import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.collections.LinkedHashMap import kotlin.test.assertFailsWith import kotlin.test.assertNull +private const val CUSTOM_HEADER_NAME = "Foo" +private const val CUSTOM_HEADER_VALUE = "bar" +private const val AUTH_HEADER_NAME = "RealmAuth" + @RunWith(AndroidJUnit4::class) class AppConfigurationTests { + val looperThread = BlockingLooperThread() + @get:Rule val tempFolder = TemporaryFolder() @@ -300,4 +314,48 @@ class AppConfigurationTests { val defaultHttpLogObfuscator = HttpLogObfuscator(LOGIN_FEATURE, AppConfiguration.loginObfuscators) assertEquals(defaultHttpLogObfuscator, config.httpLogObfuscator) } + // Check that custom headers and auth header renames are correctly used for HTTP requests + // performed from Java. + @Test + fun javaRequestCustomHeaders() { + var app: App? = null + try { + looperThread.runBlocking { + app = TestApp(builder = { builder -> + builder.addCustomRequestHeader(CUSTOM_HEADER_NAME, CUSTOM_HEADER_VALUE) + builder.authorizationHeaderName(AUTH_HEADER_NAME) + }) + runJavaRequestCustomHeadersTest(app!!) + } + } finally { + app?.close() + } + } + + private fun runJavaRequestCustomHeadersTest(app: App) { + val username = UUID.randomUUID().toString() + val password = "password" + val headerSet = AtomicBoolean(false) + + // Setup logger to inspect that we get a log message with the custom headers + val level = RealmLog.getLevel() + RealmLog.setLevel(LogLevel.ALL) + val logger = RealmLogger { level: Int, tag: String?, throwable: Throwable?, message: String? -> + if (level > LogLevel.TRACE && message!!.contains(CUSTOM_HEADER_NAME) && message.contains(CUSTOM_HEADER_VALUE) + && message.contains("RealmAuth: ")) { + headerSet.set(true) + } + } + RealmLog.add(logger) + assertFailsWithErrorCode(ErrorCode.SERVICE_UNKNOWN) { + app.registerUserAndLogin(username, password) + } + RealmLog.remove(logger) + RealmLog.setLevel(level) + + assertTrue(headerSet.get()) + looperThread.testComplete() + } + + } diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppTests.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppTests.kt index 145cdd4cce..da226cc63d 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppTests.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/AppTests.kt @@ -277,7 +277,7 @@ class AppTests { // Setup an App instance with a random encryption key Realm.init(context) - app = TestApp(customizeConfig = { + app = TestApp(builder = { it.encryptionKey(TestHelper.getRandomKey()) }) diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/ProgressListenerTests.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/ProgressListenerTests.kt index 9dea7c506e..e12d281e43 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/ProgressListenerTests.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/ProgressListenerTests.kt @@ -19,6 +19,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import io.realm.entities.DefaultSyncSchema import io.realm.entities.SyncDog +import io.realm.entities.SyncStringOnly +import io.realm.internal.OsRealmConfig import io.realm.kotlin.syncSession import io.realm.kotlin.where import io.realm.log.LogLevel @@ -27,16 +29,21 @@ import io.realm.mongodb.User import io.realm.mongodb.close import io.realm.mongodb.registerUserAndLogin import io.realm.mongodb.sync.* +import io.realm.rule.BlockingLooperThread +import org.bson.types.ObjectId import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +import java.lang.IllegalStateException import java.util.* import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertFailsWith @RunWith(AndroidJUnit4::class) class ProgressListenerTests { @@ -45,6 +52,9 @@ class ProgressListenerTests { private const val TEST_SIZE: Long = 10 } + private val looperThread = BlockingLooperThread() + private val configurationFactory = TestSyncConfigurationFactory() + private lateinit var app: TestApp private lateinit var partitionValue: String @@ -290,6 +300,83 @@ class ProgressListenerTests { } } + @Test + @Ignore("FIXME: Flacky: Tracked by https://github.com/realm/realm-java/issues/6976") + fun progressListenersWorkWhenUsingWaitForInitialRemoteData() = looperThread.runBlocking { + val username = UUID.randomUUID().toString() + val password = "password" + var user: User = app.registerUserAndLogin(username, password) + + // 1. Copy a valid Realm to the server (and pray it does it within 10 seconds) + val configOld: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .testSchema(SyncStringOnly::class.java) + .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) + .build() + Realm.getInstance(configOld).use { realm -> + realm.executeTransaction { realm -> + for (i in 0..9) { + realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "Foo$i" + } + } + realm.syncSession.uploadAllLocalChanges() + } + user.logOut() + + assertFailsWith { + app.sync.getSession(configOld) + } + + // 2. Local state should now be completely reset. Open the same sync Realm but different local name again with + // a new configuration which should download the uploaded changes (pray it managed to do so within the time frame). + // Use different user to trigger different path + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), password) + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user2, user.id) + .testSchema(SyncStringOnly::class.java) + .waitForInitialRemoteData() + .build() + + assertFalse(config.testRealmExists()) + + val countDownLatch = CountDownLatch(2) + + val indefiniteListenerComplete = AtomicBoolean(false) + val currentChangesListenerComplete = AtomicBoolean(false) + val task = Realm.getInstanceAsync(config, object : Realm.Callback() { + override fun onSuccess(realm: Realm) { + realm.syncSession.addDownloadProgressListener(ProgressMode.INDEFINITELY, object : ProgressListener { + override fun onChange(progress: Progress) { + if (progress.isTransferComplete()) { + indefiniteListenerComplete.set(true) + countDownLatch.countDown() + } + } + }) + realm.syncSession.addDownloadProgressListener(ProgressMode.CURRENT_CHANGES, object : ProgressListener { + override fun onChange(progress: Progress) { + if (progress.isTransferComplete()) { + currentChangesListenerComplete.set(true) + countDownLatch.countDown() + } + } + }) + countDownLatch.await(100, TimeUnit.SECONDS) + realm.close() + if (!indefiniteListenerComplete.get()) { + fail("Indefinite progress listener did not report complete.") + } + if (!currentChangesListenerComplete.get()) { + fail("Current changes progress listener did not report complete.") + } + looperThread.testComplete() + } + + override fun onError(exception: Throwable) { + fail(exception.toString()) + } + }) + looperThread.keepStrongReference(task) + } + @Test fun uploadListener_keepIncreasingInSize() { val config = createSyncConfig() diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncSchemeMigration.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncSchemeMigration.kt new file mode 100644 index 0000000000..018288a99c --- /dev/null +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/entities/SyncSchemeMigration.kt @@ -0,0 +1,33 @@ +/* + * 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 SyncSchemeMigration : RealmObject() { + + companion object { + const val CLASS_NAME = "SyncSchemeMigration" + } + + @PrimaryKey + @RealmField(name = "_id") + var id = ObjectId() + +} 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 index 872d230255..399a9f9a6a 100644 --- 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 @@ -20,3 +20,7 @@ package io.realm.mongodb.sync fun SyncSession.testClose() { this.close() } + +fun SyncSession.testShutdownAndWait() { + this.shutdownAndWait() +} 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 e814e8ebc3..71892e677e 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 @@ -323,3 +323,14 @@ JNIEXPORT void JNICALL Java_io_realm_mongodb_sync_SyncSession_nativeStop(JNIEnv* } CATCH_STD() } + +JNIEXPORT void JNICALL Java_io_realm_mongodb_sync_SyncSession_nativeShutdownAndWait (JNIEnv* env, jclass, jstring j_local_realm_path) { + try { + JStringAccessor local_realm_path(env, j_local_realm_path); + auto session = SyncManager::shared().get_existing_session(local_realm_path); + if (session) { + session->shutdown_and_wait(); + } + } + CATCH_STD() +} diff --git a/realm/realm-library/src/main/java/io/realm/RealmConfiguration.java b/realm/realm-library/src/main/java/io/realm/RealmConfiguration.java index ea0379494e..1ece2260e2 100644 --- a/realm/realm-library/src/main/java/io/realm/RealmConfiguration.java +++ b/realm/realm-library/src/main/java/io/realm/RealmConfiguration.java @@ -243,7 +243,7 @@ public String getPath() { * * @return {@code true} if the Realm file exists, {@code false} otherwise. */ - boolean realmExists() { + protected boolean realmExists() { return new File(canonicalPath).exists(); } diff --git a/realm/realm-library/src/objectServer/java/io/realm/internal/network/OkHttpNetworkTransport.java b/realm/realm-library/src/objectServer/java/io/realm/internal/network/OkHttpNetworkTransport.java index 370ea7db46..8494dbad6d 100644 --- a/realm/realm-library/src/objectServer/java/io/realm/internal/network/OkHttpNetworkTransport.java +++ b/realm/realm-library/src/objectServer/java/io/realm/internal/network/OkHttpNetworkTransport.java @@ -8,8 +8,8 @@ import javax.annotation.Nullable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import io.realm.mongodb.log.obfuscator.HttpLogObfuscator; import io.realm.internal.objectstore.OsJavaNetworkTransport; +import io.realm.mongodb.AppConfiguration; import io.realm.mongodb.AppException; import io.realm.mongodb.ErrorCode; import io.realm.mongodb.log.obfuscator.HttpLogObfuscator; @@ -38,6 +38,22 @@ public OkHttpNetworkTransport(@Nullable HttpLogObfuscator httpLogObfuscator) { private okhttp3.Request makeRequest(String method, String url, Map headers, String body){ okhttp3.Request.Builder builder = new okhttp3.Request.Builder().url(url); + // TODO Ensure that we have correct custom headers until OS handles it + // first of all add all custom headers + for (Map.Entry entry : getCustomRequestHeaders().entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } + // and then replace default authorization header with custom one if present + String authorizationHeaderValue = headers.get(AppConfiguration.DEFAULT_AUTHORIZATION_HEADER_NAME); + String authorizationHeaderName = getAuthorizationHeaderName(); + if (authorizationHeaderValue != null && !AppConfiguration.DEFAULT_AUTHORIZATION_HEADER_NAME.equals(authorizationHeaderName)) { + headers.remove(AppConfiguration.DEFAULT_AUTHORIZATION_HEADER_NAME); + headers.put(authorizationHeaderName, authorizationHeaderValue); + } + + for (Map.Entry entry : headers.entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } switch (method) { case "get": builder.get(); 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 1f21bfa9e9..2bbeb42baa 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 @@ -432,6 +432,11 @@ public BsonValue getPartitionValue() { return partitionValue; } + @Override + protected boolean realmExists() { + return super.realmExists(); + } + /** * Builder used to construct instances of a SyncConfiguration in a fluent manner. */ 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 4e1e66c3ad..695edeb8aa 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 @@ -635,6 +635,10 @@ private void checkTimeout(long timeout, TimeUnit unit) { } } + void shutdownAndWait() { + nativeShutdownAndWait(configuration.getPath()); + } + /** * Interface used to report any session errors. * @@ -758,4 +762,5 @@ public void throwExceptionIfNeeded() { private static native byte nativeGetConnectionState(String localRealmPath); private static native void nativeStart(String localRealmPath); private static native void nativeStop(String localRealmPath); + private static native void nativeShutdownAndWait(String localRealmPath); } diff --git a/realm/realm-library/src/syncIntegrationTest/java/io/realm/SyncedRealmIntegrationTests.java b/realm/realm-library/src/syncIntegrationTest/java/io/realm/SyncedRealmIntegrationTests.java deleted file mode 100644 index 13bed56a5f..0000000000 --- a/realm/realm-library/src/syncIntegrationTest/java/io/realm/SyncedRealmIntegrationTests.java +++ /dev/null @@ -1,546 +0,0 @@ -/* - * Copyright 2017 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; - -import android.os.SystemClock; -import androidx.test.annotation.UiThreadTest; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; - -import io.realm.entities.AllTypes; -import io.realm.entities.StringOnly; -import io.realm.exceptions.DownloadingRealmInterruptedException; -import io.realm.exceptions.RealmMigrationNeededException; -import io.realm.internal.OsRealmConfig; -import io.realm.log.LogLevel; -import io.realm.log.RealmLog; -import io.realm.log.RealmLogger; -import io.realm.objectserver.utils.Constants; -import io.realm.rule.RunTestInLooperThread; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - - -/** - * Catch all class for tests that not naturally fit anywhere else. - */ -@RunWith(AndroidJUnit4.class) -public class SyncedRealmIntegrationTests extends StandardIntegrationTest { - - @Test - @RunTestInLooperThread - public void loginLogoutResumeSyncing() throws InterruptedException { - String username = UUID.randomUUID().toString(); - String password = "password"; - SyncUser user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password, true), Constants.AUTH_URL); - - SyncConfiguration config = user.createConfiguration(Constants.USER_REALM) - .schema(StringOnly.class) - .sessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) - .build(); - - Realm realm = Realm.getInstance(config); - realm.beginTransaction(); - realm.createObject(StringOnly.class).setChars("Foo"); - realm.commitTransaction(); - SyncManager.getSession(config).uploadAllLocalChanges(); - user.logOut(); - realm.close(); - try { - assertTrue(Realm.deleteRealm(config)); - } catch (IllegalStateException e) { - // FIXME: We don't have a way to ensure that the Realm instance on client thread has been - // closed for now https://github.com/realm/realm-java/issues/5416 - if (e.getMessage().contains("It's not allowed to delete the file")) { - // retry after 1 second - SystemClock.sleep(1000); - assertTrue(Realm.deleteRealm(config)); - } - } - - user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password, false), Constants.AUTH_URL); - SyncConfiguration config2 = user.createConfiguration(Constants.USER_REALM) - .schema(StringOnly.class) - .build(); - - Realm realm2 = Realm.getInstance(config2); - SyncManager.getSession(config2).downloadAllServerChanges(); - realm2.refresh(); - assertEquals(1, realm2.where(StringOnly.class).count()); - realm2.close(); - looperThread.testComplete(); - } - - @Test - @UiThreadTest - public void waitForInitialRemoteData_mainThreadThrows() { - final SyncUser user = SyncTestUtils.createTestUser(Constants.AUTH_URL); - SyncConfiguration config = user.createConfiguration(Constants.USER_REALM) - .waitForInitialRemoteData() - .build(); - - Realm realm = null; - try { - realm = Realm.getInstance(config); - fail(); - } catch (IllegalStateException ignore) { - } finally { - if (realm != null) { - realm.close(); - } - } - } - - @Test - public void waitForInitialRemoteData() throws InterruptedException { - String username = UUID.randomUUID().toString(); - String password = "password"; - SyncUser user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password, true), Constants.AUTH_URL); - - // 1. Copy a valid Realm to the server (and pray it does it within 10 seconds) - final SyncConfiguration configOld = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .schema(StringOnly.class) - .sessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) - .build(); - Realm realm = Realm.getInstance(configOld); - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - for (int i = 0; i < 10; i++) { - realm.createObject(StringOnly.class).setChars("Foo" + i); - } - } - }); - SyncManager.getSession(configOld).uploadAllLocalChanges(); - realm.close(); - user.logOut(); - - // 2. Local state should now be completely reset. Open the same sync Realm but different local name again with - // a new configuration which should download the uploaded changes (pray it managed to do so within the time frame). - user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password), Constants.AUTH_URL); - SyncConfiguration config = user.createConfiguration(Constants.USER_REALM) - .name("newRealm") - .schema(StringOnly.class) - .waitForInitialRemoteData() - .build(); - - realm = Realm.getInstance(config); - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - for (int i = 0; i < 10; i++) { - realm.createObject(StringOnly.class).setChars("Foo 1" + i); - } - } - }); - try { - assertEquals(20, realm.where(StringOnly.class).count()); - } finally { - realm.close(); - } - } - - // This tests will start and cancel getting a Realm 10 times. The Realm should be resilient towards that - // We cannot do much better since we cannot control the order of events internally in Realm which would be - // needed to correctly test all error paths. - @Test - @Ignore("Sync somehow keeps a Realm alive, causing the Realm.deleteRealm to throw " + - " https://github.com/realm/realm-java/issues/5416") - public void waitForInitialData_resilientInCaseOfRetries() throws InterruptedException { - SyncCredentials credentials = SyncCredentials.usernamePassword(UUID.randomUUID().toString(), "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - final SyncConfiguration config = user.createConfiguration(Constants.USER_REALM) - .waitForInitialRemoteData() - .build(); - - for (int i = 0; i < 10; i++) { - Thread t = new Thread(new Runnable() { - @Override - public void run() { - Realm realm = null; - try { - // This will cause the download latch called later to immediately throw an InterruptedException. - Thread.currentThread().interrupt(); - realm = Realm.getInstance(config); - } catch (DownloadingRealmInterruptedException ignored) { - assertFalse(new File(config.getPath()).exists()); - } finally { - if (realm != null) { - realm.close(); - Realm.deleteRealm(config); - } - } - } - }); - t.start(); - t.join(); - } - } - - // This tests will start and cancel getting a Realm 10 times. The Realm should be resilient towards that - // We cannot do much better since we cannot control the order of events internally in Realm which would be - // needed to correctly test all error paths. - @Test - @RunTestInLooperThread - @Ignore("See https://github.com/realm/realm-java/issues/5373") - public void waitForInitialData_resilientInCaseOfRetriesAsync() { - SyncCredentials credentials = SyncCredentials.usernamePassword(UUID.randomUUID().toString(), "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - final SyncConfiguration config = user.createConfiguration(Constants.USER_REALM) - .sessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) - .directory(configurationFactory.getRoot()) - .waitForInitialRemoteData() - .build(); - Random randomizer = new Random(); - - for (int i = 0; i < 10; i++) { - RealmAsyncTask task = Realm.getInstanceAsync(config, new Realm.Callback() { - @Override - public void onSuccess(Realm realm) { - fail(); - } - - @Override - public void onError(Throwable exception) { - fail(exception.toString()); - } - }); - SystemClock.sleep(randomizer.nextInt(5)); - task.cancel(); - } - looperThread.testComplete(); - } - - @Test - public void waitForInitialRemoteData_readOnlyTrue() throws InterruptedException { - String username = UUID.randomUUID().toString(); - String password = "password"; - SyncUser user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password, true), Constants.AUTH_URL); - - // 1. Copy a valid Realm to the server (and pray it does it within 10 seconds) - final SyncConfiguration configOld = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .schema(StringOnly.class) - .build(); - Realm realm = Realm.getInstance(configOld); - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - for (int i = 0; i < 10; i++) { - realm.createObject(StringOnly.class).setChars("Foo" + i); - } - } - }); - SyncManager.getSession(configOld).uploadAllLocalChanges(); - realm.close(); - user.logOut(); - - // 2. Local state should now be completely reset. Open the Realm again with a new configuration which should - // download the uploaded changes (pray it managed to do so within the time frame). - user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password, false), Constants.AUTH_URL); - final SyncConfiguration configNew = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .name("newRealm") - .waitForInitialRemoteData() - .readOnly() - .schema(StringOnly.class) - .build(); - assertFalse(configNew.realmExists()); - - realm = Realm.getInstance(configNew); - assertEquals(10, realm.where(StringOnly.class).count()); - realm.close(); - user.logOut(); - } - - @Test - public void waitForInitialRemoteData_readOnlyTrue_throwsIfWrongServerSchema() { - SyncCredentials credentials = SyncCredentials.usernamePassword(UUID.randomUUID().toString(), "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - final SyncConfiguration configNew = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .waitForInitialRemoteData() - .readOnly() - .schema(StringOnly.class) - .build(); - assertFalse(configNew.realmExists()); - - Realm realm = null; - try { - // This will fail, because the server Realm is completely empty and the Client is not allowed to write the - // schema. - realm = Realm.getInstance(configNew); - fail(); - } catch (RealmMigrationNeededException ignore) { - } finally { - if (realm != null) { - realm.close(); - } - user.logOut(); - } - } - - @Test - public void waitForInitialRemoteData_readOnlyFalse_upgradeSchema() { - SyncCredentials credentials = SyncCredentials.usernamePassword(UUID.randomUUID().toString(), "password", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - final SyncConfiguration config = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .waitForInitialRemoteData() // Not readonly so Client should be allowed to write schema - .schema(StringOnly.class) // This schema should be written when opening the empty Realm. - .schemaVersion(2) - .build(); - assertFalse(config.realmExists()); - - Realm realm = Realm.getInstance(config); - try { - assertEquals(0, realm.where(StringOnly.class).count()); - } finally { - realm.close(); - user.logOut(); - } - } - - @Ignore("FIXME: Re-enable this once we can test againt a proper Stitch server") - @Test - public void defaultRealm() throws InterruptedException { - SyncCredentials credentials = SyncCredentials.usernamePassword(UUID.randomUUID().toString(), "test", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - SyncConfiguration config = user.getDefaultConfiguration(); - Realm realm = Realm.getInstance(config); - SyncManager.getSession(config).downloadAllServerChanges(); - realm.refresh(); - - try { - assertTrue(realm.isEmpty()); - } finally { - realm.close(); - user.logOut(); - } - } - - // Check that custom headers and auth header renames are correctly used for HTTP requests - // performed from Java. - @Test - @RunTestInLooperThread - public void javaRequestCustomHeaders() { - SyncManager.addCustomRequestHeader("Foo", "bar"); - SyncManager.setAuthorizationHeaderName("RealmAuth"); - runJavaRequestCustomHeadersTest(); - } - - // Check that custom headers and auth header renames are correctly used for HTTP requests - // performed from Java. - @Test - @RunTestInLooperThread - public void javaRequestCustomHeaders_specificHost() { - SyncManager.addCustomRequestHeader("Foo", "bar", Constants.HOST); - SyncManager.setAuthorizationHeaderName("RealmAuth", Constants.HOST); - runJavaRequestCustomHeadersTest(); - } - - private void runJavaRequestCustomHeadersTest() { - SyncCredentials credentials = SyncCredentials.usernamePassword(UUID.randomUUID().toString(), "test", true); - - AtomicBoolean headerSet = new AtomicBoolean(false); - RealmLog.setLevel(LogLevel.ALL); - RealmLogger logger = (level, tag, throwable, message) -> { - if (level == LogLevel.TRACE - && message.contains("Foo: bar") - && message.contains("RealmAuth: ")) { - headerSet.set(true); - } - }; - looperThread.runAfterTest(() -> { - RealmLog.remove(logger); - }); - RealmLog.add(logger); - - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - try { - user.changePassword("foo"); - } catch (ObjectServerError e) { - if (e.getErrorCode() != ErrorCode.INVALID_CREDENTIALS) { - throw e; - } - } - - assertTrue(headerSet.get()); - looperThread.testComplete(); - } - - // Test that auth header renaming, custom headers and url prefix are all propagated correctly - // to Sync. There really isn't a way to create a proper integration test since ROS used for testing - // isn't configured to accept such requests. Instead we inspect the log from Sync which will - // output the headers in TRACE mode. - @Test - @RunTestInLooperThread - public void syncAuthHeaderAndUrlPrefix() { - SyncManager.setAuthorizationHeaderName("TestAuth"); - SyncManager.addCustomRequestHeader("Test", "test"); - runSyncAuthHeadersAndUrlPrefixTest(); - } - - // Test that auth header renaming, custom headers and url prefix are all propagated correctly - // to Sync. There really isn't a way to create a proper integration test since ROS used for testing - // isn't configured to accept such requests. Instead we inspect the log from Sync which will - // output the headers in TRACE mode. - @Test - @RunTestInLooperThread - public void syncAuthHeaderAndUrlPrefix_specificHost() { - SyncManager.setAuthorizationHeaderName("TestAuth", Constants.HOST); - SyncManager.addCustomRequestHeader("Test", "test", Constants.HOST); - runSyncAuthHeadersAndUrlPrefixTest(); - } - - private void runSyncAuthHeadersAndUrlPrefixTest() { - SyncCredentials credentials = SyncCredentials.usernamePassword(UUID.randomUUID().toString(), "test", true); - SyncUser user = SyncUser.logIn(credentials, Constants.AUTH_URL); - SyncConfiguration config = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .urlPrefix("/foo") - .errorHandler(new SyncSession.ErrorHandler() { - @Override - public void onError(SyncSession session, ObjectServerError error) { - RealmLog.error(error.toString()); - } - }) - .build(); - - RealmLog.setLevel(LogLevel.ALL); - RealmLogger logger = (level, tag, throwable, message) -> { - if (tag.equals("REALM_SYNC") - && message.contains("GET /foo/") - && message.contains("TestAuth: Realm-Access-Token version=1") - && message.contains("Test: test")) { - looperThread.testComplete(); - } - }; - looperThread.runAfterTest(() -> { - RealmLog.remove(logger); - }); - RealmLog.add(logger); - Realm realm = Realm.getInstance(config); - looperThread.closeAfterTest(realm); - } - - @Test - @RunTestInLooperThread - public void progressListenersWorkWhenUsingWaitForInitialRemoteData() throws InterruptedException { - String username = UUID.randomUUID().toString(); - String password = "password"; - SyncUser user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password, true), Constants.AUTH_URL); - - // 1. Copy a valid Realm to the server (and pray it does it within 10 seconds) - final SyncConfiguration configOld = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .schema(StringOnly.class) - .sessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) - .build(); - Realm realm = Realm.getInstance(configOld); - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - for (int i = 0; i < 10; i++) { - realm.createObject(StringOnly.class).setChars("Foo" + i); - } - } - }); - SyncManager.getSession(configOld).uploadAllLocalChanges(); - realm.close(); - user.logOut(); - assertTrue(SyncManager.getAllSessions(user).isEmpty()); - - // 2. Local state should now be completely reset. Open the same sync Realm but different local name again with - // a new configuration which should download the uploaded changes (pray it managed to do so within the time frame). - user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password), Constants.AUTH_URL); - SyncConfiguration config = user.createConfiguration(Constants.USER_REALM) - .name("newRealm") - .schema(StringOnly.class) - .waitForInitialRemoteData() - .build(); - assertFalse(config.realmExists()); - AtomicBoolean indefineteListenerComplete = new AtomicBoolean(false); - AtomicBoolean currentChangesListenerComplete = new AtomicBoolean(false); - RealmAsyncTask task = Realm.getInstanceAsync(config, new Realm.Callback() { - - @Override - public void onSuccess(Realm realm) { - realm.close(); - if (!indefineteListenerComplete.get()) { - fail("Indefinete progress listener did not report complete."); - } - if (!currentChangesListenerComplete.get()) { - fail("Current changes progress listener did not report complete."); - } - looperThread.testComplete(); - } - - @Override - public void onError(Throwable exception) { - fail(exception.toString()); - } - }); - looperThread.keepStrongReference(task); - SyncManager.getSession(config).addDownloadProgressListener(ProgressMode.INDEFINITELY, new ProgressListener() { - @Override - public void onChange(Progress progress) { - if (progress.isTransferComplete()) { - indefineteListenerComplete.set(true); - } - } - }); - SyncManager.getSession(config).addDownloadProgressListener(ProgressMode.CURRENT_CHANGES, new ProgressListener() { - @Override - public void onChange(Progress progress) { - if (progress.isTransferComplete()) { - currentChangesListenerComplete.set(true); - } - } - }); - } - - // Smoke test to check that `refreshConnections` doesn't crash. - // Testing that it actually works is not feasible in a unit test. - @Test - @RunTestInLooperThread - public void refreshConnections() { - RealmLog.setLevel(LogLevel.DEBUG); - SyncManager.refreshConnections(); // No Realms - - // A single active Realm - String username = UUID.randomUUID().toString(); - String password = "password"; - SyncUser user = SyncUser.logIn(SyncCredentials.usernamePassword(username, password, true), Constants.AUTH_URL); - final SyncConfiguration config = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) - .fullSynchronization() - .schema(StringOnly.class) - .build(); - Realm realm = Realm.getInstance(config); - SyncManager.refreshConnections(); - - // A single logged out Realm - realm.close(); - SyncManager.refreshConnections(); - 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 225932df5a..124d897993 100644 --- a/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncSessionTests.kt +++ b/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncSessionTests.kt @@ -191,28 +191,6 @@ class SyncSessionTests { } } - @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 -> @@ -401,8 +379,6 @@ class SyncSessionTests { // 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 @@ -435,7 +411,6 @@ class SyncSessionTests { app.login(Credentials.emailPassword(user.profile.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) @@ -448,12 +423,9 @@ class SyncSessionTests { // 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) + .createSyncConfigurationBuilder(user, UUID.randomUUID().toString()) .modules(SyncStringOnlyModule()) .waitForInitialRemoteData() .build() @@ -473,11 +445,9 @@ class SyncSessionTests { 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) @@ -514,15 +484,13 @@ class SyncSessionTests { // 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() { + fun uploadChangesWhenRealmOutOfScope() = looperThread.runBlocking { val strongRefs: MutableList = ArrayList() val chars = CharArray(1000000) // 2MB Arrays.fill(chars, '.') val twoMBString = String(chars) val config1 = configFactory - .createSyncConfigurationBuilder(user) + .createSyncConfigurationBuilder(user, UUID.randomUUID().toString()) .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.AFTER_CHANGES_UPLOADED) .modules(SyncStringOnlyModule()) .build() @@ -545,40 +513,37 @@ class SyncSessionTests { 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() - } + val realm2 = Realm.getInstance(config2) + 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) } + all.addChangeListener(realmChangeListener) } - handlerThread.quit() } TestHelper.awaitOrFail(testCompleted, TestHelper.STANDARD_WAIT_SECS) handlerThread.join() user.logOut() + looperThread.testComplete() } // 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() app.emailPassword.registerUser(uniqueName, "password") val config1 = configFactory - .createSyncConfigurationBuilder(user) + .createSyncConfigurationBuilder(user, UUID.randomUUID().toString()) .modules(SyncStringOnlyModule()) .build() Realm.getInstance(config1).use { realm -> @@ -595,13 +560,13 @@ class SyncSessionTests { val credentials = Credentials.emailPassword(user.profile.email!!, SECRET_PASSWORD) app.login(credentials) - // now let the admin upload some commits + // Write updates from a different user 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 + handler.post { // Using a different 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()) @@ -742,7 +707,8 @@ class SyncSessionTests { @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") + "closed, but realm seems to be closed, so further investigation is needed " + + "seems to be caused by https://github.com/realm/realm-java/issues/5416") fun waitForInitialRemoteData_throwsOnTimeout() = looperThread.runBlocking { val syncConfiguration = configFactory .createSyncConfigurationBuilder(user) diff --git a/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncedRealmIntegrationTests.kt b/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncedRealmIntegrationTests.kt new file mode 100644 index 0000000000..b084b35b2e --- /dev/null +++ b/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncedRealmIntegrationTests.kt @@ -0,0 +1,333 @@ +/* + * 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 + +import android.os.SystemClock +import androidx.test.annotation.UiThreadTest +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.entities.DefaultSyncSchema +import io.realm.entities.StringOnly +import io.realm.entities.SyncSchemeMigration +import io.realm.entities.SyncStringOnly +import io.realm.exceptions.DownloadingRealmInterruptedException +import io.realm.exceptions.RealmMigrationNeededException +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.objectserver.utils.Constants +import io.realm.rule.BlockingLooperThread +import org.bson.BsonObjectId +import org.bson.types.ObjectId +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +private const val SECRET_PASSWORD = "123456" + +/** + * Catch all class for tests that not naturally fit anywhere else. + */ +@RunWith(AndroidJUnit4::class) +class SyncedRealmIntegrationTests { + + private val looperThread = BlockingLooperThread() + + private lateinit var app: App + private lateinit var user: User + private lateinit var syncConfiguration: SyncConfiguration + + private val configurationFactory: TestSyncConfigurationFactory = TestSyncConfigurationFactory() + + @Before + fun setup() { + Realm.init(InstrumentationRegistry.getInstrumentation().targetContext) + RealmLog.setLevel(LogLevel.ALL) + app = TestApp() + user = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + syncConfiguration = configurationFactory + // 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 loginLogoutResumeSyncing() = looperThread.runBlocking { + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .testSchema(SyncStringOnly::class.java) + .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) + .build() + Realm.getInstance(config).use { realm -> + realm.executeTransaction { + realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "Foo" + } + realm.syncSession.uploadAllLocalChanges() + user.logOut() + } + try { + assertTrue(Realm.deleteRealm(config)) + } catch (e: IllegalStateException) { + // TODO: We don't have a way to ensure that the Realm instance on client thread has been + // closed for now https://github.com/realm/realm-java/issues/5416 + if (e.message!!.contains("It's not allowed to delete the file")) { + // retry after 1 second + SystemClock.sleep(1000) + assertTrue(Realm.deleteRealm(config)) + } + } + + user = app.login(Credentials.emailPassword(user.profile.email, SECRET_PASSWORD)) + val config2: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .testSchema(SyncStringOnly::class.java) + .build() + Realm.getInstance(config2).use { realm -> + realm.syncSession.downloadAllServerChanges() + realm.refresh() + assertEquals(1, realm.where(SyncStringOnly::class.java).count()) + } + looperThread.testComplete() + } + + @Test + @UiThreadTest + fun waitForInitialRemoteData_mainThreadThrows() { + val user: User = SyncTestUtils.createTestUser(app) + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .waitForInitialRemoteData() + .build() + assertFailsWith { + Realm.getInstance(config).close() + } + } + + @Test + fun waitForInitialRemoteData() { + // 1. Copy a valid Realm to the server (and pray it does it within 10 seconds) + val configOld: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .testSchema(SyncStringOnly::class.java) + .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) + .build() + Realm.getInstance(configOld).use { realm -> + realm.executeTransaction { realm -> + for (i in 0..9) { + realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "Foo$i" + } + } + realm.syncSession.uploadAllLocalChanges() + } + user.logOut() + + // 2. Local state should now be completely reset. Open the same sync Realm but different local name again with + // a new configuration which should download the uploaded changes (pray it managed to do so within the time frame). + // Use different user to trigger different path + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user2, user.id) + .testSchema(SyncStringOnly::class.java) + .waitForInitialRemoteData() + .build() + Realm.getInstance(config).use { realm -> + realm.executeTransaction { realm -> + for (i in 0..9) { + realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "Foo 1$i" + } + } + assertEquals(20, realm.where(SyncStringOnly::class.java).count()) + } + } + + // This tests will start and cancel getting a Realm 10 times. The Realm should be resilient towards that + // We cannot do much better since we cannot control the order of events internally in Realm which would be + // needed to correctly test all error paths. + @Test + @Ignore("Sync somehow keeps a Realm alive, causing the Realm.deleteRealm to throw " + + " https://github.com/realm/realm-java/issues/5416") + fun waitForInitialData_resilientInCaseOfRetries() = looperThread.runBlocking { + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .waitForInitialRemoteData() + .build() + for (i in 0..9) { + val blockingLooperThread = BlockingLooperThread() + blockingLooperThread.runDetached { + var realm: Realm? = null + try { + Thread.currentThread().interrupt() + Realm.getInstance(config).close() + } catch (e: DownloadingRealmInterruptedException) { + } + // TODO: We don't have a way to ensure that the Realm instance on client thread has been + // closed for now https://github.com/realm/realm-java/issues/5416 + app.sync.getSession(config).testShutdownAndWait() + try { + Realm.deleteRealm(config) + } catch (e: IllegalStateException) { + if (e.message!!.contains("It's not allowed to delete the file")) { + // retry after 1 second + SystemClock.sleep(1000) + assertTrue(Realm.deleteRealm(config)) + } + } + blockingLooperThread.testComplete() + }.await() + } + looperThread.testComplete() + } + + // This tests will start and cancel getting a Realm 10 times. The Realm should be resilient towards that + // We cannot do much better since we cannot control the order of events internally in Realm which would be + // needed to correctly test all error paths. + @Test + fun waitForInitialData_resilientInCaseOfRetriesAsync() = looperThread.runBlocking { + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) + .waitForInitialRemoteData() + .build() + val randomizer = Random() + for (i in 0..9) { + val task = Realm.getInstanceAsync(config, object : Realm.Callback() { + override fun onSuccess(realm: Realm) { fail() } + override fun onError(exception: Throwable) { fail(exception.toString()) } + }) + SystemClock.sleep(randomizer.nextInt(5).toLong()) + task.cancel() + } + // Leave some time for the async callbacks to actually get through + looperThread.postRunnableDelayed( + Runnable { looperThread.testComplete() }, + 1000 + ) + } + + @Test + fun waitForInitialRemoteData_readOnlyTrue() { + // 1. Copy a valid Realm to the server (and pray it does it within 10 seconds) + val configOld: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .testSchema(SyncStringOnly::class.java) + .build() + Realm.getInstance(configOld).use { realm -> + realm.executeTransaction { realm -> + for (i in 0..9) { + realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "Foo$i" + } + } + realm.syncSession.uploadAllLocalChanges() + } + user.logOut() + + // 2. Local state should now be completely reset. Open the Realm again with a new configuration which should + // download the uploaded changes (pray it managed to do so within the time frame). + // Use different user to trigger different path + val user2 = app.registerUserAndLogin(TestHelper.getRandomEmail(), SECRET_PASSWORD) + val configNew: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user2, user.id) + .waitForInitialRemoteData() + .readOnly() + .testSchema(SyncStringOnly::class.java) + .build() + assertFalse(configNew.testRealmExists()) + Realm.getInstance(configNew).use { realm -> + assertEquals(10, realm.where(SyncStringOnly::class.java).count()) + } + user.logOut() + } + + @Test + fun waitForInitialRemoteData_readOnlyTrue_throwsIfWrongServerSchema() { + val configNew: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .waitForInitialRemoteData() + .readOnly() + .testSchema(SyncSchemeMigration::class.java) + .build() + assertFalse(configNew.testRealmExists()) + assertFailsWith { + Realm.getInstance(configNew).use { realm -> + realm.executeTransaction { + it.createObject(SyncSchemeMigration::class.java, ObjectId()) + } + } + } + user.logOut() + } + + @Test + fun waitForInitialRemoteData_readOnlyFalse_upgradeSchema() { + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) + .waitForInitialRemoteData() // Not readonly so Client should be allowed to write schema + .testSchema(SyncStringOnly::class.java) // This schema should be written when opening the empty Realm. + .schemaVersion(2) + .build() + assertFalse(config.testRealmExists()) + Realm.getInstance(config).use { realm -> + assertEquals(0, realm.where(SyncStringOnly::class.java).count()) + } + user.logOut() + } + + @Test + fun defaultRealm() { + val config: SyncConfiguration = SyncConfiguration.defaultConfig(user, user.id) + Realm.getInstance(config).use { realm -> + realm.syncSession.downloadAllServerChanges() + realm.refresh() + assertTrue(realm.isEmpty) + } + user.logOut() + } + + + // Smoke test to check that `refreshConnections` doesn't crash. + // Testing that it actually works is not feasible in a unit test. + @Test + fun refreshConnections() = looperThread.runBlocking { + RealmLog.setLevel(LogLevel.DEBUG) + Sync.reconnect() // No Realms + + // A single active Realm + val username = UUID.randomUUID().toString() + val password = "password" + val user: User = app.registerUserAndLogin(username, password) + val config: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM) + .testSchema(StringOnly::class.java) + .build() + val realm = Realm.getInstance(config) + Sync.reconnect() + + // A single logged out Realm + realm.close() + Sync.reconnect() + looperThread.testComplete() + } + +} diff --git a/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestApp.kt b/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestApp.kt index a39443d57a..393eefe64b 100644 --- a/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestApp.kt +++ b/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestApp.kt @@ -31,8 +31,8 @@ const val DATABASE_NAME = "test_data" // same as above class TestApp( networkTransport: OsJavaNetworkTransport? = null, - customizeConfig: (AppConfiguration.Builder) -> AppConfiguration.Builder = { it } -) : App(createConfiguration(customizeConfig)) { + builder: (AppConfiguration.Builder) -> AppConfiguration.Builder = { it } +) : App(builder(configurationBuilder()).build()) { init { if (networkTransport != null) { @@ -42,16 +42,12 @@ class TestApp( companion object { - fun createConfiguration(customizeConfig: (AppConfiguration.Builder) -> AppConfiguration.Builder = { it }): AppConfiguration { - var builder = AppConfiguration.Builder(initializeMongoDbRealm()) + fun configurationBuilder(customizeConfig: (AppConfiguration.Builder) -> AppConfiguration.Builder = { it }): AppConfiguration.Builder { + return AppConfiguration.Builder(initializeMongoDbRealm()) .baseUrl("http://127.0.0.1:9090") .appName("MongoDB Realm Integration Tests") .appVersion("1.0.") .httpLogObfuscator(null) - - builder = customizeConfig(builder) - - return builder.build() } // Initializes MongoDB Realm. Clears all local state and fetches the application ID. 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 1b03689832..f098e1228f 100644 --- a/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestSyncConfigurationFactory.kt +++ b/realm/realm-library/src/syncTestUtils/kotlin/io/realm/TestSyncConfigurationFactory.kt @@ -35,6 +35,11 @@ class TestSyncConfigurationFactory : TestRealmConfigurationFactory() { .testSessionStopPolicy(OsRealmConfig.SyncSessionStopPolicy.IMMEDIATELY) } + fun createSyncConfigurationBuilder(user: User, partitionValue: String): SyncConfiguration.Builder { + return SyncConfiguration.Builder(user, partitionValue) + .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 ceeeb7b785..43b8dae7ce 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 @@ -25,6 +25,10 @@ class SyncConfigurationExt { companion object } +fun SyncConfiguration.testRealmExists(): Boolean{ + return this.realmExists() +} + // 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)