From 2511862a774557e55a93e27865b58a89c54b9fc7 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Sat, 17 Feb 2018 20:09:28 -0800 Subject: [PATCH] [Fixes #1638] Send confirmation email doesn't generate the callback URI correctly --- .../Pages/Account/Manage/Index.cshtml | 4 +-- .../Pages/Account/Manage/Index.cshtml.cs | 2 +- .../Extensions/HttpClientExtensions.cs | 9 ++++- .../Infrastructure/DefaultUIContext.cs | 9 +++++ ...ctionalTestsServiceCollectionExtensions.cs | 7 ++++ test/Identity.FunctionalTests/LoginTests.cs | 35 +++++-------------- .../ManagementTests.cs | 22 ++++++++++++ .../Pages/Account/ConfirmEmail.cs | 27 ++++++++++++++ .../Pages/Account/Manage/Index.cs | 20 +++++++++++ test/Identity.FunctionalTests/UserStories.cs | 21 +++++++++++ .../Services/ContosoEmailSender.cs | 19 ++++++++++ .../Services/IdentityEmail.cs | 21 +++++++++++ 12 files changed, 166 insertions(+), 30 deletions(-) create mode 100644 test/Identity.FunctionalTests/Pages/Account/ConfirmEmail.cs create mode 100644 test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoEmailSender.cs create mode 100644 test/WebSites/Identity.DefaultUI.WebSite/Services/IdentityEmail.cs diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml b/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml index 4fc98a0c14..2f803385ea 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml +++ b/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml @@ -8,7 +8,7 @@ @Html.Partial("_StatusMessage", Model.StatusMessage)
-
+
@@ -26,7 +26,7 @@ else { - + }
diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs index 7b29c21f31..6f84a1e903 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, - values: new { user.Id, code }, + values: new { userId = user.Id, code = code }, protocol: Request.Scheme); await _emailSender.SendEmailAsync( user.Email, diff --git a/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs b/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs index 08bb305fb7..17c8be64b6 100644 --- a/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs +++ b/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; @@ -43,7 +44,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests } var submit = form.GetSubmission(submitButton); - var submision = new HttpRequestMessage(new HttpMethod(submit.Method.ToString()), submit.Target) + var target = (Uri)submit.Target; + if (submitButton.HasAttribute("formaction")) + { + var formaction = submitButton.GetAttribute("formaction"); + target = new Uri(formaction, UriKind.Relative); + } + var submision = new HttpRequestMessage(new HttpMethod(submit.Method.ToString()), target) { Content = new StreamContent(submit.Body) }; diff --git a/test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs b/test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs index de4484ba75..727b7932e5 100644 --- a/test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs +++ b/test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs @@ -25,6 +25,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests public DefaultUIContext WithExistingUser() => new DefaultUIContext(this) { ExistingUser = true }; + public DefaultUIContext WithConfirmedEmail() => + new DefaultUIContext(this) { EmailConfirmed = true }; + public string AuthenticatorKey { get => GetValue(nameof(AuthenticatorKey)); @@ -58,5 +61,11 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests get => GetValue(nameof(ExistingUser)); set => SetValue(nameof(ExistingUser), value); } + + public bool EmailConfirmed + { + get => GetValue(nameof(ExistingUser)); + set => SetValue(nameof(ExistingUser), value); + } } } diff --git a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs index ec2c97ce81..00d4877082 100644 --- a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs +++ b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Identity.DefaultUI.WebSite; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -18,5 +19,11 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests services.AddAuthentication() .AddContosoAuthentication(o => o.SignInScheme = IdentityConstants.ExternalScheme) .Services; + + public static IServiceCollection SetupTestEmailSender(this IServiceCollection services, IEmailSender sender) => + services.AddSingleton(sender); + + public static IServiceCollection SetupEmailRequired(this IServiceCollection services) => + services.Configure(o => o.SignIn.RequireConfirmedEmail = true); } } diff --git a/test/Identity.FunctionalTests/LoginTests.cs b/test/Identity.FunctionalTests/LoginTests.cs index 1b2a326657..9fde74fc53 100644 --- a/test/Identity.FunctionalTests/LoginTests.cs +++ b/test/Identity.FunctionalTests/LoginTests.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Threading.Tasks; using AngleSharp.Dom.Html; +using Identity.DefaultUI.WebSite.Services; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -78,12 +79,12 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests public async Task CannotLogInWithoutRequiredEmailConfirmation() { // Arrange - var testEmailSender = new TestEmailSender(); + var emailSender = new ContosoEmailSender(); var server = ServerFactory.CreateServer(builder => { builder.ConfigureServices(services => services - .AddSingleton(testEmailSender) - .Configure(opt => opt.SignIn.RequireConfirmedEmail = true)); + .SetupTestEmailSender(emailSender) + .SetupEmailRequired()); }); var client = ServerFactory.CreateDefaultClient(server); @@ -103,12 +104,12 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests public async Task CanLogInAfterConfirmingEmail() { // Arrange - TestEmailSender testEmailSender = new TestEmailSender(); + var emailSender = new ContosoEmailSender(); var server = ServerFactory.CreateServer(builder => { builder.ConfigureServices(services => services - .AddSingleton(testEmailSender) - .Configure(opt => opt.SignIn.RequireConfirmedEmail = true)); + .SetupTestEmailSender(emailSender) + .SetupEmailRequired()); }); var client = ServerFactory.CreateDefaultClient(server); @@ -121,10 +122,8 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests // Act & Assert // Use a new client to simulate a new browser session. - var emailBody = HtmlAssert.IsHtmlFragment(testEmailSender.HtmlMessage); - var linkElement = HtmlAssert.HasElement("a", emailBody); - var link = Assert.IsAssignableFrom(linkElement); - var response = await newClient.GetAsync(link.Href); + var email = Assert.Single(emailSender.SentEmails); + await UserStories.ConfirmEmailAsync(email, newClient); await UserStories.LoginExistingUserAsync(newClient, userName, password); } @@ -147,20 +146,4 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests await UserStories.LoginWithSocialLoginAsync(newClient, userName); } } - - class TestEmailSender : IEmailSender - { - public string Email { get; private set; } - public string Subject { get; private set; } - public string HtmlMessage { get; private set; } - - public Task SendEmailAsync(string email, string subject, string htmlMessage) - { - Email = email; - Subject = subject; - HtmlMessage = htmlMessage; - - return Task.CompletedTask; - } - } } diff --git a/test/Identity.FunctionalTests/ManagementTests.cs b/test/Identity.FunctionalTests/ManagementTests.cs index f871994787..aa117ea68c 100644 --- a/test/Identity.FunctionalTests/ManagementTests.cs +++ b/test/Identity.FunctionalTests/ManagementTests.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Identity.DefaultUI.WebSite.Services; using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests @@ -23,5 +24,26 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests // Act & Assert await UserStories.EnableTwoFactorAuthentication(index); } + + [Fact] + public async Task CanConfirmEmail() + { + // Arrange + var emails = new ContosoEmailSender(); + var server = ServerFactory.CreateServer(builder => + builder.ConfigureServices(s => s.SetupTestEmailSender(emails))); + var client = ServerFactory.CreateDefaultClient(server); + + var userName = $"{Guid.NewGuid()}@example.com"; + var password = $"!Test.Password1$"; + + var index = await UserStories.RegisterNewUserAsync(client, userName, password); + var manageIndex = await UserStories.SendEmailConfirmationLinkAsync(index); + + // Act & Assert + Assert.Equal(2, emails.SentEmails.Count); + var email = emails.SentEmails[1]; + await UserStories.ConfirmEmailAsync(email, client); + } } } diff --git a/test/Identity.FunctionalTests/Pages/Account/ConfirmEmail.cs b/test/Identity.FunctionalTests/Pages/Account/ConfirmEmail.cs new file mode 100644 index 0000000000..b3207cb961 --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/ConfirmEmail.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Pages.Account +{ + public class ConfirmEmail : DefaultUIPage + { + public ConfirmEmail( + HttpClient client, + IHtmlDocument document, + DefaultUIContext context) : base(client, document, context) + { + } + + public static async Task Create(IHtmlAnchorElement link, HttpClient client, DefaultUIContext context) + { + var response = await client.GetAsync(link.Href); + var confirmEmail = await ResponseAssert.IsHtmlDocumentAsync(response); + + return new ConfirmEmail(client, confirmEmail, context); + } + } +} diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs index 20e9b81356..45fb58d684 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Net.Http; using System.Threading.Tasks; using AngleSharp.Dom.Html; @@ -14,6 +15,8 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage private readonly IHtmlAnchorElement _changePasswordLink; private readonly IHtmlAnchorElement _twoFactorLink; private readonly IHtmlAnchorElement _personalDataLink; + private readonly IHtmlFormElement _updateProfileForm; + private readonly IHtmlElement _confirmEmailButton; public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context) : base(client, manage, context) @@ -24,6 +27,11 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage _changePasswordLink = HtmlAssert.HasLink("#change-password", manage); _twoFactorLink = HtmlAssert.HasLink("#two-factor", manage); _personalDataLink = HtmlAssert.HasLink("#personal-data", manage); + _updateProfileForm = HtmlAssert.HasForm("#profile-form", manage); + if (!Context.EmailConfirmed) + { + _confirmEmailButton = HtmlAssert.HasElement("button#email-verification", manage); + } } public async Task ClickTwoFactorLinkAsync() @@ -33,5 +41,17 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage return new TwoFactorAuthentication(Client, twoFactor, Context); } + + internal async Task SendConfirmationEmailAsync() + { + Assert.False(Context.EmailConfirmed); + + var response = await Client.SendAsync(_updateProfileForm, _confirmEmailButton); + var goToManage = ResponseAssert.IsRedirect(response); + var manageResponse = await Client.GetAsync(goToManage); + var manage = await ResponseAssert.IsHtmlDocumentAsync(manageResponse); + + return new Index(Client, manage, Context); + } } } diff --git a/test/Identity.FunctionalTests/UserStories.cs b/test/Identity.FunctionalTests/UserStories.cs index cb81ac3a2b..9a42eb403a 100644 --- a/test/Identity.FunctionalTests/UserStories.cs +++ b/test/Identity.FunctionalTests/UserStories.cs @@ -4,7 +4,11 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using AngleSharp.Dom.Html; +using Identity.DefaultUI.WebSite.Services; using Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage; +using Microsoft.AspNetCore.Identity.FunctionalTests.Pages.Account; +using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests { @@ -43,6 +47,12 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return await externalLogin.SendEmailAsync(email); } + internal static async Task SendEmailConfirmationLinkAsync(Index index) + { + var manage = await index.ClickManageLinkAsync(); + return await manage.SendConfirmationEmailAsync(); + } + internal static async Task LoginWithSocialLoginAsync(HttpClient client, string userName) { var index = await Index.CreateAsync( @@ -93,5 +103,16 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return await loginRecoveryCode.SendRecoveryCodeAsync(recoveryCode); } + + internal static async Task ConfirmEmailAsync(IdentityEmail email, HttpClient client) + { + var emailBody = HtmlAssert.IsHtmlFragment(email.Body); + var linkElement = HtmlAssert.HasElement("a", emailBody); + var link = Assert.IsAssignableFrom(linkElement); + return await ConfirmEmail.Create(link, client, new DefaultUIContext() + .WithAuthenticatedUser() + .WithExistingUser() + .WithConfirmedEmail()); + } } } diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoEmailSender.cs b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoEmailSender.cs new file mode 100644 index 0000000000..b1aebaded3 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoEmailSender.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.UI.Services; + +namespace Identity.DefaultUI.WebSite.Services +{ + public class ContosoEmailSender : IEmailSender + { + public IList SentEmails { get; set; } = new List(); + + public Task SendEmailAsync(string email, string subject, string htmlMessage) + { + SentEmails.Add(new IdentityEmail(email, subject, htmlMessage)); + return Task.CompletedTask; + } + } +} diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Services/IdentityEmail.cs b/test/WebSites/Identity.DefaultUI.WebSite/Services/IdentityEmail.cs new file mode 100644 index 0000000000..dbf5c4d2f6 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Services/IdentityEmail.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Identity.DefaultUI.WebSite.Services +{ + public class IdentityEmail + { + public IdentityEmail(string to, string subject, string body) + { + To = to; + Subject = subject; + Body = body; + } + + public string To { get; } + public string Subject { get; } + public string Body { get; } + } +}