Skip to content

Commit

Permalink
Merge branch 'main' into ni/evg-tests
Browse files Browse the repository at this point in the history
* main:
  Prepare for vNext (#3473)
  Prepare for 11.6.0 (#3470)
  Added RetryCustomConfirmationAsync (#3468)
  • Loading branch information
nirinchev committed Nov 4, 2023
2 parents 15ebc40 + 057dce8 commit 805bb7f
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 17 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
## 11.5.0 (2023-09-15)

### Enhancements
* None

### Fixed
* None

### Compatibility
* Realm Studio: 13.0.0 or later.

### Internal
* Using Core x.y.z.

## 11.6.0 (2023-11-03)

### Enhancements
* Added the `App.EmailPasswordAuth.RetryCustomConfirmationAsync` method to be able to run again the confirmation function on the server for a given email. (Issue [#3463](https://github.com/realm/realm-dotnet/issues/3463))
* Added `User.Changed` event that can be used to notify subscribers that something about the user changed - typically this would be the user state or the access token. (Issue [#3429](https://github.com/realm/realm-dotnet/issues/3429))
* Added support for customizing the ignore attribute applied on certain generated properties of Realm models. The configuration option is called `realm.custom_ignore_attribute` and can be set in a global configuration file (more information about global configuration files can be found in the [.NET documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files)). The Realm generator will treat this as an opaque string, that will be appended to the `IgnoreDataMember` and `XmlIgnore` attributes already applied on these members. The attributes must be fully qualified unless the namespace they reside in is added to a global usings file. For example, this is how you would add `JsonIgnore` from `System.Text.Json`:

Expand Down
2 changes: 1 addition & 1 deletion Realm/AssemblyInfo.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Product Condition="'$(Product)' == ''">Realm .NET</Product>
<VersionPrefix>11.5.0</VersionPrefix>
<VersionPrefix>11.6.0</VersionPrefix>
<Description Condition="'$(Description)' == ''">Realm is a mobile database: a replacement for SQLite</Description>
<Company>Realm Inc.</Company>
<Copyright>Copyright © $([System.DateTime]::Now.ToString(yyyy)) Realm Inc.</Copyright>
Expand Down
2 changes: 1 addition & 1 deletion Realm/Realm.Unity/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "io.realm.unity",
"version": "11.5.0",
"version": "11.6.0",
"displayName": "Realm",
"description": "Realm is an embedded, object-oriented database that lets you build real-time, always-on applications. With Realm, data is directly exposed as objects and queryable by code, removing the need for ORM's riddled with performance & maintenance issues. Additionally, objects and collections in Realm are always live, meaning that they always reflect the latest data stored in the database. You can subscribe to changes, letting you keep your UI consistently up to date.\nThe .NET Realm SDK also provide access to Atlas App Services, a secure backend that can sync data between devices, authenticate and manage users, and run serverless JavaScript functions.",
"unity": "2021.1",
Expand Down
22 changes: 22 additions & 0 deletions Realm/Realm/Handles/AppHandle.EmailPassword.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public static extern void resend_confirmation_email(AppHandle app,
[MarshalAs(UnmanagedType.LPWStr)] string email, IntPtr email_len,
IntPtr tcs_ptr, out NativeException ex);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_email_retry_custom_confirmation", CallingConvention = CallingConvention.Cdecl)]
public static extern void retry_custom_comfirmation(AppHandle app,
[MarshalAs(UnmanagedType.LPWStr)] string email, IntPtr email_len,
IntPtr tcs_ptr, out NativeException ex);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_email_send_reset_password_email", CallingConvention = CallingConvention.Cdecl)]
public static extern void send_reset_password_email(AppHandle app,
[MarshalAs(UnmanagedType.LPWStr)] string email, IntPtr email_len,
Expand Down Expand Up @@ -125,6 +130,23 @@ public async Task ResendConfirmationEmailAsync(string email)
}
}

public async Task RetryCustomConfirmationAsync(string email)
{
var tcs = new TaskCompletionSource();
var tcsHandle = GCHandle.Alloc(tcs);

try
{
EmailNativeMethods.retry_custom_comfirmation(_appHandle, email, (IntPtr)email.Length, GCHandle.ToIntPtr(tcsHandle), out var ex);
ex.ThrowIfNecessary();
await tcs.Task;
}
finally
{
tcsHandle.Free();
}
}

public async Task SendResetPasswordEmailAsync(string username)
{
var tcs = new TaskCompletionSource();
Expand Down
15 changes: 15 additions & 0 deletions Realm/Realm/Sync/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,21 @@ public Task ResendConfirmationEmailAsync(string email)
return _app.Handle.EmailPassword.ResendConfirmationEmailAsync(email);
}

/// <summary>
/// Rerun the custom confirmation function for the given mail.
/// </summary>
/// <param name="email">The email of the user.</param>
/// <returns>
/// An awaitable <see cref="Task"/> representing the asynchronous request to the server that the custom confirmation function is run again. Successful
/// completion indicates that the user has been confirmed on the server.
/// </returns>
public Task RetryCustomConfirmationAsync(string email)
{
Argument.NotNullOrEmpty(email, nameof(email));

return _app.Handle.EmailPassword.RetryCustomConfirmationAsync(email);
}

/// <summary>
/// Sends a password reset email to the specified address.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions Tests/Realm.Tests/Sync/SyncTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public static void RunBaasTestAsync(Func<Task> testFunc, int timeout = 30000)

public static string GetVerifiedUsername() => $"realm_tests_do_autoverify-{Guid.NewGuid()}";

public static string GetUnconfirmedUsername() => $"realm_tests_do_not_confirm-{Guid.NewGuid()}@g.it";

public static async Task TriggerClientResetOnServer(SyncConfigurationBase config)
{
var userId = config.User.Id;
Expand Down
36 changes: 36 additions & 0 deletions Tests/Realm.Tests/Sync/UserManagementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,42 @@ public void User_LinkCredentials_WhenInUse_Throws()
});
}

[Test]
public void User_RetryCustomConfirmationAsync_WorksInAllScenarios()
{
SyncTestHelpers.RunBaasTestAsync(async () =>
{
// Standard case
var unconfirmedMail = SyncTestHelpers.GetUnconfirmedUsername();
var credentials = Credentials.EmailPassword(unconfirmedMail, SyncTestHelpers.DefaultPassword);

// The first time the confirmation function is called we return "pending", so the user needs to be confirmed.
// At the same time we save the user email in a collection.
await DefaultApp.EmailPasswordAuth.RegisterUserAsync(unconfirmedMail, SyncTestHelpers.DefaultPassword).Timeout(10_000, detail: "Failed to register user");

var ex3 = await TestHelpers.AssertThrows<AppException>(() => DefaultApp.LogInAsync(credentials));
Assert.That(ex3.Message, Does.Contain("confirmation required"));

// The second time we call the confirmation function we find the email we saved in the collection and return "success", so the user
// gets confirmed and can log in.
await DefaultApp.EmailPasswordAuth.RetryCustomConfirmationAsync(unconfirmedMail);
var user = await DefaultApp.LogInAsync(credentials);
Assert.That(user.State, Is.EqualTo(UserState.LoggedIn));

// Logged in user case
var loggedInUser = await GetUserAsync();
var ex = await TestHelpers.AssertThrows<AppException>(() => DefaultApp.EmailPasswordAuth.RetryCustomConfirmationAsync(loggedInUser.Profile.Email!));
Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
Assert.That(ex.Message, Does.Contain("already confirmed"));

// Unknown user case
var invalidEmail = "test@gmail.com";
var ex2 = await TestHelpers.AssertThrows<AppException>(() => DefaultApp.EmailPasswordAuth.RetryCustomConfirmationAsync(invalidEmail));
Assert.That(ex2.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
Assert.That(ex2.Message, Does.Contain("user not found"));
});
}

[Test]
public void User_JWT_LogsInAndReadsDataFromToken()
{
Expand Down
59 changes: 44 additions & 15 deletions Tools/DeployApps/BaasClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -88,12 +89,33 @@ public class FunctionReturn
}

private const string ConfirmFuncSource =
@"exports = ({ token, tokenId, username }) => {
@"exports = async function ({ token, tokenId, username }) {
// process the confirm token, tokenId and username
if (username.includes(""realm_tests_do_autoverify"")) {
return { status: 'success' }
return { status: 'success' };
}
// do not confirm the user
if (username.includes(""realm_tests_do_not_confirm"")) {
const mongodb = context.services.get('BackingDB');
let collection = mongodb.db('test_db').collection('not_confirmed');
let result = await collection.findOne({'email': username});
if(result === null)
{
let newVal = {
'email': username,
'token': token,
'tokenId': tokenId,
}
await collection.insertOne(newVal);
return { status: 'pending' };
}
return { status: 'success' };
}
// fail the user confirmation
return { status: 'fail' };
};";

Expand Down Expand Up @@ -397,27 +419,34 @@ private async Task<BaasApp> CreateFlxApp(string name)
{
_output.WriteLine($"Creating FLX app {name}...");

var (app, _) = await CreateAppCore(name, new
var (app, mongoServiceId) = await CreateAppCore(name, new
{
flexible_sync = new
{
state = "enabled",
database_name = $"FLX_{Differentiator}",
queryable_fields_names = new[] { "Int64Property", "GuidProperty", "DoubleProperty", "Int", "Guid", "Id", "PartitionLike" },
permissions = new
}
});

await PostAsync<BsonDocument>($"groups/{_groupId}/apps/{app}/services/{mongoServiceId}/default_rule", new
{
roles = new[]
{
new
{
rules = new { },
defaultRoles = new[]
name = "all",
apply_when = new { },
read = true,
write = true,
insert = true,
delete = true,
document_filters = new
{
new
{
name = "all",
applyWhen = new { },
read = true,
write = true,
}
read = true,
write = true,
}
},
}
}
});

Expand Down
8 changes: 8 additions & 0 deletions wrappers/src/app_cs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,14 @@ extern "C" {
});
}

REALM_EXPORT void shared_app_email_retry_custom_confirmation(SharedApp& app, uint16_t* email_buf, size_t email_len, void* tcs_ptr, NativeException::Marshallable& ex)
{
handle_errors(ex, [&]() {
Utf16StringAccessor email(email_buf, email_len);
app->provider_client<App::UsernamePasswordProviderClient>().retry_custom_confirmation(email, get_callback_handler(tcs_ptr));
});
}

REALM_EXPORT void shared_app_email_send_reset_password_email(SharedApp& app, uint16_t* email_buf, size_t email_len, void* tcs_ptr, NativeException::Marshallable& ex)
{
handle_errors(ex, [&]() {
Expand Down

0 comments on commit 805bb7f

Please sign in to comment.