[Fixes #1638] Send confirmation email doesn't generate the callback URI correctly

This commit is contained in:
Javier Calvarro Nelson 2018-02-17 20:09:28 -08:00
parent d1aa7d527b
commit 2511862a77
12 changed files with 166 additions and 30 deletions

View File

@ -8,7 +8,7 @@
@Html.Partial("_StatusMessage", Model.StatusMessage)
<div class="row">
<div class="col-md-6">
<form method="post">
<form id="profile-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Username"></label>
@ -26,7 +26,7 @@
else
{
<input asp-for="Input.Email" class="form-control" />
<button asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>

View File

@ -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,

View File

@ -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)
};

View File

@ -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<string>(nameof(AuthenticatorKey));
@ -58,5 +61,11 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
get => GetValue<bool>(nameof(ExistingUser));
set => SetValue(nameof(ExistingUser), value);
}
public bool EmailConfirmed
{
get => GetValue<bool>(nameof(ExistingUser));
set => SetValue(nameof(ExistingUser), value);
}
}
}

View File

@ -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<IdentityOptions>(o => o.SignIn.RequireConfirmedEmail = true);
}
}

View File

@ -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<IEmailSender>(testEmailSender)
.Configure<IdentityOptions>(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<IEmailSender>(testEmailSender)
.Configure<IdentityOptions>(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<IHtmlAnchorElement>(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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<ConfirmEmail> 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);
}
}
}

View File

@ -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<TwoFactorAuthentication> ClickTwoFactorLinkAsync()
@ -33,5 +41,17 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
return new TwoFactorAuthentication(Client, twoFactor, Context);
}
internal async Task<Index> 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);
}
}
}

View File

@ -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<Account.Manage.Index> SendEmailConfirmationLinkAsync(Index index)
{
var manage = await index.ClickManageLinkAsync();
return await manage.SendConfirmationEmailAsync();
}
internal static async Task<Index> 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<ConfirmEmail> ConfirmEmailAsync(IdentityEmail email, HttpClient client)
{
var emailBody = HtmlAssert.IsHtmlFragment(email.Body);
var linkElement = HtmlAssert.HasElement("a", emailBody);
var link = Assert.IsAssignableFrom<IHtmlAnchorElement>(linkElement);
return await ConfirmEmail.Create(link, client, new DefaultUIContext()
.WithAuthenticatedUser()
.WithExistingUser()
.WithConfirmedEmail());
}
}
}

View File

@ -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<IdentityEmail> SentEmails { get; set; } = new List<IdentityEmail>();
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
SentEmails.Add(new IdentityEmail(email, subject, htmlMessage));
return Task.CompletedTask;
}
}
}

View File

@ -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; }
}
}