diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe52a8e2a..b6b39a742f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ ### Fixed * Added an error that is raised when interface based Realm classes are used with a language version lower than 8.0. At the same time, removed the use of `not` in the generated code, so that it's compatible with a minumum C# version of 8.0. (Issue [#3265](https://github.com/realm/realm-dotnet/issues/3265)) +* Logging into a single user using multiple auth providers created a separate SyncUser per auth provider. This mostly worked, but had some quirks: + - Sync sessions would not necessarily be associated with the specific SyncUser used to create them. As a result, querying a user for its sessions could give incorrect results, and logging one user out could close the wrong sessions. + - Existing local synchronized Realm files created using version of Realm from August - November 2020 would sometimes not be opened correctly and would instead be redownloaded. + - Removing one of the SyncUsers would delete all local Realm files for all SyncUsers for that user. + - Deleting the server-side user via one of the SyncUsers left the other SyncUsers in an invalid state. + - A SyncUser which was originally created via anonymous login and then linked to an identity would still be treated as an anonymous users and removed entirely on logout. + (Core 13.21.0) +* If a user was logged out while an access token refresh was in progress, the refresh completing would mark the user as logged in again and the user would be in an inconsistent state (Core 13.21.0). +* If querying over a geospatial dataset that had some objects with a type property set to something other than 'Point' (case insensitive) an exception would have been thrown. Instead of disrupting the query, those objects are now just ignored. (Core 13.21.0) +* Receiving a write_not_allowed error from the server would have led to a crash. (Core 13.22.0) +* Updating subscriptions did not trigger Realm autorefreshes, sometimes resulting in async refresh hanging until another write was performed by something else. (Core 13.23.1) +* Fix interprocess locking for concurrent realm file access resulting in a interprocess deadlock on FAT32/exFAT filesystems. (Core 13.23.1) ### Compatibility * Realm Studio: 13.0.0 or later. diff --git a/Realm/Realm/Exceptions/Sync/ErrorCode.cs b/Realm/Realm/Exceptions/Sync/ErrorCode.cs index f78d79dfeb..c51fd17c82 100644 --- a/Realm/Realm/Exceptions/Sync/ErrorCode.cs +++ b/Realm/Realm/Exceptions/Sync/ErrorCode.cs @@ -18,7 +18,6 @@ using System; using Realms.Sync.ErrorHandling; -using static System.Net.WebRequestMethods; namespace Realms.Sync.Exceptions; @@ -49,6 +48,11 @@ public enum ErrorCode /// BadChangeset = 1015, + /// + /// The client attempted to create a subscription which the server rejected. + /// + SubscriptionFailed = 1016, + /// /// The client attempted to create a subscription for a query is invalid/malformed. /// @@ -107,6 +111,13 @@ public enum ErrorCode /// WrongSyncType = 1043, + /// + /// Client attempted a write that is disallowed by permissions, or modifies an + /// object outside the current query, and the server undid the modification. + /// + /// + CompensatingWrite = 1033, + /// /// Unrecognized error code. It usually indicates incompatibility between the App Services server and client SDK versions. /// @@ -197,13 +208,6 @@ public enum ErrorCode [Obsolete("This error code is no longer reported")] InitialSyncNotCompleted = -4, - /// - /// Client attempted a write that is disallowed by permissions, or modifies an - /// object outside the current query, and the server undid the modification. - /// - /// - CompensatingWrite = 1033, - /// /// An error sent by the server when its data structures used to track client progress /// become corrupted. diff --git a/Realm/Realm/Handles/SubscriptionSetHandle.cs b/Realm/Realm/Handles/SubscriptionSetHandle.cs index 4a2c7bb646..f44aa5d5a7 100644 --- a/Realm/Realm/Handles/SubscriptionSetHandle.cs +++ b/Realm/Realm/Handles/SubscriptionSetHandle.cs @@ -34,7 +34,7 @@ internal class SubscriptionSetHandle : RealmHandle private static class NativeMethods { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void StateWaitCallback(IntPtr task_completion_source, SubscriptionSetState new_state, PrimitiveValue message); + public delegate void StateWaitCallback(IntPtr task_completion_source, SubscriptionSetState new_state, StringValue message, ErrorCode error_code); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void GetSubscriptionCallback(IntPtr managed_callback, Native.Subscription subscription); @@ -329,33 +329,25 @@ private static void OnGetSubscription(IntPtr managedCallbackPtr, Native.Subscrip } [MonoPInvokeCallback(typeof(NativeMethods.StateWaitCallback))] - private static void HandleStateWaitCallback(IntPtr taskCompletionSource, SubscriptionSetState state, PrimitiveValue message) + private static void HandleStateWaitCallback(IntPtr taskCompletionSource, SubscriptionSetState state, StringValue message, ErrorCode error_code) { var handle = GCHandle.FromIntPtr(taskCompletionSource); var tcs = (TaskCompletionSource)handle.Target!; - switch (message.Type) + switch (error_code) { - case RealmValueType.Null: + case 0: // No error tcs.TrySetResult(state); break; - case RealmValueType.Int when message.AsInt() == -1: + case (ErrorCode)1027: // OperationAborted tcs.TrySetException(new TaskCanceledException("The SubscriptionSet was closed before the wait could complete. This is likely because the Realm it belongs to was disposed.")); break; - case RealmValueType.String: - var messageString = message.AsString(); - if (messageString == "Active SubscriptionSet without a SubscriptionStore") - { - tcs.TrySetException(new TaskCanceledException("The SubscriptionSet was closed before the wait could complete. This is likely because the Realm it belongs to was disposed.")); - } - else - { - tcs.TrySetException(new SubscriptionException(messageString)); - } - + case ErrorCode.BadQuery: + case ErrorCode.SubscriptionFailed: + tcs.TrySetException(new SubscriptionException(message!)); break; default: - tcs.TrySetException(new SubscriptionException($"An unexpected error occurred and the wrong error type was supplied: {message.Type}. Please file a new issue at https://github.com/realm/realm-dotnet/issues")); + tcs.TrySetException(new SessionException((string?)message ?? "Unknown error", error_code)); break; } diff --git a/Realm/Realm/Handles/SyncUserHandle.cs b/Realm/Realm/Handles/SyncUserHandle.cs index 69f020f8f2..c3a1ea1423 100644 --- a/Realm/Realm/Handles/SyncUserHandle.cs +++ b/Realm/Realm/Handles/SyncUserHandle.cs @@ -51,9 +51,6 @@ private static class NativeMethods [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncuser_get_state", CallingConvention = CallingConvention.Cdecl)] public static extern UserState get_state(SyncUserHandle user, out NativeException ex); - [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncuser_get_auth_provider", CallingConvention = CallingConvention.Cdecl)] - public static extern Credentials.AuthProvider get_auth_provider(SyncUserHandle user, out NativeException ex); - [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncuser_get_profile_data", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr get_profile_data(SyncUserHandle user, UserProfileField field, IntPtr buffer, IntPtr buffer_length, [MarshalAs(UnmanagedType.U1)] out bool isNull, @@ -184,13 +181,6 @@ public UserState GetState() return result; } - public Credentials.AuthProvider GetProvider() - { - var result = NativeMethods.get_auth_provider(this, out var ex); - ex.ThrowIfNecessary(); - return result; - } - public bool TryGetApp([MaybeNullWhen(false)] out AppHandle appHandle) { var result = NativeMethods.get_app(this, out var ex); diff --git a/Realm/Realm/Sync/User.cs b/Realm/Realm/Sync/User.cs index d8c50755d8..3098f55380 100644 --- a/Realm/Realm/Sync/User.cs +++ b/Realm/Realm/Sync/User.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Net; using System.Threading.Tasks; using MongoDB.Bson; @@ -108,7 +109,8 @@ public string DeviceId /// Gets a value indicating which this user logged in with. /// /// The used to login the user. - public Credentials.AuthProvider Provider => Handle.GetProvider(); + [Obsolete("User.Provider wasn't working consistently and will be removed in a future version. You can get the provider of the user identity instead.")] + public Credentials.AuthProvider Provider => Identities.FirstOrDefault()?.Provider ?? Credentials.AuthProvider.Unknown; /// /// Gets the app with which this user is associated. @@ -325,7 +327,7 @@ public async Task LinkCredentialsAsync(Credentials credentials) /// A string that represents the current object. public override string ToString() { - return $"User {Id}, State: {State}, Provider: {Provider}"; + return $"User {Id}, State: {State}"; } internal void RaiseChanged() diff --git a/Tests/Realm.Tests/Database/GeospatialTests.cs b/Tests/Realm.Tests/Database/GeospatialTests.cs index 2b022c59a7..938284cb72 100644 --- a/Tests/Realm.Tests/Database/GeospatialTests.cs +++ b/Tests/Realm.Tests/Database/GeospatialTests.cs @@ -71,14 +71,8 @@ public void LinqTests(GeoShapeBase shape, string[] expectedMatches) [Test] public void Filter_InvalidPropertyType_Throws() { - PopulateCompanies(); _realm.Write(() => { - var company = _realm.All().First(); - - // We're making the point into a polygon, which is not supported - company.Location!.DynamicApi.Set("type", "Polygon"); - _realm.Add(new ObjectWithInvalidGeoPoints { CoordinatesEmbedded = new() @@ -93,8 +87,6 @@ public void Filter_InvalidPropertyType_Throws() }); }); - AssertInvalidGeoData(nameof(Company.Location), expectedError: "The only Geospatial type currently supported is 'point'"); - AssertInvalidGeoData(nameof(ObjectWithInvalidGeoPoints.TopLevelGeoPoint), expectedError: "GEOWITHIN query can only operate on a link to an embedded class"); AssertInvalidGeoData(nameof(ObjectWithInvalidGeoPoints.TypeEmbedded)); AssertInvalidGeoData(nameof(ObjectWithInvalidGeoPoints.CoordinatesEmbedded)); diff --git a/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs b/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs index 999927d4d2..2b95b9e603 100644 --- a/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs +++ b/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs @@ -671,8 +671,7 @@ public void SubscriptionSet_Remove_ByQuery_RemoveNamed() }); Assert.That(realm.Subscriptions.Count, Is.EqualTo(2)); - Assert.That(realm.Subscriptions[0].Name, Is.Null); - Assert.That(realm.Subscriptions[1].Name, Is.EqualTo("c")); + Assert.That(realm.Subscriptions.Select(o => o.Name), Is.EquivalentTo(new[] { null, "c" })); realm.Subscriptions.Update(() => { @@ -713,10 +712,7 @@ public void SubscriptionSet_Remove_ByQuery_RemoveNamed_False() }); Assert.That(realm.Subscriptions.Count, Is.EqualTo(4)); - Assert.That(realm.Subscriptions[0].Name, Is.EqualTo("a")); - Assert.That(realm.Subscriptions[1].Name, Is.EqualTo("b")); - Assert.That(realm.Subscriptions[2].Name, Is.Null); - Assert.That(realm.Subscriptions[3].Name, Is.EqualTo("c")); + Assert.That(realm.Subscriptions.Select(o => o.Name), Is.EquivalentTo(new[] { "a", "b", null, "c" })); realm.Subscriptions.Update(() => { @@ -728,9 +724,7 @@ public void SubscriptionSet_Remove_ByQuery_RemoveNamed_False() }); Assert.That(realm.Subscriptions.Count, Is.EqualTo(3)); - Assert.That(realm.Subscriptions[0].Name, Is.EqualTo("a")); - Assert.That(realm.Subscriptions[1].Name, Is.EqualTo("b")); - Assert.That(realm.Subscriptions[2].Name, Is.EqualTo("c")); + Assert.That(realm.Subscriptions.Select(o => o.Name), Is.EquivalentTo(new[] { "a", "b", "c" })); } [Test] @@ -914,9 +908,7 @@ public void SubscriptionSet_RemoveAll_RemoveNamed_False() }); Assert.That(realm.Subscriptions.Count, Is.EqualTo(3)); - Assert.That(realm.Subscriptions[0].Name, Is.EqualTo("a")); - Assert.That(realm.Subscriptions[1].Name, Is.EqualTo("b")); - Assert.That(realm.Subscriptions[2].Name, Is.EqualTo("c")); + Assert.That(realm.Subscriptions.Select(o => o.Name), Is.EquivalentTo(new[] { "a", "b", "c" })); } [Test] @@ -1925,7 +1917,6 @@ public void Integration_InitialSubscriptions_Unnamed([Values(true, false)] bool } var query = realm.All().ToArray().Select(o => o.DoubleProperty).ToArray(); - Assert.That(query.Count(), Is.EqualTo(2)); Assert.That(query, Is.EquivalentTo(new[] { 1.5, 2.5 })); }, timeout: 60_000); } @@ -1963,7 +1954,6 @@ public void Integration_InitialSubscriptions_Named([Values(true, false)] bool op } var query = realm.All().ToArray().Select(o => o.DoubleProperty).ToArray(); - Assert.That(query.Count(), Is.EqualTo(1)); Assert.That(query, Is.EquivalentTo(new[] { 2.5 })); }); } @@ -2208,7 +2198,15 @@ public void Results_Subscribe_FirstTimeOnly_DoesntWaitForChanges([Values("abc", writerRealm.Write(() => { - writerRealm.Add(new SyncAllTypesObject { DoubleProperty = 3.5, GuidProperty = testGuid, }); + for (var i = 0; i < 10; i++) + { + writerRealm.Add(new SyncAllTypesObject + { + ByteArrayProperty = TestHelpers.GetBytes(100_000), + DoubleProperty = 3.5, + GuidProperty = testGuid + }); + } }); await WaitForUploadAsync(writerRealm); @@ -2221,7 +2219,7 @@ public void Results_Subscribe_FirstTimeOnly_DoesntWaitForChanges([Values("abc", // If we manually wait for the download, we should see the second object show up. await WaitForDownloadAsync(realm); - Assert.That(query.Count(), Is.EqualTo(2)); + Assert.That(query.Count(), Is.EqualTo(11)); }); } @@ -2243,15 +2241,25 @@ public void Results_SubscribeNamed_FirstTimeOnly_DifferentQuery_WaitsForChanges( writerRealm.Write(() => { - writerRealm.Add(new SyncAllTypesObject { DoubleProperty = 3.5, GuidProperty = testGuid, }); + for (var i = 0; i < 10; i++) + { + writerRealm.Add(new SyncAllTypesObject + { + ByteArrayProperty = TestHelpers.GetBytes(100_000), + DoubleProperty = 3.5, + GuidProperty = testGuid + }); + } }); + await WaitForUploadAsync(writerRealm); + // Resubscribe to a different query with same name with waitForSync.FirstTime. Even though // we have a subscription with the same name, SubscribeAsync should wait for the new subscription. await realm.All() .Where(o => o.GuidProperty == testGuid && o.DoubleProperty > 2.0001) .SubscribeAsync(new() { Name = "abc" }); - Assert.That(query.Count(), Is.EqualTo(2)); + Assert.That(query.Count(), Is.EqualTo(11)); }); } @@ -2323,11 +2331,21 @@ public void Results_Subscribe_Always_WaitsForChanges([Values("abc", null)] strin writerRealm.Write(() => { - writerRealm.Add(new SyncAllTypesObject { DoubleProperty = 3.5, GuidProperty = testGuid, }); + for (var i = 0; i < 10; i++) + { + writerRealm.Add(new SyncAllTypesObject + { + ByteArrayProperty = TestHelpers.GetBytes(100_000), + DoubleProperty = 3.5, + GuidProperty = testGuid + }); + } }); + await WaitForUploadAsync(writerRealm); + await query.SubscribeAsync(new() { Name = name }, WaitForSyncMode.Always); - Assert.That(query.Count(), Is.EqualTo(2)); + Assert.That(query.Count(), Is.EqualTo(11)); }); } diff --git a/Tests/Realm.Tests/Sync/UserManagementTests.cs b/Tests/Realm.Tests/Sync/UserManagementTests.cs index 401e9d3a67..1c94a40000 100644 --- a/Tests/Realm.Tests/Sync/UserManagementTests.cs +++ b/Tests/Realm.Tests/Sync/UserManagementTests.cs @@ -178,7 +178,6 @@ public void EmailPasswordRegisterUser_Works() Assert.That(user, Is.Not.Null); Assert.That(user.State, Is.EqualTo(UserState.LoggedIn)); - Assert.That(user.Provider, Is.EqualTo(Credentials.AuthProvider.EmailPassword)); Assert.That(user.AccessToken, Is.Not.Empty); Assert.That(user.RefreshToken, Is.Not.Empty); @@ -756,8 +755,8 @@ public void UserApiKeys_CanLoginWithGeneratedKey() Assert.That(apiKeyUser.Id, Is.EqualTo(user.Id)); - Assert.That(apiKeyUser.Provider, Is.EqualTo(Credentials.AuthProvider.ApiKey)); - Assert.That(apiKeyUser.RefreshToken, Is.Not.EqualTo(user.RefreshToken)); + Assert.That(apiKeyUser.Identities.Select(i => i.Provider), Does.Contain(Credentials.AuthProvider.ApiKey)); + Assert.That(apiKeyUser.RefreshToken, Is.EqualTo(user.RefreshToken)); }); } @@ -784,8 +783,7 @@ public void UserApiKeys_CanLoginWithReenabledKey() Assert.That(apiKeyUser.Id, Is.EqualTo(user.Id)); - Assert.That(apiKeyUser.Provider, Is.EqualTo(Credentials.AuthProvider.ApiKey)); - Assert.That(apiKeyUser.RefreshToken, Is.Not.EqualTo(user.RefreshToken)); + Assert.That(apiKeyUser.RefreshToken, Is.EqualTo(user.RefreshToken)); }); } @@ -980,7 +978,6 @@ public void UserToStringOverride() { var user = GetFakeUser(); Assert.That(user.ToString(), Does.Contain(user.Id)); - Assert.That(user.ToString(), Does.Contain(user.Provider.ToString())); } [Test, Ignore("test")] diff --git a/wrappers/realm-core b/wrappers/realm-core index 673be45ce2..1527dc439a 160000 --- a/wrappers/realm-core +++ b/wrappers/realm-core @@ -1 +1 @@ -Subproject commit 673be45ce266cef009ac5af58e8ff7327dd0d234 +Subproject commit 1527dc439a9609beae88d39b4101b741e7a6a741 diff --git a/wrappers/src/app_cs.cpp b/wrappers/src/app_cs.cpp index 8539af5a49..ead310cf4f 100644 --- a/wrappers/src/app_cs.cpp +++ b/wrappers/src/app_cs.cpp @@ -246,8 +246,7 @@ extern "C" { id, refresh_token, access_token, - "testing", - "my-device-id")); + "testing")); }); } diff --git a/wrappers/src/subscription_set_cs.cpp b/wrappers/src/subscription_set_cs.cpp index 595d9d5e8e..a7591704f2 100644 --- a/wrappers/src/subscription_set_cs.cpp +++ b/wrappers/src/subscription_set_cs.cpp @@ -57,7 +57,7 @@ enum class CSharpState : uint8_t { Superseded }; -using StateWaitCallbackT = void(void* task_completion_source, CSharpState state, realm_value_t message); +using StateWaitCallbackT = void(void* task_completion_source, CSharpState state, realm_string_t message, ErrorCodes::Error error_code); using SubscriptionCallbackT = void(void* managed_callback, CSharpSubscription sub); namespace realm { @@ -336,19 +336,19 @@ REALM_EXPORT void realm_subscriptionset_wait_for_state(SharedSubscriptionSet& su if (auto subs = weak_subs.lock()) { subs->refresh(); if (status.is_ok()) { - s_state_wait_callback(task_completion_source, core_to_csharp_state(status.get_value()), realm_value_t{}); + s_state_wait_callback(task_completion_source, core_to_csharp_state(status.get_value()), realm_string_t{}, ErrorCodes::Error::OK); } else { - s_state_wait_callback(task_completion_source, CSharpState::Error, to_capi_value(status.get_status().reason())); + s_state_wait_callback(task_completion_source, CSharpState::Error, to_capi(status.get_status().reason()), status.get_status().code()); } } else { - s_state_wait_callback(task_completion_source, CSharpState::Error, to_capi(-1)); + s_state_wait_callback(task_completion_source, CSharpState::Error, to_capi(std::string("operation aborted")), ErrorCodes::Error::OperationAborted); } } catch (...) { auto inner_ex = convert_exception(); - s_state_wait_callback(task_completion_source, CSharpState::Error, to_capi_value(inner_ex.to_string())); + s_state_wait_callback(task_completion_source, CSharpState::Error, to_capi(inner_ex.to_string()), ErrorCodes::Error::RuntimeError); } }); }); diff --git a/wrappers/src/sync_user_cs.cpp b/wrappers/src/sync_user_cs.cpp index 5036f5c49f..af3b28c076 100644 --- a/wrappers/src/sync_user_cs.cpp +++ b/wrappers/src/sync_user_cs.cpp @@ -143,13 +143,6 @@ extern "C" { }); } - REALM_EXPORT AuthProvider realm_syncuser_get_auth_provider(SharedSyncUser& user, NativeException::Marshallable& ex) - { - return handle_errors(ex, [&] { - return to_auth_provider(user->provider_type()); - }); - } - REALM_EXPORT size_t realm_syncuser_get_custom_data(SharedSyncUser& user, uint16_t* buffer, size_t buffer_length, bool& is_null, NativeException::Marshallable& ex) { return handle_errors(ex, [&] {