diff --git a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs index 60f585a4e7..f831218c51 100644 --- a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs +++ b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs @@ -90,7 +90,7 @@ public static void RunBaasTestAsync(Func testFunc, int timeout = 30000) Task.Delay(1000).Wait(); } - public static string GetVerifiedUsername() => $"realm_tests_do_autoverify-{Guid.NewGuid()}"; + public static string GetVerifiedUsername() => $"realm_tests_do_autoverify-{Guid.NewGuid()}@g.it"; public static string GetUnconfirmedUsername() => $"realm_tests_do_not_confirm-{Guid.NewGuid()}@g.it"; diff --git a/Tests/Realm.Tests/Sync/UserManagementTests.cs b/Tests/Realm.Tests/Sync/UserManagementTests.cs index 505e4ca335..705e7194ee 100644 --- a/Tests/Realm.Tests/Sync/UserManagementTests.cs +++ b/Tests/Realm.Tests/Sync/UserManagementTests.cs @@ -400,7 +400,7 @@ public void User_LinkCredentials_WhenInUse_Throws() } [Test] - public void User_RetryCustomConfirmationAsync_WorksInAllScenarios() + public void User_RetryCustomConfirmationAsync_RerunsConfirmation() { SyncTestHelpers.RunBaasTestAsync(async () => { @@ -435,6 +435,84 @@ public void User_RetryCustomConfirmationAsync_WorksInAllScenarios() }); } + [Test] + public void User_ConfirmUserAsync_ConfirmsUser() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + 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, token and tokenId in a collection. + await DefaultApp.EmailPasswordAuth.RegisterUserAsync(unconfirmedMail, SyncTestHelpers.DefaultPassword).Timeout(10_000, detail: "Failed to register user"); + + var ex = await TestHelpers.AssertThrows(() => DefaultApp.LogInAsync(credentials)); + Assert.That(ex.Message, Does.Contain("confirmation required")); + + // This retrieves the token and tokenId we saved in the confirmation function + var functionUser = await GetUserAsync(); + var result = await functionUser.Functions.CallAsync("confirmationInfo", unconfirmedMail); + var token = result["token"].AsString; + var tokenId = result["tokenId"].AsString; + + await DefaultApp.EmailPasswordAuth.ConfirmUserAsync(token, tokenId); + var user = await DefaultApp.LogInAsync(credentials); + Assert.That(user.State, Is.EqualTo(UserState.LoggedIn)); + }); + } + + [Test] + public void User_CallResetPasswordFunctionAsync_ResetsUserPassword() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + var user = await GetUserAsync(); + var email = user.Profile.Email!; + + await user.LogOutAsync(); + Assert.That(user.State, Is.EqualTo(UserState.Removed)); + + var newPassword = "realm_tests_do_reset-testPassword"; + await DefaultApp.EmailPasswordAuth.CallResetPasswordFunctionAsync(email, newPassword); + + user = await DefaultApp.LogInAsync(Credentials.EmailPassword(email, newPassword)); + Assert.That(user.State, Is.EqualTo(UserState.LoggedIn)); + }); + } + + [Test] + public void User_ResetPasswordAsync_ConfirmsResetPassword() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + var user = await GetUserAsync(); + var email = user.Profile.Email!; + + await user.LogOutAsync(); + Assert.That(user.State, Is.EqualTo(UserState.Removed)); + + // This returns "pending" the first time, so the password change is not valid yet. We save the token and tokenId + // passed to the reset function, to confirm the password change later. + var newPassword = "realm_tests_do_not_reset-testPassword"; + await DefaultApp.EmailPasswordAuth.CallResetPasswordFunctionAsync(email, newPassword); + + var ex = await TestHelpers.AssertThrows(() => DefaultApp.LogInAsync(Credentials.EmailPassword(email, newPassword))); + Assert.That(ex.Message, Does.Contain("invalid username/password")); + + // This retrieves the token and tokenId we saved in the password reset function. + var functionUser = await GetUserAsync(); + var result = await functionUser.Functions.CallAsync("resetInfo", email); + var token = result["token"].AsString; + var tokenId = result["tokenId"].AsString; + + await DefaultApp.EmailPasswordAuth.ResetPasswordAsync(newPassword, token, tokenId); + + user = await DefaultApp.LogInAsync(Credentials.EmailPassword(email, newPassword)); + Assert.That(user.State, Is.EqualTo(UserState.LoggedIn)); + }); + } + [Test] public void User_JWT_LogsInAndReadsDataFromToken() { diff --git a/Tools/DeployApps/BaasClient.cs b/Tools/DeployApps/BaasClient.cs index 473b1678aa..ff66f19af0 100644 --- a/Tools/DeployApps/BaasClient.cs +++ b/Tools/DeployApps/BaasClient.cs @@ -120,11 +120,32 @@ public class FunctionReturn };"; private const string ResetFuncSource = - @"exports = ({ token, tokenId, username, password }) => { + @"exports = async function ({ token, tokenId, username, password, currentPasswordValid }) { // process the reset token, tokenId, username and password if (password.includes(""realm_tests_do_reset"")) { return { status: 'success' }; } + + if (password.includes(""realm_tests_do_not_reset"")) { + const mongodb = context.services.get('BackingDB'); + let collection = mongodb.db('test_db').collection('not_reset'); + 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' }; + } + // will not reset the password return { status: 'fail' }; };"; @@ -148,6 +169,20 @@ public class FunctionReturn } };"; + private const string ConfirmationInfoFuncSource = + @"exports = async function(username){ + const mongodb = context.services.get('BackingDB'); + let collection = mongodb.db('test_db').collection('not_confirmed'); + return await collection.findOne({'email': username}); + };"; + + private const string ResetPasswordInfoFuncSource = + @"exports = async function(username){ + const mongodb = context.services.get('BackingDB'); + let collection = mongodb.db('test_db').collection('not_reset'); + return await collection.findOne({'email': username}); + };"; + private readonly HttpClient _client = new(); private readonly string? _clusterName; @@ -302,6 +337,8 @@ private async Task CreateDefaultApp(string name) };"); await CreateFunction(app, "triggerClientResetOnSyncServer", TriggerClientResetOnSyncServerFuncSource, runAsSystem: true); + await CreateFunction(app, "confirmationInfo", ConfirmationInfoFuncSource, runAsSystem: true); + await CreateFunction(app, "resetInfo", ResetPasswordInfoFuncSource, runAsSystem: true); await CreateFunction(app, "documentFunc", @"exports = function(first, second){ return {