From 9e23ee14bb16a0397d2dc6479f9b0b0849a8705d Mon Sep 17 00:00:00 2001 From: Mohammad Ebrahimi Date: Sun, 27 Oct 2024 22:06:07 +0330 Subject: [PATCH] feat(templates): add tests for change phone number in Boilerplate #9027 (#9028) --- .../.template.config/template.json | 1 - .../Services/PhoneService.cs | 4 +- .../WebApplicationBuilderExtensions.cs | 2 + .../src/Tests/IdentityApiTests.cs | 4 +- .../src/Tests/IdentityPagesTests.cs | 6 +- .../src/Tests/PageTests/IdentityPagesTests.cs | 163 +++++++++++++----- .../PageModels/Email/ConfirmationEmail.cs | 4 +- .../PageModels/Email/ResetPasswordEmail.cs | 2 +- .../PageModels/Email/TokenMagicLinkEmail.cs | 2 +- .../PageModels/Identity/ConfirmPage.cs | 2 +- .../PageModels/Identity/ForgotPasswordPage.cs | 16 +- .../PageModels/Identity/ResetPasswordPage.cs | 2 +- .../Identity/SettingsPage.Account.Email.cs | 84 +++++++++ .../Identity/SettingsPage.Account.Phone.cs | 84 +++++++++ .../PageModels/Identity/SettingsPage.cs | 75 +------- .../PageModels/Identity/SignInPage.cs | 77 ++++++++- .../PageModels/Identity/SignUpPage.cs | 2 +- .../PageModels/Layout/IdentityLayout.cs | 4 +- .../Tests/Services/CulturedStringLocalizer.cs | 2 + .../src/Tests/Services/EmailReaderService.cs | 15 +- .../src/Tests/Services/FakePhoneService.cs | 72 ++++++++ .../Bit.Boilerplate/src/Tests/TestData.cs | 8 + .../src/Tests/TestsInitializer.cs | 2 +- 23 files changed, 500 insertions(+), 133 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Phone.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestData.cs diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index 225a844029..ab4bd3a572 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -486,7 +486,6 @@ "condition": "(advancedTests != true)", "exclude": [ "src/Tests/PageTests/**", - "src/Tests/TestInitializer.cs", "src/Tests/Services/CulturedStringLocalizer.cs", "src/Tests/Services/EmailReaderService.cs", "src/Tests/Services/UserService.cs" diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs index 6097896c0b..cf18c4371b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs @@ -11,7 +11,7 @@ public partial class PhoneService [AutoInject] private readonly IHttpContextAccessor httpContextAccessor = default!; [AutoInject] private readonly PhoneNumberUtil phoneNumberUtil = default!; - public string? NormalizePhoneNumber(string? phoneNumber) + public virtual string? NormalizePhoneNumber(string? phoneNumber) { if (string.IsNullOrEmpty(phoneNumber)) return null; @@ -26,7 +26,7 @@ public partial class PhoneService return phoneNumberUtil.Format(parsedPhoneNumber, PhoneNumberFormat.E164); } - public async Task SendSms(string messageText, string phoneNumber, CancellationToken cancellationToken) + public virtual async Task SendSms(string messageText, string phoneNumber, CancellationToken cancellationToken) { if (hostEnvironment.IsDevelopment()) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/WebApplicationBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/WebApplicationBuilderExtensions.cs index 206cee3377..0ee8cf8496 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/WebApplicationBuilderExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/WebApplicationBuilderExtensions.cs @@ -13,6 +13,8 @@ public static void AddTestProjectServices(this WebApplicationBuilder builder) builder.AddServerWebProjectServices(); + services.AddTransient(); + //#if (captcha == "reCaptcha") services.AddTransient(); //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs index 33a5fd306a..dde2251bdb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs @@ -26,8 +26,8 @@ await server.Build(services => await authenticationManager.SignIn(new() { - Email = "test@bitplatform.dev", - Password = "123456" + Email = TestData.DefaultTestEmail, + Password = TestData.DefaultTestPassword }, default); var userController = scope.ServiceProvider.GetRequiredService(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs index 4ca2381f7f..af6bd8dc03 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs @@ -28,9 +28,9 @@ public async Task SignIn_Should_WorkAsExpected() await Expect(Page).ToHaveTitleAsync(AppStrings.SignInPageTitle); - const string email = "test@bitplatform.dev"; - const string password = "123456"; - const string userFullName = "Boilerplate test account"; + const string email = TestData.DefaultTestEmail; + const string password = TestData.DefaultTestPassword; + const string userFullName = TestData.DefaultTestFullName; await Page.GetByPlaceholder(AppStrings.EmailPlaceholder).FillAsync(email); await Page.GetByPlaceholder(AppStrings.PasswordPlaceholder).FillAsync(password); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs index 058f41e162..f737948a48 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs @@ -33,11 +33,11 @@ public async Task SignIn(string mode) switch (mode) { case "ValidCredentials": - var identityHomePage = await signInPage.SignIn(); + var identityHomePage = await signInPage.SignInWithEmail(); await identityHomePage.AssertSignInSuccess(); break; case "InvalidCredentials": - await signInPage.SignIn(email: "invalid@bitplatform.dev", password: "invalid"); + await signInPage.SignInWithEmail(email: "invalid@bitplatform.dev", password: "invalid"); await signInPage.AssertSignInFailed(); break; default: @@ -61,7 +61,7 @@ public async Task SignOut() await signInPage.Open(); await signInPage.AssertOpen(); - var identityHomePage = await signInPage.SignIn(email); + var identityHomePage = await signInPage.SignInWithEmail(email); await identityHomePage.AssertSignInSuccess(email, userFullName: null); await dbContext.Entry(user).ReloadAsync(); @@ -118,20 +118,24 @@ public async Task SignUp(string mode) [DataRow("Token")] [DataRow("InvalidToken")] [DataRow("MagicLink")] + [DataRow("TooManyRequests")] + [DataRow("NotExisted")] public async Task ForgotPassword(string mode) { - await using var scope = TestServer.WebApp.Services.CreateAsyncScope(); - - var dbContext = scope.ServiceProvider.GetRequiredService(); - var userService = new UserService(dbContext); - var email = $"{Guid.NewGuid()}@gmail.com"; - await userService.AddUser(email); + var email = await CreateNewUser(); var forgotPasswordPage = new ForgotPasswordPage(Page, WebAppServerAddress); await forgotPasswordPage.Open(); await forgotPasswordPage.AssertOpen(); + if (mode is "NotExisted") + { + await forgotPasswordPage.ForgotPassword("not-existed@bitplatform.dev"); + await forgotPasswordPage.AssertUserNotFound(); + return; + } + var resetPasswordPage = await forgotPasswordPage.ForgotPassword(email); await resetPasswordPage.AssertOpen(); @@ -145,14 +149,19 @@ public async Task ForgotPassword(string mode) var token = await resetPasswordEmail.GetToken(); await resetPasswordPage.ContinueByToken(token); break; + case "MagicLink": + resetPasswordPage = await resetPasswordEmail.OpenMagicLink(); + break; case "InvalidToken": await resetPasswordPage.ContinueByToken("111111"); await resetPasswordPage.SetPassword(newPassword); await resetPasswordPage.AssertInvalidToken(); return; - case "MagicLink": - resetPasswordPage = await resetPasswordEmail.OpenMagicLink(); - break; + case "TooManyRequests": + await Page.GoBackAsync(); + await forgotPasswordPage.ForgotPassword(email); + await forgotPasswordPage.AssertTooManyRequests(); + return; default: throw new NotSupportedException(); } @@ -166,7 +175,7 @@ public async Task ForgotPassword(string mode) await signInPage.Open(); await signInPage.AssertOpen(); - var identityHomePage = await signInPage.SignIn(email, newPassword); + var identityHomePage = await signInPage.SignInWithEmail(email, newPassword); await identityHomePage.AssertSignInSuccess(email, userFullName: null); } @@ -174,64 +183,140 @@ public async Task ForgotPassword(string mode) [DataRow("Token")] [DataRow("InvalidToken")] [DataRow("MagicLink")] + [DataRow("TooManyRequests")] public async Task ChangeEmail(string mode) { - await using var scope = TestServer.WebApp.Services.CreateAsyncScope(); - - var dbContext = scope.ServiceProvider.GetRequiredService(); - var userService = new UserService(dbContext); - var email = $"{Guid.NewGuid()}@gmail.com"; - await userService.AddUser(email); + var email = await CreateNewUser(); var signInPage = new SignInPage(Page, WebAppServerAddress); await signInPage.Open(); await signInPage.AssertOpen(); - var identityHomePage = await signInPage.SignIn(email); + var identityHomePage = await signInPage.SignInWithEmail(email); await identityHomePage.AssertSignInSuccess(email, userFullName: null); - var settinsPage = new SettingsPage(Page, WebAppServerAddress); + var settingsPage = new SettingsPage(Page, WebAppServerAddress); - await settinsPage.Open(); - await settinsPage.AssertOpen(); + await settingsPage.Open(); + await settingsPage.AssertOpen(); - await settinsPage.ExpandAccount(); - await settinsPage.AssertExpandAccount(email); + await settingsPage.ExpandAccount(); + await settingsPage.AssertExpandAccount(email); var newEmail = $"{Guid.NewGuid()}@gmail.com"; - await settinsPage.ChangeEmail(newEmail); - await settinsPage.AssertChangeEmail(); + await settingsPage.ChangeEmail(newEmail); + await settingsPage.AssertChangeEmail(); - var confirmationEmail = await settinsPage.OpenConfirmationEmail(); + var confirmationEmail = await settingsPage.OpenConfirmationEmail(); await confirmationEmail.AssertContent(); switch (mode) { case "Token": var token = await confirmationEmail.GetToken(); - await settinsPage.ConfirmByToken(token); + await settingsPage.ConfirmEmailByToken(token); break; - case "InvalidToken": - await settinsPage.ConfirmByToken("111111"); - await settinsPage.AssertInvalidToken(); - return; case "MagicLink": - settinsPage = await confirmationEmail.OpenMagicLink(); + settingsPage = await confirmationEmail.OpenMagicLink(); break; + case "InvalidToken": + await settingsPage.ConfirmEmailByToken("111111"); + await settingsPage.AssertEmailInvalidToken(); + return; + case "TooManyRequests": + await settingsPage.ClickOnPhoneTab(); + await settingsPage.ClickOnEmailTab(); + await settingsPage.ChangeEmail(newEmail); + await settingsPage.AssertTooManyRequestsForChangeEmail(); + return; default: throw new NotSupportedException(); } - await settinsPage.AssertConfirmSuccess(); + await settingsPage.AssertConfirmEmailSuccess(); - signInPage = await settinsPage.SignOut(); + signInPage = await settingsPage.SignOut(); await signInPage.AssertOpen(); await signInPage.AssertSignOut(); - await signInPage.SignIn(email); + await signInPage.SignInWithEmail(email); await signInPage.AssertSignInFailed(); - settinsPage = await signInPage.SignIn(newEmail); - await settinsPage.AssertSignInSuccess(newEmail, userFullName: null); + settingsPage = await signInPage.SignInWithEmail(newEmail); + await settingsPage.AssertSignInSuccess(newEmail, userFullName: null); + } + + [TestMethod] + [DataRow("Token")] + [DataRow("InvalidToken")] + [DataRow("TooManyRequests")] + public async Task ChangePhone(string mode) + { + var email = await CreateNewUser(); + + var signInPage = new SignInPage(Page, WebAppServerAddress); + + await signInPage.Open(); + await signInPage.AssertOpen(); + + var identityHomePage = await signInPage.SignInWithEmail(email); + await identityHomePage.AssertSignInSuccess(email, userFullName: null); + + var settingsPage = new SettingsPage(Page, WebAppServerAddress); + + await settingsPage.Open(); + await settingsPage.AssertOpen(); + + await settingsPage.ExpandAccount(); + await settingsPage.ClickOnPhoneTab(); + await settingsPage.AssertPhoneTab(null); + + var phone = $"+1{Random.Shared.Next(1111111111, int.MaxValue)}"; + await settingsPage.ChangePhone(phone); + await settingsPage.AssertChangePhone(); + + switch (mode) + { + case "Token": + var token = settingsPage.GetPhoneToken(); + await settingsPage.ConfirmPhoneByToken(token); + + await settingsPage.AssertConfirmPhoneSuccess(); + + signInPage = await settingsPage.SignOut(); + await signInPage.AssertOpen(); + await signInPage.AssertSignOut(); + + await signInPage.ClickOnPhoneTab(); + await signInPage.AssertPhoneTab(); + + settingsPage = await signInPage.SignInWithPhone(phone); + await settingsPage.AssertSignInSuccess(email, userFullName: null); + return; + case "InvalidToken": + await settingsPage.ConfirmPhoneByToken("111111"); + await settingsPage.AssertPhoneInvalidToken(); + return; + case "TooManyRequests": + await settingsPage.ClickOnEmailTab(); + await settingsPage.ClickOnPhoneTab(); + await settingsPage.ChangePhone(phone); + await settingsPage.AssertTooManyRequestsForChangePhone(); + return; + default: + throw new NotSupportedException(); + } + } + + private async Task CreateNewUser() + { + await using var scope = TestServer.WebApp.Services.CreateAsyncScope(); + + var dbContext = scope.ServiceProvider.GetRequiredService(); + var userService = new UserService(dbContext); + var email = $"{Guid.NewGuid()}@gmail.com"; + await userService.AddUser(email); + + return email; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ConfirmationEmail.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ConfirmationEmail.cs index 2984226837..829a0ef0e7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ConfirmationEmail.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ConfirmationEmail.cs @@ -8,8 +8,8 @@ public partial class ConfirmationEmail(IBrowserContext context, Uri serve : TokenMagicLinkEmail(context, serverAddress) where TPage : RootLayout { - protected override bool WaitForRedirectOnMagicLink => true; - protected override string EmailSubject => EmailStrings.ConfirmationEmailSubject.Replace("{0}", "\\d{6}"); + protected override bool WaitForRedirectOnMagicLink => false; + protected override string EmailSubject => EmailStrings.ConfirmationEmailSubject.Replace("{0}", @"\b\d{6}\b"); protected override async Task AssertContentCore() { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ResetPasswordEmail.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ResetPasswordEmail.cs index 70cf9d8d1a..1f5afbb006 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ResetPasswordEmail.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/ResetPasswordEmail.cs @@ -8,7 +8,7 @@ public partial class ResetPasswordEmail(IBrowserContext context, Uri serverAddre : TokenMagicLinkEmail(context, serverAddress) { protected override bool WaitForRedirectOnMagicLink => false; - protected override string EmailSubject => EmailStrings.ResetPasswordEmailSubject.Replace("{0}", "\\d{6}"); + protected override string EmailSubject => EmailStrings.ResetPasswordEmailSubject.Replace("{0}", @"\b\d{6}\b"); protected override async Task AssertContentCore() { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs index 0d94b0ed1d..57c2b2d857 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs @@ -35,7 +35,7 @@ public virtual async Task GetToken() { Assert.IsNotNull(Page, OpenEmailFirstMessage); - var token = await Page.GetByText(new Regex("^\\d{6}$")).TextContentAsync(); + var token = await Page.GetByText(new Regex(@"^\d{6}$")).TextContentAsync(); Assert.IsNotNull(token, "Confirmation token not found in email"); return token; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ConfirmPage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ConfirmPage.cs index 78f6761fd8..de64bf5c0b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ConfirmPage.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ConfirmPage.cs @@ -27,7 +27,7 @@ public override async Task AssertOpen() { await Assertions.Expect(emailInput).ToBeVisibleAsync(); await Assertions.Expect(emailInput).ToBeDisabledAsync(); - await Assertions.Expect(emailInput).ToBeEditableAsync(new() { Editable = false }); + await Assertions.Expect(emailInput).Not.ToBeEditableAsync(); } await Assertions.Expect(Page.GetByPlaceholder(AppStrings.EmailTokenPlaceholder)).ToBeVisibleAsync(); await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.EmailTokenConfirmButtonText })).ToBeVisibleAsync(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ForgotPasswordPage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ForgotPasswordPage.cs index df43e86f4f..5b6b654c30 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ForgotPasswordPage.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ForgotPasswordPage.cs @@ -1,4 +1,5 @@ -using Boilerplate.Tests.PageTests.PageModels.Email; +using System.Text.RegularExpressions; +using Boilerplate.Tests.PageTests.PageModels.Email; using Boilerplate.Tests.PageTests.PageModels.Layout; namespace Boilerplate.Tests.PageTests.PageModels.Identity; @@ -24,7 +25,7 @@ public override async Task AssertOpen() await Assertions.Expect(resetPasswordLink).ToHaveAttributeAsync("href", Urls.ResetPasswordPage); } - public async Task ForgotPassword(string email = "test@bitplatform.dev") + public async Task ForgotPassword(string email = TestData.DefaultTestEmail) { this.email = email; await Page.GetByPlaceholder(AppStrings.EmailPlaceholder).FillAsync(email); @@ -33,6 +34,17 @@ public async Task ForgotPassword(string email = "test@bitplat return new(Page, WebAppServerAddress) { EmailAddress = email }; } + public async Task AssertUserNotFound() + { + await Assertions.Expect(Page.GetByText(AppStrings.UserNotFound)).ToBeVisibleAsync(); + } + + public async Task AssertTooManyRequests() + { + var pattern = new Regex(AppStrings.WaitForResetPasswordTokenRequestResendDelay.Replace("{0}", ".*")); + await Assertions.Expect(Page.GetByText(pattern)).ToBeVisibleAsync(); + } + public async Task OpenResetPasswordEmail() { Assert.IsNotNull(email, $"Call {nameof(ForgotPassword)} method first."); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ResetPasswordPage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ResetPasswordPage.cs index 8ed6bb8abd..79c86fe8c3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ResetPasswordPage.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/ResetPasswordPage.cs @@ -27,7 +27,7 @@ public override async Task AssertOpen() { await Assertions.Expect(emailInput).ToBeVisibleAsync(); await Assertions.Expect(emailInput).ToBeDisabledAsync(); - await Assertions.Expect(emailInput).ToBeEditableAsync(new() { Editable = false }); + await Assertions.Expect(emailInput).Not.ToBeEditableAsync(); } await Assertions.Expect(Page.GetByPlaceholder(AppStrings.TokenPlaceholder)).ToBeVisibleAsync(); await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Continue })).ToBeVisibleAsync(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs new file mode 100644 index 0000000000..b8a4e1d894 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs @@ -0,0 +1,84 @@ +using System.Text.RegularExpressions; +using Boilerplate.Tests.PageTests.PageModels.Email; + +namespace Boilerplate.Tests.PageTests.PageModels.Identity; + +public partial class SettingsPage +{ + public async Task ClickOnEmailTab() + { + await Page.GetByRole(AriaRole.Tab, new() { Name = AppStrings.Email }).ClickAsync(); + } + + public async Task AssertEmailTab(string userEmail = TestData.DefaultTestEmail) + { + var emailInput = Page.GetByLabel(AppStrings.Email, new() { Exact = true }).Locator("span"); + await Assertions.Expect(emailInput).ToBeVisibleAsync(); + await Assertions.Expect(emailInput).ToContainTextAsync(userEmail); + await Assertions.Expect(Page.GetByPlaceholder(AppStrings.NewEmailPlaceholder)).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Submit })).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmMessageInProfile); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Confirm })).ToBeVisibleAsync(); + } + + public async Task ChangeEmail(string newEmail) + { + this.newEmail = newEmail; + await Page.GetByPlaceholder(AppStrings.NewEmailPlaceholder).FillAsync(newEmail); + await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Submit }).ClickAsync(); + } + + public async Task AssertChangeEmail() + { + await Assertions.Expect(Page.GetByText(AppStrings.SuccessfulSendChangeEmailTokenMessage)).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmEmailSubtitle); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmEmailMessage); + var emailInput = Page.GetByPlaceholder(AppStrings.EmailPlaceholder); + await Assertions.Expect(emailInput).ToBeVisibleAsync(); + await Assertions.Expect(emailInput).ToBeDisabledAsync(); + await Assertions.Expect(emailInput).Not.ToBeEditableAsync(); + await Assertions.Expect(emailInput).ToHaveValueAsync(newEmail); + await Assertions.Expect(Page.GetByPlaceholder(AppStrings.EmailTokenPlaceholder)).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.EmailTokenConfirmButtonText })).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.NotReceivedEmailMessage); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.CheckSpamMailMessage); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.ResendEmailTokenButtonText })).ToBeVisibleAsync(); + } + + public async Task AssertTooManyRequestsForChangeEmail() + { + var pattern = new Regex(AppStrings.WaitForEmailTokenRequestResendDelay.Replace("{0}", ".*")); + await Assertions.Expect(Page.GetByText(pattern)).ToBeVisibleAsync(); + } + + public async Task> OpenConfirmationEmail() + { + Assert.IsNotNull(newEmail, $"Call {nameof(ChangeEmail)} method first."); + + var confirmationEmail = new ConfirmationEmail(Page.Context, WebAppServerAddress); + await confirmationEmail.Open(newEmail); + return confirmationEmail; + } + + public async Task ConfirmEmailByToken(string token) + { + await Page.GetByPlaceholder(AppStrings.EmailTokenPlaceholder).FillAsync(token); + await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.EmailTokenConfirmButtonText }).ClickAsync(); + } + + public async Task AssertConfirmEmailSuccess() + { + //TODO: Remove the line below when the problem with refreshing page is solved. + await Task.Delay(1000); + await Page.GotoAsync(new Uri(WebAppServerAddress, PagePath).ToString()); + + await Assertions.Expect(Page.Locator(".bit-prs.persona").Last).ToContainTextAsync(newEmail); + await ExpandAccount(); + await AssertExpandAccount(newEmail); + } + + public async Task AssertEmailInvalidToken() + { + await Assertions.Expect(Page.GetByText(AppStrings.InvalidToken)).ToBeVisibleAsync(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Phone.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Phone.cs new file mode 100644 index 0000000000..2aef385ea8 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Phone.cs @@ -0,0 +1,84 @@ +using System.Text.RegularExpressions; +using Boilerplate.Tests.Services; + +namespace Boilerplate.Tests.PageTests.PageModels.Identity; + +public partial class SettingsPage +{ + public async Task ClickOnPhoneTab() + { + await Page.GetByRole(AriaRole.Tab, new() { Name = AppStrings.Phone }).ClickAsync(); + } + + public async Task AssertPhoneTab(string? phone) + { + var phoneInput = Page.GetByLabel(AppStrings.Phone, new() { Exact = true }).Locator("span"); + if (phone is null) + { + await Assertions.Expect(phoneInput).ToBeHiddenAsync(); + } + else + { + await Assertions.Expect(phoneInput).ToBeVisibleAsync(); + await Assertions.Expect(phoneInput).ToContainTextAsync(phone); + } + await Assertions.Expect(Page.GetByPlaceholder(AppStrings.NewPhoneNumberPlaceholder)).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Submit })).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmMessageInProfile); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Confirm })).ToBeVisibleAsync(); + } + + public async Task ChangePhone(string newPhone) + { + this.newPhone = newPhone; + await Page.GetByPlaceholder(AppStrings.NewPhoneNumberPlaceholder).FillAsync(newPhone); + await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Submit }).ClickAsync(); + } + + public async Task AssertChangePhone() + { + await Assertions.Expect(Page.GetByText(AppStrings.SuccessfulSendChangePhoneNumberTokenMessage)).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmPhoneSubtitle); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmPhoneMessage); + var phoneInput = Page.GetByPlaceholder(AppStrings.PhoneNumberPlaceholder); + await Assertions.Expect(phoneInput).ToBeVisibleAsync(); + await Assertions.Expect(phoneInput).ToBeDisabledAsync(); + await Assertions.Expect(phoneInput).Not.ToBeEditableAsync(); + await Assertions.Expect(phoneInput).ToHaveValueAsync(newPhone); + await Assertions.Expect(Page.GetByPlaceholder(AppStrings.PhoneTokenPlaceholder)).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.PhoneTokenConfirmButtonText })).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.NotReceivedPhoneMessage); + await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.ResendPhoneTokenButtonText })).ToBeVisibleAsync(); + } + + public async Task AssertTooManyRequestsForChangePhone() + { + var pattern = new Regex(AppStrings.WaitForPhoneNumberTokenRequestResendDelay.Replace("{0}", ".*")); + await Assertions.Expect(Page.GetByText(pattern)).ToBeVisibleAsync(); + } + + public string GetPhoneToken() + { + var pattern = AppStrings.ChangePhoneNumberTokenSmsText.Replace("{0}", @"\b\d{6}\b"); + return FakePhoneService.GetLastOtpFor(newPhone, pattern); + } + + public async Task ConfirmPhoneByToken(string token) + { + await Page.GetByPlaceholder(AppStrings.PhoneTokenPlaceholder).FillAsync(token); + await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.PhoneTokenConfirmButtonText }).ClickAsync(); + } + + public async Task AssertConfirmPhoneSuccess() + { + await Task.Delay(1000); //Wait for redirection to complete + await ExpandAccount(); + await ClickOnPhoneTab(); + await AssertPhoneTab(newPhone); + } + + public async Task AssertPhoneInvalidToken() + { + await Assertions.Expect(Page.GetByText(AppStrings.ResourceValidationException)).ToBeVisibleAsync(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.cs index 3e3e47c344..0ea12e049c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.cs @@ -1,5 +1,4 @@ -using Boilerplate.Tests.PageTests.PageModels.Email; -using Boilerplate.Tests.PageTests.PageModels.Layout; +using Boilerplate.Tests.PageTests.PageModels.Layout; using Microsoft.AspNetCore.WebUtilities; namespace Boilerplate.Tests.PageTests.PageModels.Identity; @@ -8,6 +7,7 @@ public partial class SettingsPage(IPage page, Uri serverAddress) : IdentityLayout(page, serverAddress) { private string newEmail = QueryHelpers.ParseQuery(new Uri(page.Url).Query).GetValueOrDefault("email").ToString(); + private string newPhone; public override string PagePath => Urls.SettingsPage; public override string PageTitle => AppStrings.Settings; @@ -23,73 +23,14 @@ public override async Task AssertOpen() public async Task ExpandAccount() { - await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.AccountSubtitle }).ClickAsync(); + var accountButton = Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.AccountSubtitle }); + var isExpanded = (await accountButton.GetAttributeAsync("aria-expanded")) is not null; + if (isExpanded is false) + await accountButton.ClickAsync(); } - public async Task AssertExpandAccount(string userEmail = "test@bitplatform.dev") + public async Task AssertExpandAccount(string userEmail = TestData.DefaultTestEmail) { - var emailInput = Page.GetByLabel(AppStrings.Email, new() { Exact = true }).Locator("span"); - await Assertions.Expect(emailInput).ToBeVisibleAsync(); - await Assertions.Expect(emailInput).ToContainTextAsync(userEmail); - await Assertions.Expect(Page.GetByPlaceholder(AppStrings.NewEmailPlaceholder)).ToBeVisibleAsync(); - await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Submit })).ToBeVisibleAsync(); - await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmMessageInProfile); - await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Confirm })).ToBeVisibleAsync(); - } - - public async Task ChangeEmail(string newEmail) - { - this.newEmail = newEmail; - await Page.GetByPlaceholder(AppStrings.NewEmailPlaceholder).FillAsync(newEmail); - await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.Submit }).ClickAsync(); - } - - public async Task AssertChangeEmail() - { - await Assertions.Expect(Page.GetByText(AppStrings.SuccessfulSendChangeEmailTokenMessage)).ToBeVisibleAsync(); - await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmEmailSubtitle); - await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.ConfirmEmailMessage); - var emailInput = Page.GetByPlaceholder(AppStrings.EmailPlaceholder); - await Assertions.Expect(emailInput).ToBeVisibleAsync(); - await Assertions.Expect(emailInput).ToBeDisabledAsync(); - await Assertions.Expect(emailInput).ToBeEditableAsync(new() { Editable = false }); - await Assertions.Expect(emailInput).ToHaveValueAsync(newEmail); - await Assertions.Expect(Page.GetByPlaceholder(AppStrings.EmailTokenPlaceholder)).ToBeVisibleAsync(); - await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.EmailTokenConfirmButtonText })).ToBeVisibleAsync(); - await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.NotReceivedEmailMessage); - await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.CheckSpamMailMessage); - await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.ResendEmailTokenButtonText })).ToBeVisibleAsync(); - } - - public async Task> OpenConfirmationEmail() - { - Assert.IsNotNull(newEmail, $"Call {nameof(ChangeEmail)} method first."); - - var confirmationEmail = new ConfirmationEmail(Page.Context, WebAppServerAddress); - await confirmationEmail.Open(newEmail); - return confirmationEmail; - } - - public async Task ConfirmByToken(string token) - { - await Page.GetByPlaceholder(AppStrings.EmailTokenPlaceholder).FillAsync(token); - await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.EmailTokenConfirmButtonText }).ClickAsync(); - } - - public async Task AssertConfirmSuccess() - { - //TODO: Remove the line below when the problem with refreshing page is solved. - await Page.RunAndWaitForNavigationAsync(() => Page.ReloadAsync(), new() { WaitUntil = WaitUntilState.NetworkIdle }); - - await Assertions.Expect(Page.Locator(".bit-prs.persona").Last).ToContainTextAsync(newEmail); - await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.AccountSubtitle }).ClickAsync(); - var emailInput = Page.GetByLabel(AppStrings.Email, new() { Exact = true }).Locator("span"); - await Assertions.Expect(emailInput).ToBeVisibleAsync(); - await Assertions.Expect(emailInput).ToContainTextAsync(newEmail); - } - - public async Task AssertInvalidToken() - { - await Assertions.Expect(Page.GetByText(AppStrings.InvalidToken)).ToBeVisibleAsync(); + await AssertEmailTab(userEmail); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignInPage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignInPage.cs index 57782547a0..34c7049b06 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignInPage.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignInPage.cs @@ -13,10 +13,44 @@ public partial class SignInPage(IPage page, Uri serverAddress) public override async Task AssertOpen() { await base.AssertOpen(); + await AssertTab(Tab.Email); + } + + public async Task ClickOnEmailTab() + { + await Page.GetByRole(AriaRole.Tab, new() { Name = AppStrings.Email }).ClickAsync(); + } + + public async Task ClickOnPhoneTab() + { + await Page.GetByRole(AriaRole.Tab, new() { Name = AppStrings.PhoneNumber }).ClickAsync(); + } + public async Task AssertEmailTab() + { + await AssertTab(Tab.Email); + } + + public async Task AssertPhoneTab() + { + await AssertTab(Tab.Phone); + } + + private async Task AssertTab(Tab tab) + { AssertReturnUrl(); await Assertions.Expect(Page.GetByRole(AriaRole.Main)).ToContainTextAsync(AppStrings.SignInPanelSubtitle); - await Assertions.Expect(Page.GetByPlaceholder(AppStrings.EmailPlaceholder)).ToBeVisibleAsync(); + switch (tab) + { + case Tab.Email: + await Assertions.Expect(Page.GetByPlaceholder(AppStrings.EmailPlaceholder)).ToBeVisibleAsync(); + break; + case Tab.Phone: + await Assertions.Expect(Page.GetByPlaceholder(AppStrings.PhoneNumberPlaceholder)).ToBeVisibleAsync(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(tab), tab, null); + } await Assertions.Expect(Page.GetByPlaceholder(AppStrings.PasswordPlaceholder)).ToBeVisibleAsync(); await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.SignIn })).ToBeVisibleAsync(); var forgotPassswordLink = Page.GetByRole(AriaRole.Link, new() { Name = AppStrings.ForgotPasswordLink }); @@ -25,15 +59,42 @@ public override async Task AssertOpen() await Assertions.Expect(Page.GetByLabel(AppStrings.RememberMe)).ToBeCheckedAsync(); } - public async Task SignIn(string email = "test@bitplatform.dev", string password = "123456") + public async Task SignInWithEmail(string email = TestData.DefaultTestEmail, string password = TestData.DefaultTestPassword) { - return await SignIn(email, password); + return await SignInWithEmail(email, password); } - public async Task SignIn(string email = "test@bitplatform.dev", string password = "123456") + public async Task SignInWithEmail(string email = TestData.DefaultTestEmail, string password = TestData.DefaultTestPassword) where TPage : IdentityLayout { - await Page.GetByPlaceholder(AppStrings.EmailPlaceholder).FillAsync(email); + return await SignInCore(Tab.Email, email, password); + } + + public async Task SignInWithPhone(string phone, string password = TestData.DefaultTestPassword) + { + return await SignInWithPhone(phone, password); + } + + public async Task SignInWithPhone(string phone, string password = TestData.DefaultTestPassword) + where TPage : IdentityLayout + { + return await SignInCore(Tab.Phone, phone, password); + } + + private async Task SignInCore(Tab tab, string emailOrPhone, string password) + where TPage : IdentityLayout + { + switch (tab) + { + case Tab.Email: + await Page.GetByPlaceholder(AppStrings.EmailPlaceholder).FillAsync(emailOrPhone); + break; + case Tab.Phone: + await Page.GetByPlaceholder(AppStrings.PhoneNumberPlaceholder).FillAsync(emailOrPhone); + break; + default: + throw new ArgumentOutOfRangeException(nameof(tab), tab, null); + } await Page.GetByPlaceholder(AppStrings.PasswordPlaceholder).FillAsync(password); await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.SignIn }).ClickAsync(); @@ -71,4 +132,10 @@ private void AssertReturnUrl() else Assert.AreEqual(ReturnUrl, returnUrl); } + + private enum Tab + { + Email, + Phone + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignUpPage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignUpPage.cs index 24526e16df..8d6a5e79cd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignUpPage.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SignUpPage.cs @@ -32,7 +32,7 @@ public override async Task AssertOpen() await Assertions.Expect(Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.SignUp, Exact = true })).ToBeVisibleAsync(); } - public async Task SignUp(string email, string password = "123456") + public async Task SignUp(string email, string password = TestData.DefaultTestPassword) { this.email = email; await Page.GetByPlaceholder(AppStrings.EmailPlaceholder).FillAsync(email); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs index d62914e302..26e60e7931 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs @@ -5,7 +5,7 @@ namespace Boilerplate.Tests.PageTests.PageModels.Layout; public abstract partial class IdentityLayout(IPage page, Uri serverAddress) : RootLayout(page, serverAddress) { - public async Task AssertSignInSuccess(string userEmail = "test@bitplatform.dev", string? userFullName = "Boilerplate test account") + public async Task AssertSignInSuccess(string userEmail = TestData.DefaultTestEmail, string? userFullName = TestData.DefaultTestFullName) { var displayName = string.IsNullOrWhiteSpace(userFullName) ? userEmail : userFullName; @@ -25,4 +25,4 @@ public async Task SignOut() return new SignInPage(Page, WebAppServerAddress) { ReturnUrl = new Uri(Page.Url).PathAndQuery.TrimStart('/') }; } -} \ No newline at end of file +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/CulturedStringLocalizer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/CulturedStringLocalizer.cs index c066bfe7a6..161852a12a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/CulturedStringLocalizer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/CulturedStringLocalizer.cs @@ -8,6 +8,8 @@ public static class StringLocalizerFactory public static IStringLocalizer Create(string culture) { + ArgumentException.ThrowIfNullOrEmpty(culture); + var localizer = _stringLocalizerCache.GetOrAdd((culture, typeof(TResourceSource)), (_) => new CulturedStringLocalizer(culture)); return (IStringLocalizer)localizer; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/EmailReaderService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/EmailReaderService.cs index e8b3290d79..4ab1738182 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/EmailReaderService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/EmailReaderService.cs @@ -5,8 +5,11 @@ namespace Boilerplate.Tests.Services; public static partial class EmailReaderService { - public static string GetLastEmailFor(string toMailAddress, string emailSubject) + public static string GetLastEmailFor(string toMailAddress, string subjectPattern) { + ArgumentException.ThrowIfNullOrEmpty(toMailAddress); + ArgumentException.ThrowIfNullOrEmpty(subjectPattern); + var emailsDirectory = Path.Combine(AppContext.BaseDirectory, "App_Data", "sent-emails"); var messages = new DirectoryInfo(emailsDirectory).GetFiles().Select(Message.Load); var message = messages @@ -16,7 +19,15 @@ public static string GetLastEmailFor(string toMailAddress, string emailSubject) Assert.IsNotNull(message, "Email has not sent"); Assert.AreEqual("info@Boilerplate.com", message.Headers.From.Address); //TODO: read from AppSettings.Email.DefaultFromEmail Assert.AreEqual(EmailStrings.DefaultFromName, message.Headers.From.DisplayName); - Assert.IsTrue(Regex.IsMatch(message.Headers.Subject, emailSubject), "Email subject does not match."); + + if (subjectPattern is not null && Regex.IsMatch(message.Headers.Subject, subjectPattern) is false) + { + throw new AssertFailedException($@" + Email subject does not match. + expected pattern: {subjectPattern} + actual subject: {message.Headers.Subject} + "); + } return message.HtmlBody.GetBodyAsText(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs new file mode 100644 index 0000000000..6aea2ffb9d --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs @@ -0,0 +1,72 @@ +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using Boilerplate.Server.Api; +using Boilerplate.Server.Api.Services; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using PhoneNumbers; + +namespace Boilerplate.Tests.Services; + +public partial class FakePhoneService(ServerApiAppSettings appSettings, IHostEnvironment hostEnvironment, IHttpContextAccessor httpContextAccessor, ILogger logger, PhoneNumberUtil phoneNumberUtil) + : PhoneService(appSettings, hostEnvironment, httpContextAccessor, logger, phoneNumberUtil) +{ + private static readonly ConcurrentDictionary LastSmsPerPhone = new(); + + public override Task SendSms(string messageText, string phoneNumber, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(messageText); + ArgumentException.ThrowIfNullOrEmpty(phoneNumber); + + LastSmsPerPhone.AddOrUpdate(phoneNumber, messageText, (_, _) => messageText); + return Task.CompletedTask; + } + + public static string GetLastSmsFor(string phoneNumber, string pattern) + { + ArgumentException.ThrowIfNullOrEmpty(phoneNumber); + ArgumentException.ThrowIfNullOrEmpty(pattern); + + if (LastSmsPerPhone.TryGetValue(phoneNumber, out var message) is false) + Assert.IsNotNull(message, "Sms has not sent"); + + if (pattern is not null && Regex.IsMatch(message, pattern) is false) + { + throw new AssertFailedException($""" + Sms text does not match. + expected pattern: {pattern} + actual text: {message} + """); + } + + return message; + } + + /// + /// Extracts the last OTP sent to the specified phone number. + /// + /// The phone number to check + /// The 6-digit OTP from the last SMS + /// Thrown when no valid OTP was found in the message + public static string GetLastOtpFor(string phoneNumber, string pattern) + { + ArgumentException.ThrowIfNullOrEmpty(phoneNumber); + ArgumentException.ThrowIfNullOrEmpty(pattern); + + var message = GetLastSmsFor(phoneNumber, pattern); + var otp = Regex.Match(message, @"\b\d{6}\b").Value; + Assert.IsNotNull(otp, $"No valid 6-digit OTP found in message: {message}"); + return otp; + } + + public static void Remove(string phoneNumber) + { + ArgumentException.ThrowIfNullOrEmpty(phoneNumber); + LastSmsPerPhone.TryRemove(phoneNumber, out _); + } + + public static void Clear() + { + LastSmsPerPhone.Clear(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestData.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestData.cs new file mode 100644 index 0000000000..d340722353 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestData.cs @@ -0,0 +1,8 @@ +namespace Boilerplate.Tests; + +public partial class TestData +{ + public const string DefaultTestEmail = "test@bitplatform.dev"; + public const string DefaultTestPassword = "123456"; + public const string DefaultTestFullName = "Boilerplate test account"; +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs index 743c65c37a..b76cf41d50 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs @@ -68,7 +68,7 @@ private static async Task InitializeAuthenticationState(AppTestServer testServer await signinPage.Open(); await signinPage.AssertOpen(); - var signedInPage = await signinPage.SignIn(); + var signedInPage = await signinPage.SignInWithEmail(); await signedInPage.AssertSignInSuccess(); var state = await playwrightPage.Page.Context.StorageStateAsync();