diff --git a/Realm/Realm/Handles/AppHandle.cs b/Realm/Realm/Handles/AppHandle.cs index 7221a42521..353cbd0e28 100644 --- a/Realm/Realm/Handles/AppHandle.cs +++ b/Realm/Realm/Handles/AppHandle.cs @@ -103,6 +103,12 @@ public static extern IntPtr get_user_for_testing( [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_clear_cached_apps", CallingConvention = CallingConvention.Cdecl)] public static extern void clear_cached_apps(out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_get_base_file_path", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_base_file_path(AppHandle app, IntPtr buffer, IntPtr buffer_length, out NativeException ex); + + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_get_base_uri", CallingConvention = CallingConvention.Cdecl)] + public static extern StringValue get_base_uri(AppHandle app, out NativeException ex); + public static class EmailPassword { [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_email_register_user", CallingConvention = CallingConvention.Cdecl)] @@ -349,6 +355,22 @@ public SyncUserHandle GetUserForTesting(string id, string refreshToken, string a return new SyncUserHandle(result); } + public string GetBaseFilePath() + { + return MarshalHelpers.GetString((IntPtr buffer, IntPtr length, out bool isNull, out NativeException ex) => + { + isNull = false; + return NativeMethods.get_base_file_path(this, buffer, length, out ex); + })!; + } + + public Uri GetBaseUri() + { + var value = NativeMethods.get_base_uri(this, out var ex); + ex.ThrowIfNecessary(); + return new Uri(value!); + } + protected override void Unbind() => NativeMethods.destroy(handle); [MonoPInvokeCallback(typeof(NativeMethods.UserCallback))] diff --git a/Realm/Realm/Sync/App.cs b/Realm/Realm/Sync/App.cs index d7458ddf2c..255243df67 100644 --- a/Realm/Realm/Sync/App.cs +++ b/Realm/Realm/Sync/App.cs @@ -119,6 +119,21 @@ public class App .Select(handle => new User(handle, this)) .ToArray(); + /// + /// Gets the root folder relative to which all local data for this application will be stored. This data includes + /// metadata for users and synchronized Realms. + /// + /// The app's base path. + /// + public string BaseFilePath => Handle.GetBaseFilePath(); + + /// + /// Gets the base url for this Realm application. + /// + /// The app's base url. + /// + public Uri BaseUri => Handle.GetBaseUri(); + internal App(AppHandle handle) { Handle = handle; @@ -129,7 +144,6 @@ internal App(AppHandle handle) /// /// The , specifying key parameters for the app behavior. /// An instance can now be used to login users, call functions, or open synchronized Realms. - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The http client is owned by the app and must be kept alive.")] public static App Create(AppConfiguration config) { Argument.NotNull(config, nameof(config)); diff --git a/Tests/Realm.Tests/Sync/AppTests.cs b/Tests/Realm.Tests/Sync/AppTests.cs index 1fe31b1ce6..55a1aa0458 100644 --- a/Tests/Realm.Tests/Sync/AppTests.cs +++ b/Tests/Realm.Tests/Sync/AppTests.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -127,6 +128,9 @@ void AssertBundleId(params string[] expectedValues) [Test] public void AppCreate_CreatesApp() { + var basePath = Path.Combine(InteropConfig.GetDefaultStorageFolder("No error expected here"), "foo-bar"); + Directory.CreateDirectory(basePath); + // This is mostly a smoke test to ensure that nothing blows up when setting all properties. var config = new AppConfiguration("abc-123") { @@ -135,12 +139,14 @@ public void AppCreate_CreatesApp() LocalAppVersion = "1.2.3", MetadataEncryptionKey = new byte[64], MetadataPersistenceMode = MetadataPersistenceMode.Encrypted, - BaseFilePath = InteropConfig.GetDefaultStorageFolder("No error expected here"), + BaseFilePath = basePath, DefaultRequestTimeout = TimeSpan.FromSeconds(123) }; var app = CreateApp(config); Assert.That(app.Sync, Is.Not.Null); + Assert.That(app.BaseUri, Is.EqualTo(config.BaseUri)); + Assert.That(app.BaseFilePath, Is.EqualTo(config.BaseFilePath)); } [Test] diff --git a/Tests/Realm.Tests/Sync/SessionTests.cs b/Tests/Realm.Tests/Sync/SessionTests.cs index 462583d43b..a3f4332492 100644 --- a/Tests/Realm.Tests/Sync/SessionTests.cs +++ b/Tests/Realm.Tests/Sync/SessionTests.cs @@ -1222,6 +1222,28 @@ public void Session_PermissionDenied_DoesntCrash() }, timeout: 1000000); } + [Test] + public void Session_GetUser_GetApp_ReturnsMeaningfulValue() + { + // Session.User.App doesn't have a reference to the managed app, which means we need to + // recreate it from the native one. This test verifies that the properties on the native + // app match the ones on the managed one. + var fakeConfig = new Realms.Sync.AppConfiguration("foo-bar") + { + BaseUri = new Uri("http://localhost:12345"), + BaseFilePath = Path.Combine(InteropConfig.GetDefaultStorageFolder("no error expected"), Guid.NewGuid().ToString()) + }; + Directory.CreateDirectory(fakeConfig.BaseFilePath); + + var fakeApp = CreateApp(fakeConfig); + + var realm = GetRealm(GetFakeConfig(fakeApp)); + var session = GetSession(realm); + + Assert.That(session.User.App.BaseFilePath, Is.EqualTo(fakeConfig.BaseFilePath)); + Assert.That(session.User.App.BaseUri, Is.EqualTo(fakeConfig.BaseUri)); + } + private static ClientResetHandlerBase GetClientResetHandler( Type type, BeforeResetCallback? beforeCb = null, diff --git a/wrappers/src/app_cs.cpp b/wrappers/src/app_cs.cpp index fbc150ce7b..88bf887a41 100644 --- a/wrappers/src/app_cs.cpp +++ b/wrappers/src/app_cs.cpp @@ -312,6 +312,21 @@ extern "C" { App::clear_cached_apps(); } + REALM_EXPORT size_t shared_app_get_base_file_path(SharedApp& app, uint16_t* buffer, size_t buffer_length, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]() { + std::string base_file_path(app->sync_manager()->config().base_file_path); + return stringdata_to_csharpstringbuffer(base_file_path, buffer, buffer_length); + }); + } + + REALM_EXPORT realm_string_t shared_app_get_base_uri(SharedApp& app, uint16_t* buffer, size_t buffer_length, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]() { + return to_capi(app->base_url()); + }); + } + #pragma region EmailPassword REALM_EXPORT void shared_app_email_register_user(SharedApp& app, uint16_t* username_buf, size_t username_len, uint16_t* password_buf, size_t password_len, void* tcs_ptr, NativeException::Marshallable& ex)