diff --git a/.github/workflows/include-static-analysis.yml b/.github/workflows/include-static-analysis.yml index 2232554864..60ec663580 100644 --- a/.github/workflows/include-static-analysis.yml +++ b/.github/workflows/include-static-analysis.yml @@ -35,6 +35,7 @@ jobs: run: ./gradlew ktlintCheck - name: Stash Ktlint results + if: always() run: | rm -rf /tmp/ktlint rm -rf /tmp/detekt @@ -50,6 +51,7 @@ jobs: - name: Publish Ktlint results uses: actions/upload-artifact@v4 + if: always() with: name: Ktlint Analyzer report path: /tmp/ktlint/* @@ -85,7 +87,8 @@ jobs: - name: Run Detekt run: ./gradlew detekt - - name: Stash Detekt results + - name: Stash Detekt results + if: always() run: | rm -rf /tmp/detekt mkdir /tmp/detekt @@ -99,6 +102,7 @@ jobs: - name: Publish Detekt results uses: actions/upload-artifact@v4 + if: always() with: name: Detekt Analyzer report path: /tmp/detekt/* diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7e9c45a8e2..4ef75fe85f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -122,7 +122,7 @@ jobs: key: jni-linux-lib-${{ needs.check-cache.outputs.packages-sha }} - name: Setup Java 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ vars.VERSION_JAVA_DISTRIBUTION }} java-version: ${{ vars.VERSION_JAVA }} @@ -473,7 +473,7 @@ jobs: echo '#!/bin/bash\nccache clang++ "$@"%"' > /usr/local/bin/ccache-clang++ - name: Setup Android SDK - uses: android-actions/setup-android@v2 + uses: android-actions/setup-android@v3 - name: Install NDK run: sdkmanager --install "ndk;${{ env.NDK_VERSION }}" @@ -547,7 +547,9 @@ jobs: uses: actions/setup-java@v4 with: distribution: ${{ vars.VERSION_JAVA_DISTRIBUTION }} - java-version: ${{ vars.VERSION_JAVA }} + java-version: | + 17 + ${{ vars.VERSION_JAVA }} - name: Setup Gradle and task/dependency caching uses: gradle/actions/setup-gradle@v3 @@ -602,9 +604,13 @@ jobs: echo '#!/bin/bash\nccache clang++ "$@"%"' > /usr/local/bin/ccache-clang++ - name: Setup Android SDK - uses: android-actions/setup-android@v2 + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} + uses: android-actions/setup-android@v3 - name: Install NDK + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} run: sdkmanager --install "ndk;${{ env.NDK_VERSION }}" - name: Build Android Base Test Apk diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc4468177..238c7b0f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,7 @@ * Minimum R8: 8.0.34. ### Internal -* None. - +* Updated to Realm Core 14.10.4 commit 4f83c590c4340dd7760d5f070e2e81613eb536aa. ## 2.1.0 (2024-07-12) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt index 39d88e2e12..5350877d65 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt @@ -68,6 +68,7 @@ public open class UnrecoverableSyncException internal constructor(message: Strin * Thrown when the type of sync used by the server does not match the one used by the client, i.e. * the server and client disagrees whether to use Partition-based or Flexible Sync. */ +@Suppress("DEPRECATION") public class WrongSyncTypeException internal constructor(message: String) : UnrecoverableSyncException(message) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt index 46fbc13e17..a6f015b9fb 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt @@ -17,7 +17,6 @@ import io.realm.kotlin.mongodb.exceptions.FunctionExecutionException import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException import io.realm.kotlin.mongodb.exceptions.ServiceException import io.realm.kotlin.mongodb.exceptions.SyncException -import io.realm.kotlin.mongodb.exceptions.UnrecoverableSyncException import io.realm.kotlin.mongodb.exceptions.UserAlreadyConfirmedException import io.realm.kotlin.mongodb.exceptions.UserAlreadyExistsException import io.realm.kotlin.mongodb.exceptions.UserNotFoundException @@ -91,19 +90,26 @@ internal fun convertSyncError(syncError: SyncError): SyncException { syncError.compensatingWrites, syncError.isFatal ) + ErrorCode.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED, ErrorCode.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED, - ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED -> { + ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED, + -> { // Permission denied errors should be unrecoverable according to Core, i.e. the // client will disconnect sync and transition to the "inactive" state - UnrecoverableSyncException(message) + @Suppress("DEPRECATION") io.realm.kotlin.mongodb.exceptions.UnrecoverableSyncException( + message + ) } + else -> { // An error happened we are not sure how to handle. Just report as a generic // SyncException. when (syncError.isFatal) { false -> SyncException(message, syncError.isFatal) - true -> UnrecoverableSyncException(message) + true -> @Suppress("DEPRECATION") io.realm.kotlin.mongodb.exceptions.UnrecoverableSyncException( + message + ) } } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt index 9fcfdc8a1b..c7dd207ae4 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt @@ -168,7 +168,7 @@ internal open class SyncSessionImpl( nativePointer, error, message, - true + false ) } diff --git a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index cfc7d1f33f..706f9815c4 100644 --- a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -56,6 +56,10 @@ actual object PlatformUtils { } SystemClock.sleep(5000) // 5 seconds to give the GC some time to process } + + actual fun copyFile(originPath: String, targetPath: String) { + File(originPath).copyTo(File(targetPath)) + } } // Allocs as much garbage as we can. Pass maxSize = 0 to use all available memory in the process. diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index ee6a77661d..60b595f793 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -7,6 +7,7 @@ import kotlin.time.Duration expect object PlatformUtils { fun createTempDir(prefix: String = Utils.createRandomString(16), readOnly: Boolean = false): String fun deleteTempDir(path: String) + fun copyFile(originPath: String, targetPath: String) fun sleep(duration: Duration) fun threadId(): ULong fun triggerGC() diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt index 0377f0a106..02526a98e1 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt @@ -69,6 +69,7 @@ class EncryptionTests { // Initialize an encrypted Realm val encryptedConf = RealmConfiguration .Builder( + schema = setOf(Sample::class) ) .directory(tmpDir) diff --git a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index 498ba62df6..c9708e5148 100644 --- a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.test.platform +import java.io.File import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -41,6 +42,10 @@ actual object PlatformUtils { return dir.absolutePathString() } + actual fun copyFile(originPath: String, targetPath: String) { + File(originPath).copyTo(File(targetPath)) + } + actual fun deleteTempDir(path: String) { val rootPath: Path = Paths.get(path) val pathsToDelete: List = diff --git a/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index d92ec1d549..324734b843 100644 --- a/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -72,4 +72,8 @@ actual object PlatformUtils { actual fun triggerGC() { GC.collect() } + + actual fun copyFile(originPath: String, targetPath: String) { + platform.Foundation.NSFileManager.defaultManager.copyItemAtPath(originPath, targetPath, null) + } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt index 373678ac1c..c129bb95cc 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt @@ -438,12 +438,20 @@ class AppTests { // Create a configuration pointing to the metadata Realm for that app val metadataDir = "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/server-utility/metadata/" + + // Workaround for https://github.com/realm/realm-core/issues/7876 + // We cannot validate if the test app metadata realm is encrypted directly, as it is cached + // and subsequent access wont validate the encryption key. Copying the Realm allows to bypass + // the cache. + PlatformUtils.copyFile(metadataDir + "sync_metadata.realm", metadataDir + "copy_sync_metadata.realm") + val wrongKey = TestHelper.getRandomKey() val config = RealmConfiguration .Builder(setOf()) - .name("sync_metadata.realm") + .name("copy_sync_metadata.realm") .directory(metadataDir) .encryptionKey(wrongKey) + .schemaVersion(7) .build() assertTrue(fileExists(config.path)) @@ -478,10 +486,18 @@ class AppTests { // Create a configuration pointing to the metadata Realm for that app val metadataDir = "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/server-utility/metadata/" + + // Workaround for https://github.com/realm/realm-core/issues/7876 + // We cannot validate if the test app metadata realm is encrypted directly, as it is cached + // and subsequent access wont validate the encryption key. Copying the Realm allows to bypass + // the cache. + PlatformUtils.copyFile(metadataDir + "sync_metadata.realm", metadataDir + "copy_sync_metadata.realm") + val config = RealmConfiguration .Builder(setOf()) - .name("sync_metadata.realm") + .name("copy_sync_metadata.realm") .directory(metadataDir) + .schemaVersion(7) .build() assertTrue(fileExists(config.path)) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt index 9c67b39196..53728762cf 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt @@ -239,7 +239,7 @@ class FLXProgressListenerTests { try { val flow = realm.syncSession.progressAsFlow(Direction.UPLOAD, ProgressMode.INDEFINITELY) val job = async { - withTimeout(10.seconds) { + withTimeout(30.seconds) { flow.collect { channel.trySend(true) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 66a26acc6b..5e96219439 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -65,6 +65,7 @@ import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs @@ -710,10 +711,8 @@ class SyncClientResetIntegrationTests { exception: ClientResetRequiredException ) { // Notify that this callback has been invoked - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertContains(exception.message!!, "User-provided callback failed") + assertIs(exception.cause) assertEquals( "User exception", @@ -788,10 +787,8 @@ class SyncClientResetIntegrationTests { exception: ClientResetRequiredException ) { // Notify that this callback has been invoked - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertContains(exception.message!!, "User-provided callback failed") + channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } }).build() @@ -1080,7 +1077,7 @@ class SyncClientResetIntegrationTests { assertNotNull(exception.originalFilePath) assertFalse(fileExists(exception.recoveryFilePath)) assertTrue(fileExists(exception.originalFilePath)) - assertTrue(exception.message!!.contains("Simulate Client Reset")) + assertContains(exception.message!!, "Simulate Client Reset") } } channel.close() @@ -1123,10 +1120,8 @@ class SyncClientResetIntegrationTests { exception: ClientResetRequiredException ) { // Notify that this callback has been invoked - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertContains(exception.message!!, "User-provided callback failed") + channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } }).build() @@ -1193,11 +1188,8 @@ class SyncClientResetIntegrationTests { // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) - - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + println(exception.message) + assertContains(exception.message!!, "User-provided callback failed") channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } @@ -1400,10 +1392,7 @@ class SyncClientResetIntegrationTests { assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertTrue(exception.message!!.contains("User-provided callback failed")) channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index b5bf6141a9..4d30150a34 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -350,14 +350,14 @@ class SyncedRealmTests { syncSession = (realm.syncSession as SyncSessionImpl).nativePointer, error = ErrorCode.RLM_ERR_ACCOUNT_NAME_IN_USE, errorMessage = "Non fatal error", - isFatal = true, // flipped https://jira.mongodb.org/browse/RCORE-2146 + isFatal = false, ) RealmInterop.realm_sync_session_handle_error_for_testing( syncSession = (realm.syncSession as SyncSessionImpl).nativePointer, error = ErrorCode.RLM_ERR_INTERNAL_SERVER_ERROR, errorMessage = "Fatal error", - isFatal = false, // flipped https://jira.mongodb.org/browse/RCORE-2146 + isFatal = true, ) } } @@ -1650,14 +1650,17 @@ class SyncedRealmTests { println("Partition based sync bundled realm is in ${config2.path}") } - // This test cannot run multiple times on the same server instance as the primary - // key of the objects from asset-pbs.realm will not be unique on secondary runs. @Test fun initialRealm_partitionBasedSync() { val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) } + + runBlocking { + app.asTestApp.deleteDocuments(app.configuration.appId, "ParentPk", "{}") + } + val config1 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler {